From 4f51307d9744de1a17759d1e5e94fc131c6783b9 Mon Sep 17 00:00:00 2001 From: Naman Jain Date: Thu, 19 Aug 2021 15:55:13 +0530 Subject: [PATCH 1/2] feat(acl): allow access to all the predicates using wildcard (#7991) There are usecases that need read/write/modify permissions over all the predicates of the namespace. It is quite tedious to manage the permissions every time a new predicate is created. This PR adds a feature to allow a group, access to all the predicates in the namespace using wildcard dgraph.all. This example provides to dev group, read+write access to all the predicates mutation { updateGroup( input: { filter: { name: { eq: "dev" } } set: { rules: [{ predicate: "dgraph.all", permission: 6 }] } } ) { group { name rules { permission predicate } } } } NOTE: The permission to a predicate for a group (say dev) is a union of permissions from dgraph.all and the permissions to specific predicate (say name). So suppose dgraph.all is given READ permission, while predicate name is given WRITE permission. Then the group will have both READ and WRITE permission. (cherry picked from commit 3504044d6942ae75594a5605623a0bdc4116f84b) --- edgraph/access_ee.go | 8 ++- ee/acl/acl_test.go | 160 +++++++++++++++++++++++++++++++++++++++++++ worker/acl_cache.go | 38 +++++++--- 3 files changed, 196 insertions(+), 10 deletions(-) diff --git a/edgraph/access_ee.go b/edgraph/access_ee.go index ad224a92dad..f10fb273c9d 100644 --- a/edgraph/access_ee.go +++ b/edgraph/access_ee.go @@ -642,8 +642,14 @@ func authorizePreds(ctx context.Context, userData *userData, preds []string, blockedPreds[pred] = struct{}{} } } + + if worker.HasAccessToAllPreds(ns, groupIds, aclOp) { + // Setting allowed to nil allows access to all predicates. Note that the access to ACL + // predicates will still be blocked. + return &authPredResult{allowed: nil, blocked: blockedPreds}, nil + } // User can have multiple permission for same predicate, add predicate - allowedPreds := make([]string, len(worker.AclCachePtr.GetUserPredPerms(userId))) + allowedPreds := make([]string, 0, len(worker.AclCachePtr.GetUserPredPerms(userId))) // only if the acl.Op is covered in the set of permissions for the user for predicate, perm := range worker.AclCachePtr.GetUserPredPerms(userId) { if (perm & aclOp.Code) > 0 { diff --git a/ee/acl/acl_test.go b/ee/acl/acl_test.go index 6a3a928c04a..76436ce39be 100644 --- a/ee/acl/acl_test.go +++ b/ee/acl/acl_test.go @@ -1683,6 +1683,166 @@ func TestValQueryWithACLPermissions(t *testing.T) { } } + +func TestAllPredsPermission(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) + defer cancel() + dg, err := testutil.DgraphClientWithGroot(testutil.SockAddr) + require.NoError(t, err) + + testutil.DropAll(t, dg) + + op := api.Operation{Schema: ` + name : string @index(exact) . + nickname : string @index(exact) . + age : int . + type TypeName { + name: string + nickname: string + age: int + } + `} + require.NoError(t, dg.Alter(ctx, &op)) + + resetUser(t) + + token, err := testutil.HttpLogin(&testutil.LoginParams{ + Endpoint: adminEndpoint, + UserID: "groot", + Passwd: "password", + Namespace: x.GalaxyNamespace, + }) + require.NoError(t, err, "login failed") + + createGroup(t, token, devGroup) + addToGroup(t, token, userid, devGroup) + + txn := dg.NewTxn() + mutation := &api.Mutation{ + SetNquads: []byte(` + _:a "RandomGuy" . + _:a "23" . + _:a "RG" . + _:a "TypeName" . + _:b "RandomGuy2" . + _:b "25" . + _:b "RG2" . + _:b "TypeName" . + `), + CommitNow: true, + } + _, err = txn.Mutate(ctx, mutation) + require.NoError(t, err) + + query := `{q1(func: has(name)){ + v as name + a as age + } + q2(func: eq(val(v), "RandomGuy")) { + val(v) + val(a) + }}` + + // Test that groot has access to all the predicates + resp, err := dg.NewReadOnlyTxn().Query(ctx, query) + require.NoError(t, err, "Error while querying data") + testutil.CompareJSON(t, `{"q1":[{"name":"RandomGuy","age":23},{"name":"RandomGuy2","age":25}],"q2":[{"val(v)":"RandomGuy","val(a)":23}]}`, + string(resp.GetJson())) + + // All test cases + tests := []struct { + input string + descriptionNoPerm string + outputNoPerm string + descriptionNamePerm string + outputNamePerm string + descriptionNameAgePerm string + outputNameAgePerm string + }{ + { + ` + { + q1(func: has(name), orderasc: name) { + n as name + a as age + } + q2(func: eq(val(n), "RandomGuy")) { + val(n) + val(a) + } + } + `, + "alice doesn't have access to name or age", + `{}`, + + `alice has access to name`, + `{"q1":[{"name":"RandomGuy"},{"name":"RandomGuy2"}],"q2":[{"val(n)":"RandomGuy"}]}`, + + "alice has access to name and age", + `{"q1":[{"name":"RandomGuy","age":23},{"name":"RandomGuy2","age":25}],"q2":[{"val(n)":"RandomGuy","val(a)":23}]}`, + }, + } + + userClient, err := testutil.DgraphClient(testutil.SockAddr) + require.NoError(t, err) + time.Sleep(defaultTimeToSleep) + + err = userClient.LoginIntoNamespace(ctx, userid, userpassword, x.GalaxyNamespace) + require.NoError(t, err) + + // Query via user when user has no permissions + for _, tc := range tests { + desc := tc.descriptionNoPerm + t.Run(desc, func(t *testing.T) { + resp, err := userClient.NewTxn().Query(ctx, tc.input) + require.NoError(t, err) + testutil.CompareJSON(t, tc.outputNoPerm, string(resp.Json)) + }) + } + + // Login to groot to modify accesses (1) + token, err = testutil.HttpLogin(&testutil.LoginParams{ + Endpoint: adminEndpoint, + UserID: "groot", + Passwd: "password", + Namespace: x.GalaxyNamespace, + }) + require.NoError(t, err, "login failed") + + // Give read access of all predicates to dev + addRulesToGroup(t, token, devGroup, []rule{{"dgraph.all", Read.Code}}) + time.Sleep(defaultTimeToSleep) + + for _, tc := range tests { + desc := tc.descriptionNameAgePerm + t.Run(desc, func(t *testing.T) { + resp, err := userClient.NewTxn().Query(ctx, tc.input) + require.NoError(t, err) + testutil.CompareJSON(t, tc.outputNameAgePerm, string(resp.Json)) + }) + } + + // Mutation shall fail. + mutation = &api.Mutation{ + SetNquads: []byte(` + _:a "RandomGuy" . + _:a "23" . + _:a "TypeName" . + `), + CommitNow: true, + } + txn = userClient.NewTxn() + _, err = txn.Mutate(ctx, mutation) + require.Error(t, err) + require.Contains(t, err.Error(), "unauthorized to mutate") + + // Give write access of all predicates to dev. Now mutation should succeed. + addRulesToGroup(t, token, devGroup, []rule{{"dgraph.all", Write.Code | Read.Code}}) + time.Sleep(defaultTimeToSleep) + txn = userClient.NewTxn() + _, err = txn.Mutate(ctx, mutation) + require.NoError(t, err) +} func TestNewACLPredicates(t *testing.T) { ctx, _ := context.WithTimeout(context.Background(), 100*time.Second) diff --git a/worker/acl_cache.go b/worker/acl_cache.go index 4a02ea69bbc..fc181218dbc 100644 --- a/worker/acl_cache.go +++ b/worker/acl_cache.go @@ -139,18 +139,17 @@ func (cache *AclCache) Update(ns uint64, groups []acl.Group) { func (cache *AclCache) AuthorizePredicate(groups []string, predicate string, operation *acl.Operation) error { - if x.IsAclPredicate(x.ParseAttr(predicate)) { + ns, attr := x.ParseNamespaceAttr(predicate) + if x.IsAclPredicate(attr) { return errors.Errorf("only groot is allowed to access the ACL predicate: %s", predicate) } - AclCachePtr.RLock() - predPerms := AclCachePtr.predPerms - AclCachePtr.RUnlock() - - if groupPerms, found := predPerms[predicate]; found { - if hasRequiredAccess(groupPerms, groups, operation) { - return nil - } + // Check if group has access to all the predicates (using "dgraph.all" wildcard). + if HasAccessToAllPreds(ns, groups, operation) { + return nil + } + if hasAccessToPred(predicate, groups, operation) { + return nil } // no rule has been defined that can match the predicate @@ -160,6 +159,27 @@ func (cache *AclCache) AuthorizePredicate(groups []string, predicate string, } +// accessAllPredicate is a wildcard to allow access to all non-ACL predicates to non-guardian group. +const accessAllPredicate = "dgraph.all" + +func HasAccessToAllPreds(ns uint64, groups []string, operation *acl.Operation) bool { + pred := x.NamespaceAttr(ns, accessAllPredicate) + return hasAccessToPred(pred, groups, operation) +} + +func hasAccessToPred(pred string, groups []string, operation *acl.Operation) bool { + AclCachePtr.RLock() + defer AclCachePtr.RUnlock() + predPerms := AclCachePtr.predPerms + + if groupPerms, found := predPerms[pred]; found { + if hasRequiredAccess(groupPerms, groups, operation) { + return true + } + } + return false +} + // 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, From fdbc95ee628403f8831dff75e44e40b045179699 Mon Sep 17 00:00:00 2001 From: Naman Jain Date: Wed, 18 Aug 2021 17:13:18 +0530 Subject: [PATCH 2/2] fix(acl): subscribe for the correct predicates (#7992) We were subscribing to the wrong predicates. Hence the ACL cache was not getting updated. (cherry picked from commit 1b75c01db6de51f537e2713f2de10316950d7013) --- edgraph/access_ee.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/edgraph/access_ee.go b/edgraph/access_ee.go index f10fb273c9d..6fe746ed7f0 100644 --- a/edgraph/access_ee.go +++ b/edgraph/access_ee.go @@ -404,8 +404,8 @@ const queryAcls = ` ` var aclPrefixes = [][]byte{ - x.PredicatePrefix(x.GalaxyAttr("dgraph.acl.permission")), - x.PredicatePrefix(x.GalaxyAttr("dgraph.acl.predicate")), + x.PredicatePrefix(x.GalaxyAttr("dgraph.rule.permission")), + x.PredicatePrefix(x.GalaxyAttr("dgraph.rule.predicate")), x.PredicatePrefix(x.GalaxyAttr("dgraph.acl.rule")), x.PredicatePrefix(x.GalaxyAttr("dgraph.user.group")), x.PredicatePrefix(x.GalaxyAttr("dgraph.type.Group")),