diff --git a/dgraph/cmd/alpha/run.go b/dgraph/cmd/alpha/run.go index 58d8cc812e0..54e8d42fd52 100644 --- a/dgraph/cmd/alpha/run.go +++ b/dgraph/cmd/alpha/run.go @@ -814,8 +814,9 @@ func run() { // initialization of the admin account can only be done after raft nodes are running // and health check passes - edgraph.ResetAcl(updaters) - edgraph.RefreshAcls(updaters) + edgraph.InitializeAcl(updaters) + edgraph.RefreshACLs(updaters.Ctx()) + edgraph.SubscribeForAclUpdates(updaters) }() // Graphql subscribes to alpha to get schema updates. We need to close that before we diff --git a/edgraph/access.go b/edgraph/access.go index 88bb5b985b8..3c1ad80a680 100644 --- a/edgraph/access.go +++ b/edgraph/access.go @@ -42,17 +42,26 @@ func (s *Server) Login(ctx context.Context, } // ResetAcl is an empty method since ACL is only supported in the enterprise version. -func ResetAcl(closer *z.Closer) { +func InitializeAcl(closer *z.Closer) { // do nothing } -// ResetAcls is an empty method since ACL is only supported in the enterprise version. -func RefreshAcls(closer *z.Closer) { +func upsertGuardianAndGroot(closer *z.Closer, ns uint64) { + // do nothing +} + +// SubscribeForAclUpdates is an empty method since ACL is only supported in the enterprise version. +func SubscribeForAclUpdates(closer *z.Closer) { // do nothing <-closer.HasBeenClosed() closer.Done() } +// RefreshACLs is an empty method since ACL is only supported in the enterprise version. +func RefreshACLs(ctx context.Context) { + return +} + func authorizeAlter(ctx context.Context, op *api.Operation) error { return nil } diff --git a/edgraph/access_ee.go b/edgraph/access_ee.go index dff3eb26dd8..42128193d68 100644 --- a/edgraph/access_ee.go +++ b/edgraph/access_ee.go @@ -312,8 +312,43 @@ func authorizeUser(ctx context.Context, userid string, password string) ( return user, nil } -// RefreshAcls queries for the ACL triples and refreshes the ACLs accordingly. -func RefreshAcls(closer *z.Closer) { +func refreshAclCache(ctx context.Context, ns, refreshTs uint64) error { + req := &Request{ + req: &api.Request{ + Query: queryAcls, + ReadOnly: true, + StartTs: refreshTs, + }, + doAuth: NoAuthorize, + } + + ctx = x.AttachNamespace(ctx, ns) + queryResp, err := (&Server{}).doQuery(ctx, req) + if err != nil { + return errors.Errorf("unable to retrieve acls: %v", err) + } + groups, err := acl.UnmarshalGroups(queryResp.GetJson(), "allAcls") + if err != nil { + return err + } + + worker.AclCachePtr.Update(ns, groups) + glog.V(2).Infof("Updated the ACL cache for namespace: %#x", ns) + return nil + +} + +func RefreshACLs(ctx context.Context) { + for ns := range schema.State().Namespaces() { + if err := refreshAclCache(ctx, ns, 0); err != nil { + glog.Errorf("Error while retrieving acls for namespace %#x: %v", ns, err) + } + } + worker.AclCachePtr.Set() +} + +// SubscribeForAclUpdates subscribes for ACL predicates and updates the acl cache. +func SubscribeForAclUpdates(closer *z.Closer) { defer func() { glog.Infoln("RefreshAcls closed") closer.Done() @@ -323,38 +358,13 @@ func RefreshAcls(closer *z.Closer) { return } - // retrieve the full data set of ACLs from the corresponding alpha server, and update the - // aclCachePtr var maxRefreshTs uint64 retrieveAcls := func(ns uint64, refreshTs uint64) error { if refreshTs <= maxRefreshTs { return nil } maxRefreshTs = refreshTs - - glog.V(3).Infof("Refreshing ACLs") - req := &Request{ - req: &api.Request{ - Query: queryAcls, - ReadOnly: true, - StartTs: refreshTs, - }, - doAuth: NoAuthorize, - } - - ctx := x.AttachNamespace(closer.Ctx(), ns) - queryResp, err := (&Server{}).doQuery(ctx, req) - if err != nil { - return errors.Errorf("unable to retrieve acls: %v", err) - } - groups, err := acl.UnmarshalGroups(queryResp.GetJson(), "allAcls") - if err != nil { - return err - } - - aclCachePtr.update(ns, groups) - glog.V(3).Infof("Updated the ACL cache") - return nil + return refreshAclCache(closer.Ctx(), ns, refreshTs) } closer.AddRunning(1) @@ -402,10 +412,10 @@ var aclPrefixes = [][]byte{ x.PredicatePrefix(x.GalaxyAttr("dgraph.xid")), } -// clears the aclCachePtr and upserts the Groot account. -func ResetAcl(closer *z.Closer) { +// upserts the Groot account. +func InitializeAcl(closer *z.Closer) { defer func() { - glog.Infof("ResetAcl closed") + glog.Infof("InitializeAcl closed") closer.Done() }() @@ -611,12 +621,16 @@ func authorizePreds(ctx context.Context, userData *userData, preds []string, if err != nil { return nil, errors.Wrapf(err, "While authorizing preds") } + if !worker.AclCachePtr.Loaded() { + RefreshACLs(ctx) + } + userId := userData.userId groupIds := userData.groupIds blockedPreds := make(map[string]struct{}) for _, pred := range preds { nsPred := x.NamespaceAttr(ns, pred) - if err := aclCachePtr.authorizePredicate(groupIds, nsPred, aclOp); err != nil { + if err := worker.AclCachePtr.AuthorizePredicate(groupIds, nsPred, aclOp); err != nil { logAccess(&accessEntry{ userId: userId, groups: groupIds, @@ -628,22 +642,20 @@ func authorizePreds(ctx context.Context, userData *userData, preds []string, blockedPreds[pred] = struct{}{} } } - aclCachePtr.RLock() - allowedPreds := make([]string, len(aclCachePtr.userPredPerms[userId])) // User can have multiple permission for same predicate, add predicate + allowedPreds := make([]string, len(worker.AclCachePtr.GetUserPredPerms(userId))) // only if the acl.Op is covered in the set of permissions for the user - for predicate, perm := range aclCachePtr.userPredPerms[userId] { + for predicate, perm := range worker.AclCachePtr.GetUserPredPerms(userId) { if (perm & aclOp.Code) > 0 { allowedPreds = append(allowedPreds, predicate) } } - aclCachePtr.RUnlock() return &authPredResult{allowed: allowedPreds, blocked: blockedPreds}, nil } // authorizeAlter parses the Schema in the operation and authorizes the operation -// using the aclCachePtr. It will return error if any one of the predicates specified in alter -// are not authorized. +// using the worker.AclCachePtr. It will return error if any one of the predicates +// specified in alter are not authorized. func authorizeAlter(ctx context.Context, op *api.Operation) error { if len(worker.Config.HmacSecret) == 0 { // the user has not turned on the acl feature @@ -768,7 +780,7 @@ func isAclPredMutation(nquads []*api.NQuad) bool { return false } -// authorizeMutation authorizes the mutation using the aclCachePtr. It will return permission +// authorizeMutation authorizes the mutation using the worker.AclCachePtr. It will return permission // denied error if any one of the predicates in mutation(set or delete) is unauthorized. // At this stage, namespace is not attached in the predicates. func authorizeMutation(ctx context.Context, gmu *gql.Mutation) error { diff --git a/edgraph/server.go b/edgraph/server.go index 7cf906fd174..2d8415208a6 100644 --- a/edgraph/server.go +++ b/edgraph/server.go @@ -409,7 +409,7 @@ func (s *Server) Alter(ctx context.Context, op *api.Operation) (*api.Payload, er // reset their in-memory GraphQL schema _, err = UpdateGQLSchema(ctx, "", "") // recreate the admin account after a drop all operation - ResetAcl(nil) + InitializeAcl(nil) return empty, err } @@ -447,7 +447,7 @@ func (s *Server) Alter(ctx context.Context, op *api.Operation) (*api.Payload, er // just reinsert the GraphQL schema, no need to alter dgraph schema as this was drop_data _, err = UpdateGQLSchema(ctx, graphQLSchema, "") // recreate the admin account after a drop data operation - ResetAcl(nil) + InitializeAcl(nil) return empty, err } diff --git a/graphql/admin/restore.go b/graphql/admin/restore.go index 4dfb2f6b738..1496653c25a 100644 --- a/graphql/admin/restore.go +++ b/graphql/admin/restore.go @@ -84,7 +84,7 @@ func resolveRestore(ctx context.Context, m schema.Mutation) (*resolve.Resolved, go func() { wg.Wait() - edgraph.ResetAcl(nil) + edgraph.InitializeAcl(nil) }() return resolve.DataResult( diff --git a/systest/acl/restore/acl-secret b/systest/acl/restore/acl-secret new file mode 100644 index 00000000000..f02b5b99605 --- /dev/null +++ b/systest/acl/restore/acl-secret @@ -0,0 +1 @@ +12345678901234567890123456789012 \ No newline at end of file diff --git a/systest/acl/restore/acl_restore_test.go b/systest/acl/restore/acl_restore_test.go new file mode 100644 index 00000000000..fb7710fee01 --- /dev/null +++ b/systest/acl/restore/acl_restore_test.go @@ -0,0 +1,124 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "testing" + + "github.com/dgraph-io/dgo/v210" + "github.com/dgraph-io/dgo/v210/protos/api" + "github.com/dgraph-io/dgraph/graphql/e2e/common" + "github.com/dgraph-io/dgraph/testutil" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" +) + +// disableDraining disables draining mode before each test for increased reliability. +func disableDraining(t *testing.T) { + drainRequest := `mutation draining { + draining(enable: false) { + response { + code + message + } + } + }` + + params := testutil.GraphQLParams{ + Query: drainRequest, + } + b, err := json.Marshal(params) + require.NoError(t, err) + + token := testutil.Login(t, &testutil.LoginParams{UserID: "groot", Passwd: "password", Namespace: 0}) + + client := &http.Client{} + req, err := http.NewRequest("POST", testutil.AdminUrl(), bytes.NewBuffer(b)) + require.Nil(t, err) + req.Header.Add("content-type", "application/json") + req.Header.Add("X-Dgraph-AccessToken", token.AccessJwt) + + resp, err := client.Do(req) + require.NoError(t, err) + buf, err := ioutil.ReadAll(resp.Body) + fmt.Println(string(buf)) + require.NoError(t, err) + require.Contains(t, string(buf), "draining mode has been set to false") +} + +func sendRestoreRequest(t *testing.T, location, backupId string, backupNum int) { + if location == "" { + location = "/data/backup2" + } + params := &testutil.GraphQLParams{ + Query: `mutation restore($location: String!, $backupId: String, $backupNum: Int) { + restore(input: {location: $location, backupId: $backupId, backupNum: $backupNum}) { + code + message + } + }`, + Variables: map[string]interface{}{ + "location": location, + "backupId": backupId, + "backupNum": backupNum, + }, + } + + token := testutil.Login(t, &testutil.LoginParams{UserID: "groot", Passwd: "password", Namespace: 0}) + resp := testutil.MakeGQLRequestWithAccessJwt(t, params, token.AccessJwt) + resp.RequireNoGraphQLErrors(t) + + var restoreResp struct { + Restore struct { + Code string + Message string + RestoreId int + } + } + require.NoError(t, json.Unmarshal(resp.Data, &restoreResp)) + require.Equal(t, restoreResp.Restore.Code, "Success") +} + +func TestAclCacheRestore(t *testing.T) { + disableDraining(t) + conn, err := grpc.Dial(testutil.SockAddr, grpc.WithInsecure()) + require.NoError(t, err) + dg := dgo.NewDgraphClient(api.NewDgraphClient(conn)) + dg.Login(context.Background(), "groot", "password") + + sendRestoreRequest(t, "/backups", "vibrant_euclid5", 1) + testutil.WaitForRestore(t, dg) + + token := testutil.Login(t, + &testutil.LoginParams{UserID: "alice1", Passwd: "password", Namespace: 0}) + params := &common.GraphQLParams{ + Query: `query{ + queryPerson{ + name + age + } + }`, + + Headers: make(http.Header), + } + params.Headers.Set("X-Dgraph-AccessToken", token.AccessJwt) + + resp := params.ExecuteAsPost(t, common.GraphqlURL) + require.Nil(t, resp.Errors) + + expected := ` + { + "queryPerson": [ + { + "name": "MinhajSh", + "age": 20 + } + ] + } + ` + require.JSONEq(t, expected, string(resp.Data)) +} diff --git a/systest/acl/restore/data/backups/dgraph.20210730.124449.146/r21-g1.backup b/systest/acl/restore/data/backups/dgraph.20210730.124449.146/r21-g1.backup new file mode 100644 index 00000000000..c9fc5de7081 Binary files /dev/null and b/systest/acl/restore/data/backups/dgraph.20210730.124449.146/r21-g1.backup differ diff --git a/systest/acl/restore/data/backups/manifest.json b/systest/acl/restore/data/backups/manifest.json new file mode 100644 index 00000000000..26ed45a88c4 --- /dev/null +++ b/systest/acl/restore/data/backups/manifest.json @@ -0,0 +1,33 @@ +{ + "Manifests":[ + { + "type":"full", + "since":0, + "read_ts":21, + "groups":{ + "1":[ + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000dgraph.password", + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000dgraph.graphql.p_query", + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000dgraph.acl.rule", + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000dgraph.drop.op", + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000dgraph.graphql.xid", + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000dgraph.xid", + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000dgraph.rule.predicate", + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000dgraph.graphql.schema", + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000dgraph.rule.permission", + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000Person.name", + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000dgraph.user.group", + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000Person.age", + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000dgraph.type" + ] + }, + "backup_id":"vibrant_euclid5", + "backup_num":1, + "version":2103, + "path":"dgraph.20210730.124449.146", + "encrypted":false, + "drop_operations":null, + "compression":"snappy" + } + ] +} \ No newline at end of file diff --git a/systest/acl/restore/docker-compose.yml b/systest/acl/restore/docker-compose.yml new file mode 100644 index 00000000000..7790bc68999 --- /dev/null +++ b/systest/acl/restore/docker-compose.yml @@ -0,0 +1,50 @@ +version: "3.5" +services: + alpha1: + image: dgraph/dgraph:latest + working_dir: /data/alpha1 + labels: + cluster: test + ports: + - 8080 + - 9080 + volumes: + - type: bind + source: $GOPATH/bin + target: /gobin + read_only: true + - type: bind + source: ./data/backups + target: /backups + read_only: false + - type: bind + source: ./acl-secret + target: /secret/hmac + read_only: true + command: /gobin/dgraph alpha --my=alpha1:7080 --zero=zero1:5080 + --logtostderr -v=2 --raft "idx=1; group=1" --security "whitelist=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16;" + --acl "secret-file=/secret/hmac;" + deploy: + resources: + limits: + memory: 32G + zero1: + image: dgraph/dgraph:latest + working_dir: /data/zero1 + labels: + cluster: test + ports: + - 5080 + - 6080 + volumes: + - type: bind + source: $GOPATH/bin + target: /gobin + read_only: true + command: /gobin/dgraph zero --raft='idx=1' --my=zero1:5080 --logtostderr + -v=2 --bindall + deploy: + resources: + limits: + memory: 32G +volumes: {} diff --git a/systest/online-restore/online_restore_test.go b/systest/online-restore/online_restore_test.go index 6ce373adde4..2cc6ec6c6dc 100644 --- a/systest/online-restore/online_restore_test.go +++ b/systest/online-restore/online_restore_test.go @@ -71,7 +71,6 @@ func sendRestoreRequest(t *testing.T, location, backupId string, backupNum int) } require.NoError(t, json.Unmarshal(resp.Data, &restoreResp)) require.Equal(t, restoreResp.Restore.Code, "Success") - return } // disableDraining disables draining mode before each test for increased reliability. diff --git a/edgraph/acl_cache.go b/worker/acl_cache.go similarity index 80% rename from edgraph/acl_cache.go rename to worker/acl_cache.go index 7515a705e92..4a02ea69bbc 100644 --- a/edgraph/acl_cache.go +++ b/worker/acl_cache.go @@ -10,7 +10,7 @@ * https://github.com/dgraph-io/dgraph/blob/master/licenses/DCL.txt */ -package edgraph +package worker import ( "sync" @@ -21,22 +21,52 @@ import ( ) // aclCache is the cache mapping group names to the corresponding group acls -type aclCache struct { +type AclCache struct { sync.RWMutex + loaded bool predPerms map[string]map[string]int32 userPredPerms map[string]map[string]int32 } -var aclCachePtr = &aclCache{ +func (cache *AclCache) reset() { + cache.Lock() + defer cache.Unlock() + cache.loaded = false +} + +func ResetAclCache() { + AclCachePtr.reset() +} + +func (cache *AclCache) Loaded() bool { + cache.RLock() + defer cache.RUnlock() + return cache.loaded +} + +func (cache *AclCache) Set() { + cache.Lock() + defer cache.Unlock() + cache.loaded = true +} + +var AclCachePtr = &AclCache{ + loaded: false, predPerms: make(map[string]map[string]int32), userPredPerms: make(map[string]map[string]int32), } -func (cache *aclCache) update(ns uint64, groups []acl.Group) { +func (cache *AclCache) GetUserPredPerms(userId string) map[string]int32 { + cache.Lock() + defer cache.Unlock() + return cache.userPredPerms[userId] +} + +func (cache *AclCache) Update(ns uint64, groups []acl.Group) { // 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] where friend and name are predicates, - // However in the aclCachePtr in memory, we need to change the structure and store + // However in the AclCachePtr in memory, we need to change the structure and store // the information in two formats for efficient look-ups. // // First in which ACL rules are divided by predicates, e.g. @@ -101,21 +131,21 @@ func (cache *aclCache) update(ns uint64, groups []acl.Group) { } } - aclCachePtr.Lock() - defer aclCachePtr.Unlock() - aclCachePtr.predPerms = predPerms - aclCachePtr.userPredPerms = userPredPerms + AclCachePtr.Lock() + defer AclCachePtr.Unlock() + AclCachePtr.predPerms = predPerms + AclCachePtr.userPredPerms = userPredPerms } -func (cache *aclCache) authorizePredicate(groups []string, predicate string, +func (cache *AclCache) AuthorizePredicate(groups []string, predicate string, operation *acl.Operation) error { if x.IsAclPredicate(x.ParseAttr(predicate)) { return errors.Errorf("only groot is allowed to access the ACL predicate: %s", predicate) } - aclCachePtr.RLock() - predPerms := aclCachePtr.predPerms - aclCachePtr.RUnlock() + AclCachePtr.RLock() + predPerms := AclCachePtr.predPerms + AclCachePtr.RUnlock() if groupPerms, found := predPerms[predicate]; found { if hasRequiredAccess(groupPerms, groups, operation) { diff --git a/edgraph/acl_cache_test.go b/worker/acl_cache_test.go similarity index 78% rename from edgraph/acl_cache_test.go rename to worker/acl_cache_test.go index 6a22cc112a8..42d9643ed07 100644 --- a/edgraph/acl_cache_test.go +++ b/worker/acl_cache_test.go @@ -10,7 +10,7 @@ * https://github.com/dgraph-io/dgraph/blob/master/licenses/DCL.txt */ -package edgraph +package worker import ( "testing" @@ -21,14 +21,14 @@ import ( ) func TestAclCache(t *testing.T) { - aclCachePtr = &aclCache{ + AclCachePtr = &AclCache{ predPerms: make(map[string]map[string]int32), } var emptyGroups []string group := "dev" predicate := x.GalaxyAttr("friend") - require.Error(t, aclCachePtr.authorizePredicate(emptyGroups, predicate, acl.Read), + require.Error(t, AclCachePtr.AuthorizePredicate(emptyGroups, predicate, acl.Read), "the anonymous user should not have access when the acl cache is empty") acls := []acl.Acl{ @@ -44,16 +44,16 @@ func TestAclCache(t *testing.T) { Rules: acls, }, } - aclCachePtr.update(x.GalaxyNamespace, groups) + AclCachePtr.Update(x.GalaxyNamespace, groups) // after a rule is defined, the anonymous user should no longer have access - require.Error(t, aclCachePtr.authorizePredicate(emptyGroups, predicate, acl.Read), + require.Error(t, AclCachePtr.AuthorizePredicate(emptyGroups, predicate, acl.Read), "the anonymous user should not have access when the predicate has acl defined") - require.NoError(t, aclCachePtr.authorizePredicate([]string{group}, predicate, acl.Read), + require.NoError(t, AclCachePtr.AuthorizePredicate([]string{group}, predicate, acl.Read), "the user with group authorized should have access") // update the cache with empty acl list in order to clear the cache - aclCachePtr.update(x.GalaxyNamespace, []acl.Group{}) + AclCachePtr.Update(x.GalaxyNamespace, []acl.Group{}) // the anonymous user should have access again - require.Error(t, aclCachePtr.authorizePredicate(emptyGroups, predicate, acl.Read), + require.Error(t, AclCachePtr.AuthorizePredicate(emptyGroups, predicate, acl.Read), "the anonymous user should not have access when the acl cache is empty") } diff --git a/worker/online_restore_ee.go b/worker/online_restore_ee.go index e9932470752..9598d3d36ce 100644 --- a/worker/online_restore_ee.go +++ b/worker/online_restore_ee.go @@ -268,6 +268,7 @@ func handleRestoreProposal(ctx context.Context, req *pb.RestoreRequest, pidx uin return errors.Wrapf(err, "cannot load schema after restore") } + ResetAclCache() // Propose a snapshot immediately after all the work is done to prevent the restore // from being replayed. go func(idx uint64) {