Skip to content

Commit

Permalink
addressed Manish's comments
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucas Wang committed Feb 12, 2019
1 parent dbf92b1 commit fe34e26
Show file tree
Hide file tree
Showing 8 changed files with 66 additions and 191 deletions.
21 changes: 13 additions & 8 deletions dgraph/cmd/alpha/login_ee.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,13 @@
package alpha

import (
"bytes"
"context"
"fmt"
"encoding/json"
"net/http"

"github.com/dgraph-io/dgo/protos/api"
"github.com/dgraph-io/dgraph/edgraph"
"github.com/dgraph-io/dgraph/x"
"github.com/golang/glog"
)

func loginHandler(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -55,12 +53,19 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
x.SetStatusWithData(w, x.Error, err.Error())
}

var out bytes.Buffer
out.WriteString(fmt.Sprintf("ACCESS JWT:\n%s\n", jwt.AccessJwt))
out.WriteString(fmt.Sprintf("REFRESH JWT:\n%s\n", jwt.RefreshJwt))
if _, err := writeResponse(w, r, out.Bytes()); err != nil {
glog.Errorf("Error while writing response: %v", err)
response := map[string]interface{}{}
mp := map[string]interface{}{}
mp["accessJWT"] = jwt.AccessJwt
mp["refreshJWT"] = jwt.RefreshJwt
response["data"] = mp

js, err := json.Marshal(response)
if err != nil {
x.SetStatusWithData(w, x.Error, err.Error())
return
}

writeResponse(w, r, js)
}

func init() {
Expand Down
5 changes: 2 additions & 3 deletions dgraph/cmd/alpha/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,8 @@ they form a Raft group and provide synchronous replication.
// with the flag name so that the values are picked up by Cobra/Viper's various config inputs
// (e.g, config file, env vars, cli flags, etc.)
flag := Alpha.Cmd.Flags()
flag.Bool("enterprise_features", false,
"Enable Dgraph enterprise features. "+
"If you set this to true, you agree to the Dgraph Community License.")
flag.Bool("enterprise_features", false, "Enable Dgraph enterprise features. "+
"If you set this to true, you agree to the Dgraph Community License.")
flag.StringP("postings", "p", "p", "Directory to store posting lists.")

// Options around how to set up Badger.
Expand Down
2 changes: 1 addition & 1 deletion dgraph/cmd/zero/zero.go
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ func (s *Server) ShouldServe(
var proposal pb.ZeroProposal
// Multiple Groups might be assigned to same tablet, so during proposal we will check again.
tablet.Force = false
if _, isAclPred := x.AclPreds[tablet.Predicate]; isAclPred {
if x.IsAclPredicate(tablet.Predicate) {
// force all the acl predicates to be allocated to group 1
// this is to make it eaiser to stream ACL updates to all alpha servers
// since they only need to open one pipeline to receive updates for all ACL predicates
Expand Down
151 changes: 9 additions & 142 deletions edgraph/access_ee.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,8 @@ package edgraph

import (
"context"
"encoding/json"
"fmt"
"regexp"
"strings"
"sync"
"time"

"github.com/pkg/errors"
Expand Down Expand Up @@ -312,77 +309,7 @@ func RefreshAcls(closer *y.Closer) {
return err
}

// In dgraph, acl rules are divided by groups, e.g.
// the dev group has the following blob representing its ACL rules
// [friend, 4], [name, 7], [^user.*name$, 4]
// where friend and name are predicates,
// and the last one is a regex that can match multiple predicates.
// However in the aclCache in memory, we need to change the structure so that ACL rules are
// divided by predicates, e.g.
// friend ->
// dev -> 4
// sre -> 6
// name ->
// dev -> 7
// the reason is that we want to efficiently determine if any ACL rule has been defined
// for a given predicate, and allow the operation if none is defined, per the fail open
// approach

// predPerms is the map descriebed above that maps a single
// predicate to a submap, and the submap maps a group to a permission
predPerms := make(map[string]map[string]int32)
// predRegexPerms is a map from a regex string to a PredRegexRule, and a PredRegexRule
// contains a map from a group to a permission
predRegexPerms := make(map[string]*PredRegexRule)
for _, group := range groups {
aclBytes := []byte(group.Acls)
var acls []acl.Acl
if err := json.Unmarshal(aclBytes, &acls); err != nil {
glog.Errorf("Unable to unmarshal the aclBytes: %v", err)
continue
}

for _, acl := range acls {
if len(acl.Predicate) > 0 {
if groupPerms, found := predPerms[acl.Predicate]; found {
groupPerms[group.GroupID] = acl.Perm
} else {
groupPerms := make(map[string]int32)
groupPerms[group.GroupID] = acl.Perm
predPerms[acl.Predicate] = groupPerms
}
} else if len(acl.Regex) > 0 {
if predRegexRule, found := predRegexPerms[acl.Regex]; found {
predRegexRule.groupPerms[group.GroupID] = acl.Perm
} else {
predRegex, err := regexp.Compile(acl.Regex)
if err != nil {
glog.Errorf("Unable to compile the predicate regex %v "+
"to create an ACL rule", acl.Regex)
continue
}

groupPermsMap := make(map[string]int32)
groupPermsMap[group.GroupID] = acl.Perm
predRegexPerms[acl.Regex] = &PredRegexRule{
predRegex: predRegex,
groupPerms: groupPermsMap,
}
}
}
}
}

// convert the predRegexPerms into a slice
var predRegexRules []*PredRegexRule
for _, predRegexRule := range predRegexPerms {
predRegexRules = append(predRegexRules, predRegexRule)
}

aclCache.Lock()
aclCache.predPerms = predPerms
aclCache.predRegexRules = predRegexRules
aclCache.Unlock()
aclCache.update(groups)
glog.V(3).Infof("Updated the ACL cache")
return nil
}
Expand All @@ -408,20 +335,6 @@ const queryAcls = `
}
`

type PredRegexRule struct {
predRegex *regexp.Regexp
groupPerms map[string]int32
}

// the acl cache mapping group names to the corresponding group acls
type AclCache struct {
sync.RWMutex
predPerms map[string]map[string]int32
predRegexRules []*PredRegexRule
}

var aclCache AclCache

// clear the aclCache and upsert the Groot account.
func ResetAcl() {
if len(Config.HmacSecret) == 0 {
Expand Down Expand Up @@ -479,7 +392,7 @@ func ResetAcl() {
return nil
}

aclCache = AclCache{
aclCache = &AclCache{
predPerms: make(map[string]map[string]int32),
predRegexRules: make([]*PredRegexRule, 0),
}
Expand Down Expand Up @@ -532,18 +445,20 @@ func authorizeAlter(ctx context.Context, op *api.Operation) error {
// treat the user as an anonymous guest who has not joined any group yet
// such a user can still get access to predicates that have no ACL rule defined, per the
// fail open approach
userId = "anonymous"
} else {
return status.Error(codes.Unauthenticated, err.Error())
}

// if we get here, we know the user is not Groot.
if op.DropAll {
return fmt.Errorf("only Groot is allowed to drop all data")
return fmt.Errorf("only Groot is allowed to drop all data, but the current user is %s",
userId)
}

if len(op.DropAttr) > 0 {
// check that we have the modify permission on the predicate
err := authorizePredicate(groupIds, op.DropAttr, acl.Modify)
err := aclCache.authorizePredicate(groupIds, op.DropAttr, acl.Modify)
logACLAccess(&ACLAccessLog{
userId: userId,
groups: groupIds,
Expand All @@ -564,7 +479,7 @@ func authorizeAlter(ctx context.Context, op *api.Operation) error {
return err
}
for _, update := range update.Schemas {
err := authorizePredicate(groupIds, update.Predicate, acl.Modify)
err := aclCache.authorizePredicate(groupIds, update.Predicate, acl.Modify)
logACLAccess(&ACLAccessLog{
userId: userId,
groups: groupIds,
Expand Down Expand Up @@ -617,7 +532,7 @@ func authorizeMutation(ctx context.Context, mu *api.Mutation) error {
return err
}
for pred := range parsePredsFromMutation(gmu.Set) {
err := authorizePredicate(groupIds, pred, acl.Write)
err := aclCache.authorizePredicate(groupIds, pred, acl.Write)
logACLAccess(&ACLAccessLog{
userId: userId,
groups: groupIds,
Expand Down Expand Up @@ -706,7 +621,7 @@ func authorizeQuery(ctx context.Context, req *api.Request) error {
}

for pred := range parsePredsFromQuery(parsedReq.Query) {
err := authorizePredicate(groupIds, pred, acl.Read)
err := aclCache.authorizePredicate(groupIds, pred, acl.Read)
logACLAccess(&ACLAccessLog{
userId: userId,
groups: groupIds,
Expand All @@ -721,51 +636,3 @@ func authorizeQuery(ctx context.Context, req *api.Request) error {
}
return nil
}

// hasRequiredAccess checks if any group in the passed in groups is allowed to perform the operation
// according to the acl rules stored in groupPerms
func hasRequiredAccess(groupPerms map[string]int32, groups []string,
operation *acl.Operation) bool {
for _, group := range groups {
groupPerm, found := groupPerms[group]
if found && (groupPerm&operation.Code != 0) {
return true
}
}
return false
}

func authorizePredicate(groups []string, predicate string, operation *acl.Operation) error {
aclCache.RLock()
predPerms, predRegexRules := aclCache.predPerms, aclCache.predRegexRules
aclCache.RUnlock()

var singlePredMatch bool
if groupPerms, found := predPerms[predicate]; found {
singlePredMatch = true
if hasRequiredAccess(groupPerms, groups, operation) {
return nil
}
}

var predRegexMatch bool
for _, predRegexRule := range predRegexRules {
if predRegexRule.predRegex.MatchString(predicate) {
predRegexMatch = true
if hasRequiredAccess(predRegexRule.groupPerms, groups, operation) {
return nil
}
}
}

if singlePredMatch || predRegexMatch {
// there is an ACL rule defined that can match the predicate
// and the operation has not been allowed
return fmt.Errorf("unauthorized to do %s on predicate %s",
operation.Name, predicate)
}

// no rule has been defined that can match the predicate
// by default we follow the fail open approach and allow the operation
return nil
}
47 changes: 23 additions & 24 deletions ee/acl/acl_curl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ import (
"time"

"github.com/dgraph-io/dgraph/x"
"github.com/golang/glog"
"github.com/stretchr/testify/require"
)

func TestCurlAuthorization(t *testing.T) {
t.Logf("testing with port 9180")
glog.Infof("testing with port 9180")
dg, cancel := x.GetDgraphClientOnPort(9180)
defer cancel()
createAccountAndData(t, dg)
Expand Down Expand Up @@ -66,7 +67,7 @@ func TestCurlAuthorization(t *testing.T) {
// sleep long enough (longer than 10s, the access JWT TTL defined in the docker-compose.yml
// in this directory) for the accessJwt to expire, in order to test auto login through refresh
// JWT
t.Logf("Sleeping for 12 seconds for accessJwt to expire")
glog.Infof("Sleeping for 12 seconds for accessJwt to expire")
time.Sleep(12 * time.Second)
verifyCurlCmd(t, queryArgs(), &FailureConfig{
shouldFail: true,
Expand All @@ -89,7 +90,7 @@ func TestCurlAuthorization(t *testing.T) {

createGroupAndAcls(t, unusedGroup, false)
// wait for 35 seconds to ensure the new acl have reached all acl caches
t.Logf("Sleeping for 35 seconds for acl caches to be refreshed")
glog.Infof("Sleeping for 35 seconds for acl caches to be refreshed")
time.Sleep(35 * time.Second)
verifyCurlCmd(t, queryArgs(), &FailureConfig{
shouldFail: true,
Expand All @@ -113,7 +114,7 @@ func TestCurlAuthorization(t *testing.T) {
})

createGroupAndAcls(t, devGroup, true)
t.Logf("Sleeping for 35 seconds for acl caches to be refreshed")
glog.Infof("Sleeping for 35 seconds for acl caches to be refreshed")
time.Sleep(35 * time.Second)
// refresh the jwts again
accessJwt, refreshJwt = curlLogin(t, refreshJwt)
Expand Down Expand Up @@ -155,26 +156,25 @@ func curlLogin(t *testing.T, refreshJwt string) (string, string) {
out, err := userLoginCmd.Output()
require.NoError(t, err, "the login should have succeeded")

// search for access JWT and refresh JWT in the curl output
outputLines := strings.Split(string(out), "\n")
var newAccessJwt string
var newRefreshJwt string
for idx := 0; idx < len(outputLines); idx++ {
line := outputLines[idx]
if line == "ACCESS JWT:" {
idx++
require.True(t, idx < len(outputLines),
"no line found after ACCESS JWT")
newAccessJwt = outputLines[idx]
} else if line == "REFRESH JWT:" {
idx++
require.True(t, idx < len(outputLines),
"no line found after REFRESH JWT")
newRefreshJwt = outputLines[idx]
}
var outputJson map[string]map[string]string
if err := json.Unmarshal(out, &outputJson); err != nil {
t.Fatal("unable to unmarshal the output to get JWTs")
}
glog.Infof("got output: %v", outputJson)

data, found := outputJson["data"]
if !found {
t.Fatal("no data entry found in the output")
}

newAccessJwt, found := data["accessJWT"]
if !found {
t.Fatal("no access JWT found in the output")
}
newRefreshJwt, found := data["refreshJWT"]
if !found {
t.Fatal("no refresh JWT found in the output")
}
require.True(t, len(newAccessJwt) > 0, "no access jwt received")
require.True(t, len(newRefreshJwt) > 0, "no refresh jwt received")

return newAccessJwt, newRefreshJwt
}
Expand Down Expand Up @@ -215,7 +215,6 @@ func verifyOutput(t *testing.T, bytes []byte, failureConfig *FailureConfig) {

func verifyCurlCmd(t *testing.T, args []string,
failureConfig *FailureConfig) {
//t.Logf("curl %s\n", strings.Join(args, " "))
queryCmd := exec.Command("curl", args...)

output, err := queryCmd.Output()
Expand Down
Loading

0 comments on commit fe34e26

Please sign in to comment.