diff --git a/dgraph/cmd/alpha/http.go b/dgraph/cmd/alpha/http.go index 9f2aa38e2f3..104aecfc990 100644 --- a/dgraph/cmd/alpha/http.go +++ b/dgraph/cmd/alpha/http.go @@ -156,7 +156,7 @@ func queryHandler(w http.ResponseWriter, r *http.Request) { d := r.URL.Query().Get("debug") ctx := context.WithValue(context.Background(), query.DebugKey, d) - + ctx = attachAccessJwt(ctx, r) // If ro is set, run this as a readonly query. if ro := r.URL.Query().Get("ro"); len(ro) > 0 && req.StartTs == 0 { if ro == "true" || ro == "1" { @@ -252,6 +252,7 @@ func mutationHandler(w http.ResponseWriter, r *http.Request) { } mu.CommitNow = c } + ctx := attachAccessJwt(context.Background(), r) ts, err := extractStartTs(r.URL.Path) if err != nil { @@ -260,7 +261,7 @@ func mutationHandler(w http.ResponseWriter, r *http.Request) { } mu.StartTs = ts - resp, err := (&edgraph.Server{}).Mutate(context.Background(), mu) + resp, err := (&edgraph.Server{}).Mutate(ctx, mu) if err != nil { x.SetStatusWithData(w, x.ErrorInvalidRequest, err.Error()) return @@ -400,6 +401,19 @@ func abortHandler(w http.ResponseWriter, r *http.Request) { writeResponse(w, r, js) } +func attachAccessJwt(ctx context.Context, r *http.Request) context.Context { + if accessJwt := r.Header.Get("X-Dgraph-AccessJWT"); accessJwt != "" { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + md = metadata.New(nil) + } + + md.Append("accessJwt", accessJwt) + ctx = metadata.NewIncomingContext(ctx, md) + } + return ctx +} + func alterHandler(w http.ResponseWriter, r *http.Request) { if commonHandler(w, r) { return @@ -427,6 +441,7 @@ func alterHandler(w http.ResponseWriter, r *http.Request) { // Pass in an auth token, if present. md.Append("auth-token", r.Header.Get("X-Dgraph-AuthToken")) ctx := metadata.NewIncomingContext(context.Background(), md) + ctx = attachAccessJwt(ctx, r) if _, err = (&edgraph.Server{}).Alter(ctx, op); err != nil { x.SetStatus(w, x.Error, err.Error()) return diff --git a/dgraph/cmd/alpha/login_ee.go b/dgraph/cmd/alpha/login_ee.go new file mode 100644 index 00000000000..b98a1f4eb7c --- /dev/null +++ b/dgraph/cmd/alpha/login_ee.go @@ -0,0 +1,76 @@ +// +build !oss + +/* + * Copyright 2018 Dgraph Labs, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package alpha + +import ( + "context" + "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) { + if commonHandler(w, r) { + return + } + + user := r.Header.Get("X-Dgraph-User") + password := r.Header.Get("X-Dgraph-Password") + refreshJwt := r.Header.Get("X-Dgraph-RefreshJWT") + ctx := context.Background() + resp, err := (&edgraph.Server{}).Login(ctx, &api.LoginRequest{ + Userid: user, + Password: password, + RefreshToken: refreshJwt, + }) + + if err != nil { + x.SetStatusWithData(w, x.ErrorInvalidRequest, err.Error()) + return + } + + jwt := &api.Jwt{} + if err := jwt.Unmarshal(resp.Json); err != nil { + x.SetStatusWithData(w, x.Error, err.Error()) + } + + response := map[string]interface{}{} + mp := make(map[string]string) + 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 + } + + if _, err := writeResponse(w, r, js); err != nil { + glog.Errorf("Error while writing response: %v", err) + } +} + +func init() { + http.HandleFunc("/login", loginHandler) +} diff --git a/dgraph/cmd/alpha/run.go b/dgraph/cmd/alpha/run.go index 420fd44d962..0b58b7081c2 100644 --- a/dgraph/cmd/alpha/run.go +++ b/dgraph/cmd/alpha/run.go @@ -89,6 +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.StringP("postings", "p", "p", "Directory to store posting lists.") // Options around how to set up Badger. @@ -128,8 +130,9 @@ they form a Raft group and provide synchronous replication. " The token can be passed as follows: For HTTP requests, in X-Dgraph-AuthToken header."+ " For Grpc, in auth-token key in the context.") - flag.String("hmac_secret_file", "", "The file storing the HMAC secret"+ - " that is used for signing the JWT. Enterprise feature.") + flag.String("acl_secret_file", "", "The file that stores the HMAC secret, "+ + "which is used for signing the JWT and should have at least 32 ASCII characters. "+ + "Enterprise feature.") flag.Duration("acl_access_ttl", 6*time.Hour, "The TTL for the access jwt. "+ "Enterprise feature.") flag.Duration("acl_refresh_ttl", 30*24*time.Hour, "The TTL for the refresh jwt. "+ @@ -440,12 +443,11 @@ func run() { AllottedMemory: Alpha.Conf.GetFloat64("lru_mb"), } - secretFile := Alpha.Conf.GetString("hmac_secret_file") + secretFile := Alpha.Conf.GetString("acl_secret_file") if secretFile != "" { if !Alpha.Conf.GetBool("enterprise_features") { - glog.Errorf("You must enable Dgraph enterprise features with the " + + glog.Fatalf("You must enable Dgraph enterprise features with the " + "--enterprise_features option in order to use ACL.") - os.Exit(1) } hmacSecret, err := ioutil.ReadFile(secretFile) @@ -453,8 +455,7 @@ func run() { glog.Fatalf("Unable to read HMAC secret from file: %v", secretFile) } if len(hmacSecret) < 32 { - glog.Errorf("The HMAC secret file should contain at least 256 bits (32 ascii chars)") - os.Exit(1) + glog.Fatalf("The HMAC secret file should contain at least 256 bits (32 ascii chars)") } opts.HmacSecret = hmacSecret diff --git a/dgraph/cmd/alpha/run_test.go b/dgraph/cmd/alpha/run_test.go index 4c15ef77d42..5a6d0cfd334 100644 --- a/dgraph/cmd/alpha/run_test.go +++ b/dgraph/cmd/alpha/run_test.go @@ -141,7 +141,14 @@ func alterSchema(s string) error { if err != nil { return err } - _, _, err = runRequest(req) + for { + // keep retrying until we succeed or receive a non-retriable error + _, _, err = runRequest(req) + if err == nil || !strings.Contains(err.Error(), "Please retry operation") { + break + } + } + return err } diff --git a/dgraph/cmd/root.go b/dgraph/cmd/root.go index c5ad99686f4..0f8f5a47869 100644 --- a/dgraph/cmd/root.go +++ b/dgraph/cmd/root.go @@ -85,8 +85,6 @@ func initCmds() { "Use 0.0.0.0 instead of localhost to bind to all addresses on local machine.") RootCmd.PersistentFlags().Bool("expose_trace", false, "Allow trace endpoint to be accessible from remote") - RootCmd.PersistentFlags().Bool("enterprise_features", false, - "Enable Dgraph enterprise features. If you set this to true, you agree to the Dgraph Community License.") rootConf.BindPFlags(RootCmd.PersistentFlags()) flag.CommandLine.AddGoFlagSet(goflag.CommandLine) diff --git a/dgraph/cmd/zero/zero.go b/dgraph/cmd/zero/zero.go index a12fa2405a0..57b29a02e33 100644 --- a/dgraph/cmd/zero/zero.go +++ b/dgraph/cmd/zero/zero.go @@ -551,6 +551,12 @@ 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 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 + tablet.GroupId = 1 + } proposal.Tablet = tablet if err := s.Node.proposeAndWait(ctx, &proposal); err != nil && err != errTabletAlreadyServed { span.Annotatef(nil, "While proposing tablet: %v", err) diff --git a/dgraph/docker-compose.yml b/dgraph/docker-compose.yml index c5aac26cfb6..bd6f1ccdf84 100644 --- a/dgraph/docker-compose.yml +++ b/dgraph/docker-compose.yml @@ -19,7 +19,7 @@ services: source: $GOPATH/bin target: /gobin read_only: true - command: /gobin/dgraph zero -o 0 --my=zero1:5080 --replicas 3 --idx 1 --logtostderr -v=2 --enterprise_features --bindall --expose_trace --profile_mode block --block_rate 10 + command: /gobin/dgraph zero -o 0 --my=zero1:5080 --replicas 3 --idx 1 --logtostderr -v=2 --bindall --expose_trace --profile_mode block --block_rate 10 zero2: image: dgraph/dgraph:latest @@ -38,7 +38,7 @@ services: source: $GOPATH/bin target: /gobin read_only: true - command: /gobin/dgraph zero -o 2 --my=zero2:5082 --replicas 3 --idx 2 --logtostderr -v=2 --enterprise_features --peer=zero1:5080 + command: /gobin/dgraph zero -o 2 --my=zero2:5082 --replicas 3 --idx 2 --logtostderr -v=2 --peer=zero1:5080 zero3: image: dgraph/dgraph:latest @@ -57,7 +57,7 @@ services: source: $GOPATH/bin target: /gobin read_only: true - command: /gobin/dgraph zero -o 3 --my=zero3:5083 --replicas 3 --idx 3 --logtostderr -v=2 --enterprise_features --peer=zero1:5080 + command: /gobin/dgraph zero -o 3 --my=zero3:5083 --replicas 3 --idx 3 --logtostderr -v=2 --peer=zero1:5080 dg1: image: dgraph/dgraph:latest @@ -74,7 +74,7 @@ services: labels: cluster: test service: alpha - command: /gobin/dgraph alpha --my=dg1:7180 --lru_mb=1024 --zero=zero1:5080 -o 100 --expose_trace --trace 1.0 --profile_mode block --block_rate 10 --logtostderr -v=2 --enterprise_features --whitelist 10.0.0.0:10.255.255.255,172.16.0.0:172.31.255.255,192.168.0.0:192.168.255.255 + command: /gobin/dgraph alpha --my=dg1:7180 --lru_mb=1024 --zero=zero1:5080 -o 100 --expose_trace --trace 1.0 --profile_mode block --block_rate 10 --logtostderr -v=2 --whitelist 10.0.0.0:10.255.255.255,172.16.0.0:172.31.255.255,192.168.0.0:192.168.255.255 dg2: image: dgraph/dgraph:latest @@ -93,7 +93,7 @@ services: labels: cluster: test service: alpha - command: /gobin/dgraph alpha --my=dg2:7182 --lru_mb=1024 --zero=zero1:5080 -o 102 --expose_trace --trace 1.0 --profile_mode block --block_rate 10 --logtostderr -v=2 --enterprise_features --whitelist 10.0.0.0:10.255.255.255,172.16.0.0:172.31.255.255,192.168.0.0:192.168.255.255 + command: /gobin/dgraph alpha --my=dg2:7182 --lru_mb=1024 --zero=zero1:5080 -o 102 --expose_trace --trace 1.0 --profile_mode block --block_rate 10 --logtostderr -v=2 --whitelist 10.0.0.0:10.255.255.255,172.16.0.0:172.31.255.255,192.168.0.0:192.168.255.255 dg3: image: dgraph/dgraph:latest @@ -112,4 +112,4 @@ services: labels: cluster: test service: alpha - command: /gobin/dgraph alpha --my=dg3:7183 --lru_mb=1024 --zero=zero1:5080 -o 103 --expose_trace --trace 1.0 --profile_mode block --block_rate 10 --logtostderr -v=2 --enterprise_features --whitelist 10.0.0.0:10.255.255.255,172.16.0.0:172.31.255.255,192.168.0.0:192.168.255.255 + command: /gobin/dgraph alpha --my=dg3:7183 --lru_mb=1024 --zero=zero1:5080 -o 103 --expose_trace --trace 1.0 --profile_mode block --block_rate 10 --logtostderr -v=2 --whitelist 10.0.0.0:10.255.255.255,172.16.0.0:172.31.255.255,192.168.0.0:192.168.255.255 diff --git a/edgraph/access_ee.go b/edgraph/access_ee.go index 363f149acb5..863cdf18ca6 100644 --- a/edgraph/access_ee.go +++ b/edgraph/access_ee.go @@ -15,9 +15,11 @@ package edgraph import ( "context" "fmt" - "sync" + "strings" "time" + "github.com/pkg/errors" + "github.com/dgraph-io/badger/y" "github.com/dgraph-io/dgo/protos/api" @@ -43,7 +45,7 @@ func (s *Server) Login(ctx context.Context, var addr string if ip, ok := peer.FromContext(ctx); ok { addr = ip.Addr.String() - glog.Infof("login request from: %s", addr) + glog.Infof("Login request from: %s", addr) span.Annotate([]otrace.Attribute{ otrace.StringAttribute("client_ip", addr), }, "client ip for login") @@ -51,7 +53,7 @@ func (s *Server) Login(ctx context.Context, user, err := s.authenticateLogin(ctx, request) if err != nil { - errMsg := fmt.Sprintf("authentication from address %s failed: %v", addr, err) + errMsg := fmt.Sprintf("Authentication from address %s failed: %v", addr, err) glog.Errorf(errMsg) return nil, fmt.Errorf(errMsg) } @@ -116,7 +118,7 @@ func (s *Server) authenticateLogin(ctx context.Context, request *api.LoginReques "user not found for id %v", userId) } - glog.Infof("authenticated user %s through refresh token", userId) + glog.Infof("Authenticated user %s through refresh token", userId) return user, nil } @@ -291,7 +293,7 @@ func RefreshAcls(closer *y.Closer) { // retrieve the full data set of ACLs from the corresponding alpha server, and update the // aclCache retrieveAcls := func() error { - glog.V(1).Infof("Refreshing ACLs") + glog.V(3).Infof("Refreshing ACLs") queryRequest := api.Request{ Query: queryAcls, } @@ -307,19 +309,8 @@ func RefreshAcls(closer *y.Closer) { return err } - storedEntries := 0 - for _, group := range groups { - // convert the serialized acl into a map for easy lookups - group.MappedAcls, err = acl.UnmarshalAcl([]byte(group.Acls)) - if err != nil { - glog.Errorf("Error while unmarshalling ACLs for group %v:%v", group, err) - continue - } - - storedEntries++ - aclCache.Store(group.GroupID, &group) - } - glog.V(1).Infof("Updated the ACL cache with %d entries", storedEntries) + aclCache.update(groups) + glog.V(3).Infof("Updated the ACL cache") return nil } @@ -344,9 +335,6 @@ const queryAcls = ` } ` -// the acl cache mapping group names to the corresponding group acls -var aclCache sync.Map - // clear the aclCache and upsert the Groot account. func ResetAcl() { if len(Config.HmacSecret) == 0 { @@ -404,7 +392,10 @@ func ResetAcl() { return nil } - aclCache = sync.Map{} + aclCache = &AclCache{ + predPerms: make(map[string]map[string]int32), + predRegexRules: make([]*PredRegexRule, 0), + } for { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -417,16 +408,18 @@ func ResetAcl() { } } +var errNoJwt = errors.New("no accessJwt available") + // extract the userId, groupIds from the accessJwt in the context func extractUserAndGroups(ctx context.Context) ([]string, error) { // extract the jwt and unmarshal the jwt to get the list of groups md, ok := metadata.FromIncomingContext(ctx) if !ok { - return nil, fmt.Errorf("no metadata available") + return nil, errNoJwt } accessJwt := md.Get("accessJwt") if len(accessJwt) == 0 { - return nil, fmt.Errorf("no accessJwt available") + return nil, errNoJwt } return validateToken(accessJwt[0]) @@ -440,34 +433,61 @@ func authorizeAlter(ctx context.Context, op *api.Operation) error { } userData, err := extractUserAndGroups(ctx) - if err != nil { + var userId string + var groupIds []string + if err == nil { + if isGroot(userData) { + return nil + } + userId = userData[0] + groupIds = userData[1:] + } else if err == errNoJwt { + // 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 isGroot(userData) { - return nil - } // 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, current user is %s", userData[0]) + return fmt.Errorf("only Groot is allowed to drop all data, but the current user is %s", + userId) } - groupIds := userData[1:] if len(op.DropAttr) > 0 { // check that we have the modify permission on the predicate - if err := authorizePredicate(groupIds, op.DropAttr, acl.Modify); err != nil { + err := aclCache.authorizePredicate(groupIds, op.DropAttr, acl.Modify) + logAccess(&AccessEntry{ + userId: userId, + groups: groupIds, + predicate: op.DropAttr, + operation: acl.Modify, + allowed: err != nil, + }) + + if err != nil { return status.Error(codes.PermissionDenied, - fmt.Sprintf("unauthorized to alter the predicate:%v", err)) + fmt.Sprintf("unauthorized to alter the predicate: %v", err)) } return nil } - result, err := schema.Parse(op.Schema) + update, err := schema.Parse(op.Schema) if err != nil { return err } - for _, update := range result.Schemas { - if err := authorizePredicate(groupIds, update.Predicate, acl.Modify); err != nil { + for _, update := range update.Schemas { + err := aclCache.authorizePredicate(groupIds, update.Predicate, acl.Modify) + logAccess(&AccessEntry{ + userId: userId, + groups: groupIds, + predicate: update.Predicate, + operation: acl.Modify, + allowed: err != nil, + }) + if err != nil { return status.Error(codes.PermissionDenied, fmt.Sprintf("unauthorized to alter the predicate: %v", err)) } @@ -492,22 +512,35 @@ func authorizeMutation(ctx context.Context, mu *api.Mutation) error { } userData, err := extractUserAndGroups(ctx) - if err != nil { + var userId string + var groupIds []string + if err == nil { + if isGroot(userData) { + return nil + } + userId = userData[0] + groupIds = userData[1:] + } else if err == errNoJwt { + // 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 + } else { return status.Error(codes.Unauthenticated, err.Error()) } - if isGroot(userData) { - // Groot has access to everything. - return nil - } gmu, err := parseMutationObject(mu) if err != nil { return err } - - groupIds := userData[1:] for pred := range parsePredsFromMutation(gmu.Set) { - if err := authorizePredicate(groupIds, pred, acl.Write); err != nil { + err := aclCache.authorizePredicate(groupIds, pred, acl.Write) + logAccess(&AccessEntry{ + userId: userId, + groups: groupIds, + predicate: pred, + operation: acl.Write, + allowed: err != nil, + }) + if err != nil { return status.Error(codes.PermissionDenied, fmt.Sprintf("unauthorized to mutate the predicate: %v", err)) } @@ -542,6 +575,20 @@ func isGroot(userData []string) bool { return userData[0] == x.GrootId } +type AccessEntry struct { + userId string + groups []string + predicate string + operation *acl.Operation + allowed bool +} + +func logAccess(log *AccessEntry) { + glog.V(1).Infof("ACL-LOG Authorizing user %s with groups %s on predicate %s "+ + "for %s, allowed %v", log.userId, strings.Join(log.groups, ","), + log.predicate, log.operation.Name, log.allowed) +} + //authorizeQuery authorizes the query using the aclCache func authorizeQuery(ctx context.Context, req *api.Request) error { if len(Config.HmacSecret) == 0 { @@ -550,12 +597,20 @@ func authorizeQuery(ctx context.Context, req *api.Request) error { } userData, err := extractUserAndGroups(ctx) - if err != nil { + var userId string + var groupIds []string + if err == nil { + if isGroot(userData) { + return nil + } + userId = userData[0] + groupIds = userData[1:] + } else if err == errNoJwt { + // 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 + } else { return status.Error(codes.Unauthenticated, err.Error()) } - if isGroot(userData) { - return nil - } parsedReq, err := gql.Parse(gql.Request{ Str: req.Query, @@ -565,40 +620,19 @@ func authorizeQuery(ctx context.Context, req *api.Request) error { return err } - groupIds := userData[1:] for pred := range parsePredsFromQuery(parsedReq.Query) { - if err := authorizePredicate(groupIds, pred, acl.Read); err != nil { + err := aclCache.authorizePredicate(groupIds, pred, acl.Read) + logAccess(&AccessEntry{ + userId: userId, + groups: groupIds, + predicate: pred, + operation: acl.Read, + allowed: err != nil, + }) + if err != nil { return status.Error(codes.PermissionDenied, fmt.Sprintf("unauthorized to query the predicate: %v", err)) } } return nil } - -func authorizePredicate(groups []string, predicate string, operation *acl.Operation) error { - for _, group := range groups { - if err := hasAccess(group, predicate, operation); err == nil { - return nil - } - } - return fmt.Errorf("unauthorized to do %s on predicate %s", operation.Name, predicate) -} - -// hasAccess checks the aclCache and returns whether the specified group is authorized to perform -// the operation on the given predicate -func hasAccess(groupId string, predicate string, operation *acl.Operation) error { - entry, found := aclCache.Load(groupId) - if !found { - return fmt.Errorf("acl not found for group %v", groupId) - } - aclGroup := entry.(*acl.Group) - perm, found := aclGroup.MappedAcls[predicate] - allowed := found && (perm&operation.Code) != 0 - glog.V(1).Infof("Authorizing group %v on predicate %v for %s, allowed %v", groupId, - predicate, operation.Name, allowed) - if !allowed { - return fmt.Errorf("group %s not allowed to do %s on predicate %s", - groupId, operation.Name, predicate) - } - return nil -} diff --git a/edgraph/acl_cache.go b/edgraph/acl_cache.go new file mode 100644 index 00000000000..6fb8ad4841b --- /dev/null +++ b/edgraph/acl_cache.go @@ -0,0 +1,160 @@ +// +build !oss + +/* + * Copyright 2018 Dgraph Labs, Inc. All rights reserved. + * + * Licensed under the Dgraph Community License (the "License"); you + * may not use this file except in compliance with the License. You + * may obtain a copy of the License at + * + * https://github.com/dgraph-io/dgraph/blob/master/licenses/DCL.txt + */ + +package edgraph + +import ( + "encoding/json" + "fmt" + "regexp" + "sync" + + "github.com/dgraph-io/dgraph/ee/acl" + "github.com/golang/glog" +) + +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 + +func (cache *AclCache) update(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], [^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() + defer aclCache.Unlock() + aclCache.predPerms = predPerms + aclCache.predRegexRules = predRegexRules +} + +func (cache *AclCache) 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 +} + +// 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 +} diff --git a/edgraph/acl_cache_test.go b/edgraph/acl_cache_test.go new file mode 100644 index 00000000000..8d998652949 --- /dev/null +++ b/edgraph/acl_cache_test.go @@ -0,0 +1,80 @@ +// +build !oss + +/* + * Copyright 2018 Dgraph Labs, Inc. All rights reserved. + * + * Licensed under the Dgraph Community License (the "License"); you + * may not use this file except in compliance with the License. You + * may obtain a copy of the License at + * + * https://github.com/dgraph-io/dgraph/blob/master/licenses/DCL.txt + */ + +package edgraph + +import ( + "encoding/json" + "testing" + + "github.com/dgraph-io/dgraph/ee/acl" + "github.com/stretchr/testify/require" +) + +func TestAclCache(t *testing.T) { + aclCache = &AclCache{ + predPerms: make(map[string]map[string]int32), + predRegexRules: make([]*PredRegexRule, 0), + } + + var emptyGroups []string + group := "dev" + predicate := "friend" + require.NoError(t, aclCache.authorizePredicate(emptyGroups, predicate, acl.Read), + "the anonymous user should have access when the acl cache is empty") + + acls := []acl.Acl{ + acl.Acl{ + Predicate: predicate, + Perm: 4, + }, + } + aclBytes, _ := json.Marshal(acls) + groups := []acl.Group{ + acl.Group{ + GroupID: group, + Acls: string(aclBytes), + }, + } + aclCache.update(groups) + // after a rule is defined, the anonymous user should no longer have access + require.Error(t, aclCache.authorizePredicate(emptyGroups, predicate, acl.Read), + "the anonymous user should not have access when the predicate has acl defined") + require.NoError(t, aclCache.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 + aclCache.update([]acl.Group{}) + // the anonymous user should have access again + require.NoError(t, aclCache.authorizePredicate(emptyGroups, predicate, acl.Read), + "the anonymous user should have access when the acl cache is empty") + + // define acls using regex + acls1 := []acl.Acl{ + acl.Acl{ + Regex: "^fri", + Perm: 4, + }, + } + aclBytes1, _ := json.Marshal(acls1) + groups1 := []acl.Group{ + acl.Group{ + GroupID: group, + Acls: string(aclBytes1), + }, + } + aclCache.update(groups1) + require.Error(t, aclCache.authorizePredicate(emptyGroups, predicate, acl.Read), + "the anonymous user should not have access when the predicate has acl defined") + require.NoError(t, aclCache.authorizePredicate([]string{group}, predicate, acl.Read), + "the user with group authorized should have access") +} diff --git a/ee/acl/acl_curl_test.go b/ee/acl/acl_curl_test.go new file mode 100644 index 00000000000..48ecafca506 --- /dev/null +++ b/ee/acl/acl_curl_test.go @@ -0,0 +1,224 @@ +// +build !oss + +/* + * Copyright 2018 Dgraph Labs, Inc. and Contributors + * + * Licensed under the Dgraph Community License (the "License"); you + * may not use this file except in compliance with the License. You + * may obtain a copy of the License at + * + * https://github.com/dgraph-io/dgraph/blob/master/licenses/DCL.txt + */ + +package acl + +import ( + "encoding/json" + "fmt" + "os/exec" + "strings" + "testing" + "time" + + "github.com/dgraph-io/dgraph/x" + "github.com/golang/glog" + "github.com/stretchr/testify/require" +) + +func TestCurlAuthorization(t *testing.T) { + glog.Infof("testing with port 9180") + dg, cancel := x.GetDgraphClientOnPort(9180) + defer cancel() + createAccountAndData(t, dg) + + // test query through curl + accessJwt, refreshJwt := curlLogin(t, "") + + // test fail open with the accessJwt + queryArgs := func() []string { + return []string{"-H", fmt.Sprintf("X-Dgraph-AccessJWT:%s", accessJwt), + "-d", query, curlQueryEndpoint} + } + + verifyCurlCmd(t, queryArgs(), &FailureConfig{ + shouldFail: false, + }) + + mutateArgs := func() []string { + return []string{"-H", fmt.Sprintf("X-Dgraph-AccessJWT:%s", accessJwt), + "-d", fmt.Sprintf(`{ set { + _:a <%s> "string" . + }}`, predicateToWrite), curlMutateEndpoint} + + } + verifyCurlCmd(t, mutateArgs(), &FailureConfig{ + shouldFail: false, + }) + + alterArgs := func() []string { + return []string{"-H", fmt.Sprintf("X-Dgraph-AccessJWT:%s", accessJwt), + "-d", fmt.Sprintf(`%s: int .`, predicateToAlter), curlAlterEndpoint} + } + + verifyCurlCmd(t, alterArgs(), &FailureConfig{ + shouldFail: false, + }) + + // 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 + glog.Infof("Sleeping for 12 seconds for accessJwt to expire") + time.Sleep(12 * time.Second) + verifyCurlCmd(t, queryArgs(), &FailureConfig{ + shouldFail: true, + failMsg: "Token is expired", + }) + verifyCurlCmd(t, mutateArgs(), &FailureConfig{ + shouldFail: true, + failMsg: "Token is expired", + }) + verifyCurlCmd(t, alterArgs(), &FailureConfig{ + shouldFail: true, + failMsg: "Token is expired", + }) + // login again using the refreshJwt + accessJwt, refreshJwt = curlLogin(t, refreshJwt) + // verify that the query works again with the new access jwt + verifyCurlCmd(t, queryArgs(), &FailureConfig{ + shouldFail: false, + }) + + createGroupAndAcls(t, unusedGroup, false) + // wait for 35 seconds to ensure the new acl have reached all acl caches + glog.Infof("Sleeping for 35 seconds for acl caches to be refreshed") + time.Sleep(35 * time.Second) + verifyCurlCmd(t, queryArgs(), &FailureConfig{ + shouldFail: true, + failMsg: "Token is expired", + }) + // refresh the jwts again + accessJwt, refreshJwt = curlLogin(t, refreshJwt) + // verify that with an ACL rule defined, all the operations should be denied when the acsess JWT + // does not have the required permissions + verifyCurlCmd(t, queryArgs(), &FailureConfig{ + shouldFail: true, + failMsg: "PermissionDenied", + }) + verifyCurlCmd(t, mutateArgs(), &FailureConfig{ + shouldFail: true, + failMsg: "PermissionDenied", + }) + verifyCurlCmd(t, alterArgs(), &FailureConfig{ + shouldFail: true, + failMsg: "PermissionDenied", + }) + + createGroupAndAcls(t, devGroup, true) + 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) + // verify that the operations should be allowed again through the dev group + verifyCurlCmd(t, queryArgs(), &FailureConfig{ + shouldFail: false, + }) + verifyCurlCmd(t, mutateArgs(), &FailureConfig{ + shouldFail: false, + }) + verifyCurlCmd(t, alterArgs(), &FailureConfig{ + shouldFail: false, + }) +} + +var curlLoginEndpoint = "localhost:8180/login" +var curlQueryEndpoint = "localhost:8180/query" +var curlMutateEndpoint = "localhost:8180/mutate" +var curlAlterEndpoint = "localhost:8180/alter" + +// curlLogin sends a curl request to the curlLoginEndpoint +// and returns the access JWT and refresh JWT extracted from +// the curl command output +func curlLogin(t *testing.T, refreshJwt string) (string, string) { + // login with alice's account using curl + args := []string{"-X", "POST", + curlLoginEndpoint} + + if len(refreshJwt) > 0 { + args = append(args, + "-H", fmt.Sprintf(`X-Dgraph-RefreshJWT:%s`, refreshJwt)) + } else { + args = append(args, + "-H", fmt.Sprintf(`X-Dgraph-User:%s`, userid), + "-H", fmt.Sprintf(`X-Dgraph-Password:%s`, userpassword)) + } + + userLoginCmd := exec.Command("curl", args...) + out, err := userLoginCmd.Output() + require.NoError(t, err, "the login should have succeeded") + + 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") + } + + return newAccessJwt, newRefreshJwt +} + +type ErrorEntry struct { + Code string `json:"code"` + Message string `json:"message"` +} + +type Output struct { + Data map[string]interface{} `json:"data"` + Errors []ErrorEntry `json:"errors"` +} + +type FailureConfig struct { + shouldFail bool + failMsg string +} + +func verifyOutput(t *testing.T, bytes []byte, failureConfig *FailureConfig) { + output := Output{} + require.NoError(t, json.Unmarshal(bytes, &output), + "unable to unmarshal the curl output") + + if failureConfig.shouldFail { + require.True(t, len(output.Errors) > 0, "no error entry found") + if len(failureConfig.failMsg) > 0 { + errorEntry := output.Errors[0] + require.True(t, strings.Contains(errorEntry.Message, failureConfig.failMsg), + fmt.Sprintf("the failure msg\n%s\nis not part of the curl error output:%s\n", + failureConfig.failMsg, errorEntry.Message)) + } + } else { + require.True(t, len(output.Data) > 0, + fmt.Sprintf("no data entry found in the output:%+v", output)) + } +} + +func verifyCurlCmd(t *testing.T, args []string, + failureConfig *FailureConfig) { + queryCmd := exec.Command("curl", args...) + + output, err := queryCmd.Output() + // the curl command should always succeed + require.NoError(t, err, "the curl command should have succeeded") + verifyOutput(t, output, failureConfig) +} diff --git a/ee/acl/acl_test.go b/ee/acl/acl_test.go index deb1f2f7d83..de4f09a23bf 100644 --- a/ee/acl/acl_test.go +++ b/ee/acl/acl_test.go @@ -15,7 +15,6 @@ package acl import ( "context" "fmt" - "log" "os" "os/exec" "path/filepath" @@ -128,22 +127,35 @@ func testAuthorization(t *testing.T, dg *dgo.Dgraph) { t.Fatalf("unable to login using the account %v", userid) } + // initially the query, mutate and alter operations should all succeed + // when there are no rules defined on the predicates (the fail open approach) + queryPredicateWithUserAccount(t, dg, false) + mutatePredicateWithUserAccount(t, dg, false) + alterPredicateWithUserAccount(t, dg, false) + createGroupAndAcls(t, unusedGroup, false) + // wait for 35 seconds to ensure the new acl have reached all acl caches + glog.Infof("Sleeping for 35 seconds for acl caches to be refreshed") + time.Sleep(35 * time.Second) + + // now all these operations should fail since there are rules defined on the unusedGroup queryPredicateWithUserAccount(t, dg, true) mutatePredicateWithUserAccount(t, dg, true) alterPredicateWithUserAccount(t, dg, true) + // create the dev group and add the user to it + createGroupAndAcls(t, devGroup, true) - createGroupAndAcls(t) // wait for 35 seconds to ensure the new acl have reached all acl caches - log.Println("Sleeping for 35 seconds for acl to catch up") + glog.Infof("Sleeping for 35 seconds for acl caches to be refreshed") time.Sleep(35 * time.Second) + // now the operations should succeed again through the devGroup queryPredicateWithUserAccount(t, dg, false) // sleep long enough (10s per the docker-compose.yml in this directory) // for the accessJwt to expire in order to test auto login through refresh jwt - log.Println("Sleeping for 12 seconds for accessJwt to expire") + glog.Infof("Sleeping for 12 seconds for accessJwt to expire") time.Sleep(12 * time.Second) mutatePredicateWithUserAccount(t, dg, false) - log.Println("Sleeping for 12 seconds for accessJwt to expire") + glog.Infof("Sleeping for 12 seconds for accessJwt to expire") time.Sleep(12 * time.Second) alterPredicateWithUserAccount(t, dg, false) } @@ -152,8 +164,15 @@ var predicateToRead = "predicate_to_read" var queryAttr = "name" var predicateToWrite = "predicate_to_write" var predicateToAlter = "predicate_to_alter" -var group = "dev" +var devGroup = "dev" +var unusedGroup = "unusedGroup" var rootDir = filepath.Join(os.TempDir(), "acl_test") +var query = fmt.Sprintf(` + { + q(func: eq(%s, "SF")) { + %s + } + }`, predicateToRead, queryAttr) func alterReservedPredicates(t *testing.T, dg *dgo.Dgraph) { ctx := context.Background() @@ -184,12 +203,6 @@ func queryPredicateWithUserAccount(t *testing.T, dg *dgo.Dgraph, shouldFail bool // login with alice's account ctx := context.Background() txn := dg.NewTxn() - query := fmt.Sprintf(` - { - q(func: eq(%s, "SF")) { - %s - } - }`, predicateToRead, queryAttr) txn = dg.NewTxn() _, err := txn.Query(ctx, query) @@ -254,7 +267,7 @@ func createAccountAndData(t *testing.T, dg *dgo.Dgraph) { require.NoError(t, txn.Commit(ctx)) } -func createGroupAndAcls(t *testing.T) { +func createGroupAndAcls(t *testing.T, group string, addUserToGroup bool) { // create a new group createGroupCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), "acl", "groupadd", @@ -265,12 +278,14 @@ func createGroupAndAcls(t *testing.T) { } // add the user to the group - addUserToGroupCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), - "acl", "usermod", - "-d", dgraphEndpoint, - "-u", userid, "-g", group, "-x", "password") - if err := addUserToGroupCmd.Run(); err != nil { - t.Fatalf("Unable to add user %s to group %s: %v", userid, group, err) + if addUserToGroup { + addUserToGroupCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), + "acl", "usermod", + "-d", dgraphEndpoint, + "-u", userid, "-g", group, "-x", "password") + if err := addUserToGroupCmd.Run(); err != nil { + t.Fatalf("Unable to add user %s to group %s:%v", userid, group, err) + } } // add READ permission on the predicateToRead to the group @@ -314,3 +329,116 @@ func createGroupAndAcls(t *testing.T) { t.Fatalf("Unable to add permission on %s to group %s: %v", predicateToAlter, group, err) } } + +func TestPasswordReset(t *testing.T) { + glog.Infof("testing with port 9180") + dg, cancel := x.GetDgraphClientOnPort(9180) + defer cancel() + createAccountAndData(t, dg) + // test login using the current password + ctx := context.Background() + err := dg.Login(ctx, userid, userpassword) + require.NoError(t, err, "Logging in with the current password should have succeeded") + + // reset password for the user alice + newPassword := userpassword + "123" + chPdCmd := exec.Command("dgraph", "acl", "passwd", "-d", dgraphEndpoint, "-u", + userid, "--new_password", newPassword, "-x", "password") + checkOutput(t, chPdCmd, false) + glog.Infof("Successfully changed password for %v", userid) + + // test that logging in using the old password should now fail + err = dg.Login(ctx, userid, userpassword) + require.Error(t, err, "Logging in with old password should no longer work") + + // test logging in using the new password + err = dg.Login(ctx, userid, newPassword) + require.NoError(t, err, "Logging in with new password should work now") +} + +func TestPredicateRegex(t *testing.T) { + glog.Infof("testing with port 9180") + dg, cancel := x.GetDgraphClientOnPort(9180) + defer cancel() + createAccountAndData(t, dg) + ctx := context.Background() + err := dg.Login(ctx, userid, userpassword) + require.NoError(t, err, "Logging in with the current password should have succeeded") + + // the operations should be allowed when no rule is defined (the fail open approach) + queryPredicateWithUserAccount(t, dg, false) + mutatePredicateWithUserAccount(t, dg, false) + alterPredicateWithUserAccount(t, dg, false) + createGroupAndAcls(t, unusedGroup, false) + + // wait for 35 seconds to ensure the new acl have reached all acl caches + glog.Infof("Sleeping for 35 seconds for acl caches to be refreshed") + time.Sleep(35 * time.Second) + // the operations should all fail when there is a rule defined, but the current user is not + // allowed + queryPredicateWithUserAccount(t, dg, true) + mutatePredicateWithUserAccount(t, dg, true) + alterPredicateWithUserAccount(t, dg, true) + + // create a new group + createGroupCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), + "acl", "groupadd", + "-d", dgraphEndpoint, + "-g", devGroup, "-x", "password") + if err := createGroupCmd.Run(); err != nil { + t.Fatalf("Unable to create group:%v", err) + } + + // add the user to the group + addUserToGroupCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), + "acl", "usermod", + "-d", dgraphEndpoint, + "-u", userid, "-g", devGroup, "-x", "password") + if err := addUserToGroupCmd.Run(); err != nil { + t.Fatalf("Unable to add user %s to group %s:%v", userid, devGroup, err) + } + + addReadToNameCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), + "acl", "chmod", + "-d", dgraphEndpoint, + "-g", devGroup, "--pred", "name", "-P", strconv.Itoa(int(Read.Code)|int(Write.Code)), + "-x", + "password") + if err := addReadToNameCmd.Run(); err != nil { + t.Fatalf("Unable to add READ permission on %s to group %s:%v", + "name", devGroup, err) + } + + // add READ+WRITE permission on the regex ^predicate_to(.*)$ pred filter to the group + predRegex := "^predicate_to(.*)$" + addReadWriteToRegexPermCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), + "acl", "chmod", + "-d", dgraphEndpoint, + "-g", devGroup, "--pred_regex", predRegex, "-P", + strconv.Itoa(int(Read.Code)|int(Write.Code)), "-x", "password") + if err := addReadWriteToRegexPermCmd.Run(); err != nil { + t.Fatalf("Unable to add READ+WRITE permission on %s to group %s:%v", + predRegex, devGroup, err) + } + + glog.Infof("Sleeping for 35 seconds for acl caches to be refreshed") + time.Sleep(35 * time.Second) + queryPredicateWithUserAccount(t, dg, false) + mutatePredicateWithUserAccount(t, dg, false) + // the alter operation should still fail since the regex pred does not have the Modify + // permission + alterPredicateWithUserAccount(t, dg, true) +} + +func TestAccessWithoutLoggingIn(t *testing.T) { + dg, cancel := x.GetDgraphClientOnPort(9180) + defer cancel() + + createAccountAndData(t, dg) + // without logging in, + // the anonymous user should be evaluated as if the user does not belong to any group, + // and access should be granted if there is no ACL rule defined for a predicate (fail open) + queryPredicateWithUserAccount(t, dg, false) + mutatePredicateWithUserAccount(t, dg, false) + alterPredicateWithUserAccount(t, dg, false) +} diff --git a/ee/acl/docker-compose.yml b/ee/acl/docker-compose.yml index efc15c85ca6..f1db67ce7c5 100644 --- a/ee/acl/docker-compose.yml +++ b/ee/acl/docker-compose.yml @@ -37,7 +37,7 @@ services: - 9180:9180 security_opt: - seccomp:unconfined - command: /gobin/dgraph alpha --my=dg1:7180 --lru_mb=1024 --zero=zero1:5080 -o 100 --expose_trace --trace 1.0 --profile_mode block --block_rate 10 --logtostderr -v=3 --hmac_secret_file /dgraph-acl/hmac-secret --enterprise_features --acl_access_ttl 10s + command: /gobin/dgraph alpha --my=dg1:7180 --lru_mb=1024 --zero=zero1:5080 -o 100 --expose_trace --trace 1.0 --profile_mode block --block_rate 10 --logtostderr -v=3 --acl_secret_file /dgraph-acl/hmac-secret --enterprise_features --acl_access_ttl 10s labels: cluster: test @@ -58,6 +58,6 @@ services: - 9182:9182 security_opt: - seccomp:unconfined - command: /gobin/dgraph alpha --my=dg2:7182 --lru_mb=1024 --zero=zero1:5080 -o 102 --expose_trace --trace 1.0 --profile_mode block --block_rate 10 --logtostderr -v=3 --hmac_secret_file /dgraph-acl/hmac-secret --enterprise_features --acl_access_ttl 10s + command: /gobin/dgraph alpha --my=dg2:7182 --lru_mb=1024 --zero=zero1:5080 -o 102 --expose_trace --trace 1.0 --profile_mode block --block_rate 10 --logtostderr -v=3 --acl_secret_file /dgraph-acl/hmac-secret --enterprise_features --acl_access_ttl 10s labels: cluster: test diff --git a/ee/acl/groups.go b/ee/acl/groups.go index 2ebffea497e..c08e69bc81c 100644 --- a/ee/acl/groups.go +++ b/ee/acl/groups.go @@ -16,41 +16,41 @@ import ( "context" "encoding/json" "fmt" + "regexp" "strings" "github.com/dgraph-io/dgo" "github.com/dgraph-io/dgo/protos/api" "github.com/dgraph-io/dgraph/x" - "github.com/golang/glog" "github.com/spf13/viper" ) func groupAdd(conf *viper.Viper) error { groupId := conf.GetString("group") if len(groupId) == 0 { - return fmt.Errorf("The group id should not be empty") + return fmt.Errorf("the group id should not be empty") } dc, cancel, err := getClientWithAdminCtx(conf) - defer cancel() if err != nil { - return fmt.Errorf("unable to get admin context:%v", err) + return fmt.Errorf("unable to get admin context: %v", err) } + defer cancel() ctx := context.Background() txn := dc.NewTxn() defer func() { if err := txn.Discard(ctx); err != nil { - glog.Errorf("Unable to discard transaction:%v", err) + fmt.Printf("Unable to discard transaction: %v\n", err) } }() group, err := queryGroup(ctx, txn, groupId) if err != nil { - return fmt.Errorf("Error while querying group:%v", err) + return fmt.Errorf("error while querying group: %v", err) } if group != nil { - return fmt.Errorf("The group with id %v already exists", groupId) + return fmt.Errorf("the group with id %v already exists", groupId) } createGroupNQuads := []*api.NQuad{ @@ -66,39 +66,39 @@ func groupAdd(conf *viper.Viper) error { Set: createGroupNQuads, } if _, err = txn.Mutate(ctx, mu); err != nil { - return fmt.Errorf("Unable to create group: %v", err) + return fmt.Errorf("unable to create group: %v", err) } - glog.Infof("Created new group with id %v", groupId) + fmt.Printf("Created new group with id %v\n", groupId) return nil } func groupDel(conf *viper.Viper) error { groupId := conf.GetString("group") if len(groupId) == 0 { - return fmt.Errorf("The group id should not be empty") + return fmt.Errorf("the group id should not be empty") } dc, cancel, err := getClientWithAdminCtx(conf) - defer cancel() if err != nil { - return fmt.Errorf("unable to get admin context:%v", err) + return fmt.Errorf("unable to get admin context: %v", err) } + defer cancel() ctx := context.Background() txn := dc.NewTxn() defer func() { if err := txn.Discard(ctx); err != nil { - glog.Errorf("Unable to discard transaction:%v", err) + fmt.Printf("Unable to discard transaction: %v\n", err) } }() group, err := queryGroup(ctx, txn, groupId) if err != nil { - return fmt.Errorf("Error while querying group:%v", err) + return fmt.Errorf("error while querying group: %v", err) } if group == nil || len(group.Uid) == 0 { - return fmt.Errorf("Unable to delete group because it does not exist: %v", groupId) + return fmt.Errorf("unable to delete group because it does not exist: %v", groupId) } deleteGroupNQuads := []*api.NQuad{ @@ -113,10 +113,10 @@ func groupDel(conf *viper.Viper) error { Del: deleteGroupNQuads, } if _, err := txn.Mutate(ctx, mu); err != nil { - return fmt.Errorf("Unable to delete group: %v", err) + return fmt.Errorf("unable to delete group: %v", err) } - glog.Infof("Deleted group with id %v", groupId) + fmt.Printf("Deleted group with id %v\n", groupId) return nil } @@ -135,7 +135,7 @@ func queryGroup(ctx context.Context, txn *dgo.Txn, groupid string, queryResp, err := txn.QueryWithVars(ctx, query, queryVars) if err != nil { - glog.Errorf("Error while query group with id %s: %v", groupid, err) + fmt.Printf("Error while querying group with id %s: %v\n", groupid, err) return nil, err } group, err = UnmarshalGroup(queryResp.GetJson(), "group") @@ -145,65 +145,80 @@ func queryGroup(ctx context.Context, txn *dgo.Txn, groupid string, return group, nil } -type Acl struct { - Predicate string `json:"predicate"` - Perm int32 `json:"perm"` -} - func chMod(conf *viper.Viper) error { groupId := conf.GetString("group") predicate := conf.GetString("pred") + predRegex := conf.GetString("pred_regex") perm := conf.GetInt("perm") if len(groupId) == 0 { - return fmt.Errorf("The groupid must not be empty") + return fmt.Errorf("the groupid must not be empty") + } + if len(predicate) > 0 && len(predRegex) > 0 { + return fmt.Errorf("only one of --pred or --pred_regex must be specified") + } + if len(predicate) == 0 && len(predRegex) == 0 { + return fmt.Errorf("only one of --pred or --pred_regex must be specified") } - if len(predicate) == 0 { - return fmt.Errorf("The predicate must not be empty") + if len(predRegex) > 0 { + // make sure the predRegex can be compiled as a regex + if _, err := regexp.Compile(predRegex); err != nil { + return fmt.Errorf("unable to compile %v as a regular expression: %v", + predRegex, err) + } } dc, cancel, err := getClientWithAdminCtx(conf) - defer cancel() if err != nil { - return fmt.Errorf("unable to get admin context:%v", err) + return fmt.Errorf("unable to get admin context: %v", err) } + defer cancel() ctx := context.Background() txn := dc.NewTxn() defer func() { if err := txn.Discard(ctx); err != nil { - glog.Errorf("Unable to discard transaction:%v", err) + fmt.Printf("Unable to discard transaction: %v\n", err) } }() group, err := queryGroup(ctx, txn, groupId, "dgraph.group.acl") if err != nil { - return fmt.Errorf("Error while querying group:%v", err) + return fmt.Errorf("error while querying group: %v\n", err) } if group == nil || len(group.Uid) == 0 { - return fmt.Errorf("Unable to change permission for group because it does not exist: %v", + return fmt.Errorf("unable to change permission for group because it does not exist: %v", groupId) } var currentAcls []Acl if len(group.Acls) != 0 { if err := json.Unmarshal([]byte(group.Acls), ¤tAcls); err != nil { - return fmt.Errorf("Unable to unmarshal the acls associated with the group %v:%v", + return fmt.Errorf("unable to unmarshal the acls associated with the group %v: %v", groupId, err) } } - newAcls, updated := updateAcl(currentAcls, Acl{ - Predicate: predicate, - Perm: int32(perm), - }) + var newAcl Acl + if len(predicate) > 0 { + newAcl = Acl{ + Predicate: predicate, + Perm: int32(perm), + } + } else { + newAcl = Acl{ + Regex: predRegex, + Perm: int32(perm), + } + } + newAcls, updated := updateAcl(currentAcls, newAcl) if !updated { - glog.Infof("Nothing needs to be changed for the permission of group:%v", groupId) + fmt.Printf("Nothing needs to be changed for the permission of group: %v\n", groupId) return nil } newAclBytes, err := json.Marshal(newAcls) if err != nil { - return fmt.Errorf("Unable to marshal the updated acls:%v", err) + return fmt.Errorf("unable to marshal the updated acls: %v", err) } chModNQuads := &api.NQuad{ @@ -217,19 +232,26 @@ func chMod(conf *viper.Viper) error { } if _, err = txn.Mutate(ctx, mu); err != nil { - return fmt.Errorf("Unable to change mutations for the group %v on predicate %v: %v", + return fmt.Errorf("unable to change mutations for the group %v on predicate %v: %v", groupId, predicate, err) } - glog.Infof("Successfully changed permission for group %v on predicate %v to %v", + fmt.Printf("Successfully changed permission for group %v on predicate %v to %v\n", groupId, predicate, perm) return nil } +func isSameAcl(acl1 *Acl, acl2 *Acl) bool { + return (len(acl1.Predicate) > 0 && len(acl2.Predicate) > 0 && + acl1.Predicate == acl2.Predicate) || + (len(acl1.Regex) > 0 && len(acl2.Regex) > 0 && acl1.Regex == acl2.Regex) +} + // returns whether the existing acls slice is changed func updateAcl(acls []Acl, newAcl Acl) ([]Acl, bool) { for idx, aclEntry := range acls { - if aclEntry.Predicate == newAcl.Predicate { + if isSameAcl(&aclEntry, &newAcl) { if aclEntry.Perm == newAcl.Perm { + // new permission is the same as the current one, no update return acls, false } if newAcl.Perm < 0 { diff --git a/ee/acl/groups_test.go b/ee/acl/groups_test.go index c4b7592d38a..294d97cb580 100644 --- a/ee/acl/groups_test.go +++ b/ee/acl/groups_test.go @@ -23,41 +23,51 @@ func TestUpdateAcl(t *testing.T) { Predicate: "friend", Perm: 4, } + updatedAcls1, changed := updateAcl(currenAcls, newAcl) require.True(t, changed, "the acl list should be changed") - require.Equal(t, 1, len(updatedAcls1), "the updated acl list should have 1 element") + require.Equal(t, 1, len(updatedAcls1), + "the updated acl list should have 1 element") // trying to update the acl list again with the exactly same acl won't change it updatedAcls2, changed := updateAcl(updatedAcls1, newAcl) - require.False(t, changed, "the acl list should not be changed through update with "+ - "an existing element") - require.Equal(t, 1, len(updatedAcls2), "the updated acl list should still have 1 element") - require.Equal(t, int32(4), updatedAcls2[0].Perm, "the perm should still have the value of 4") + require.False(t, changed, + "the acl list should not be changed through update with an existing element") + require.Equal(t, 1, len(updatedAcls2), + "the updated acl list should still have 1 element") + require.Equal(t, int32(4), updatedAcls2[0].Perm, + "the perm should still have the value of 4") newAcl.Perm = 6 updatedAcls3, changed := updateAcl(updatedAcls1, newAcl) require.True(t, changed, "the acl list should be changed through update "+ "with element of new perm") - require.Equal(t, 1, len(updatedAcls3), "the updated acl list should still have 1 element") - require.Equal(t, int32(6), updatedAcls3[0].Perm, "the updated perm should be 6 now") + require.Equal(t, 1, len(updatedAcls3), + "the updated acl list should still have 1 element") + require.Equal(t, int32(6), updatedAcls3[0].Perm, + "the updated perm should be 6 now") newAcl = Acl{ Predicate: "buddy", Perm: 6, } + updatedAcls4, changed := updateAcl(updatedAcls3, newAcl) require.True(t, changed, "the acl should be changed through update "+ "with element of new predicate") - require.Equal(t, 2, len(updatedAcls4), "the acl list should have 2 elements now") + require.Equal(t, 2, len(updatedAcls4), + "the acl list should have 2 elements now") newAcl = Acl{ Predicate: "buddy", Perm: -3, } + updatedAcls5, changed := updateAcl(updatedAcls4, newAcl) require.True(t, changed, "the acl should be changed through update "+ "with element of negative predicate") - require.Equal(t, 1, len(updatedAcls5), "the acl list should have 1 element now") - require.Equal(t, "friend", updatedAcls5[0].Predicate, "the left acl should have the original "+ - "first predicate") + require.Equal(t, 1, len(updatedAcls5), + "the acl list should have 1 element now") + require.Equal(t, "friend", updatedAcls5[0].Predicate, + "the left acl should have the original first predicate") } diff --git a/ee/acl/run_ee.go b/ee/acl/run_ee.go index ff05f7e159b..693240f42a7 100644 --- a/ee/acl/run_ee.go +++ b/ee/acl/run_ee.go @@ -58,10 +58,10 @@ func init() { CmdAcl.Cmd.AddCommand(sc.Cmd) sc.Conf = viper.New() if err := sc.Conf.BindPFlags(sc.Cmd.Flags()); err != nil { - glog.Fatalf("Unable to bind flags for command %v:%v", sc, err) + glog.Fatalf("Unable to bind flags for command %v: %v", sc, err) } if err := sc.Conf.BindPFlags(CmdAcl.Cmd.PersistentFlags()); err != nil { - glog.Fatalf("Unable to bind persistent flags from acl for command %v:%v", sc, err) + glog.Fatalf("Unable to bind persistent flags from acl for command %v: %v", sc, err) } sc.Conf.SetEnvPrefix(sc.EnvPrefix) } @@ -75,7 +75,7 @@ func initSubcommands() []*x.SubCommand { Short: "Run Dgraph acl tool to add a user", Run: func(cmd *cobra.Command, args []string) { if err := userAdd(cmdUserAdd.Conf); err != nil { - glog.Errorf("Unable to add user:%v", err) + fmt.Printf("Unable to add user: %v\n", err) os.Exit(1) } }, @@ -84,6 +84,22 @@ func initSubcommands() []*x.SubCommand { userAddFlags.StringP("user", "u", "", "The user id to be created") userAddFlags.StringP("password", "p", "", "The password for the user") + // user change password command + var cmdPasswd x.SubCommand + cmdPasswd.Cmd = &cobra.Command{ + Use: "passwd", + Short: "Run Dgraph acl tool to change a user's password", + Run: func(cmd *cobra.Command, args []string) { + if err := userPasswd(cmdPasswd.Conf); err != nil { + fmt.Printf("Unable to change password for user: %v\n", err) + os.Exit(1) + } + }, + } + chPwdFlags := cmdPasswd.Cmd.Flags() + chPwdFlags.StringP("user", "u", "", "The user id to be created") + chPwdFlags.StringP("new_password", "", "", "The new password for the user") + // user deletion command var cmdUserDel x.SubCommand cmdUserDel.Cmd = &cobra.Command{ @@ -91,7 +107,7 @@ func initSubcommands() []*x.SubCommand { Short: "Run Dgraph acl tool to delete a user", Run: func(cmd *cobra.Command, args []string) { if err := userDel(cmdUserDel.Conf); err != nil { - glog.Errorf("Unable to delete the user:%v", err) + fmt.Printf("Unable to delete the user: %v\n", err) os.Exit(1) } }, @@ -106,7 +122,7 @@ func initSubcommands() []*x.SubCommand { Short: "Run Dgraph acl tool to add a group", Run: func(cmd *cobra.Command, args []string) { if err := groupAdd(cmdGroupAdd.Conf); err != nil { - glog.Errorf("Unable to add group:%v", err) + fmt.Printf("Unable to add group: %v\n", err) os.Exit(1) } }, @@ -121,7 +137,7 @@ func initSubcommands() []*x.SubCommand { Short: "Run Dgraph acl tool to delete a group", Run: func(cmd *cobra.Command, args []string) { if err := groupDel(cmdGroupDel.Conf); err != nil { - glog.Errorf("Unable to delete group:%v", err) + fmt.Printf("Unable to delete group: %v\n", err) os.Exit(1) } }, @@ -136,7 +152,7 @@ func initSubcommands() []*x.SubCommand { Short: "Run Dgraph acl tool to change a user's groups", Run: func(cmd *cobra.Command, args []string) { if err := userMod(cmdUserMod.Conf); err != nil { - glog.Errorf("Unable to modify user:%v", err) + fmt.Printf("Unable to modify user: %v\n", err) os.Exit(1) } }, @@ -152,7 +168,7 @@ func initSubcommands() []*x.SubCommand { Short: "Run Dgraph acl tool to change a group's permissions", Run: func(cmd *cobra.Command, args []string) { if err := chMod(cmdChMod.Conf); err != nil { - glog.Errorf("Unable to change permission for group:%v", err) + fmt.Printf("Unable to change permisson for group: %v\n", err) os.Exit(1) } }, @@ -162,8 +178,10 @@ func initSubcommands() []*x.SubCommand { "is to be changed") chModFlags.StringP("pred", "p", "", "The predicates whose acls"+ " are to be changed") + chModFlags.StringP("pred_regex", "", "", "The regular expression specifying predicates"+ + " whose acls are to be changed") chModFlags.IntP("perm", "P", 0, "The acl represented using "+ - "an integer, 4 for read-only, 2 for write-only, and 1 for modify-only") + "an integer: 4 for read, 2 for write, and 1 for modify.") var cmdInfo x.SubCommand cmdInfo.Cmd = &cobra.Command{ @@ -171,7 +189,7 @@ func initSubcommands() []*x.SubCommand { Short: "Show info about a user or group", Run: func(cmd *cobra.Command, args []string) { if err := info(cmdInfo.Conf); err != nil { - glog.Errorf("Unable to show info:%v", err) + fmt.Printf("Unable to show info: %v\n", err) os.Exit(1) } }, @@ -180,7 +198,7 @@ func initSubcommands() []*x.SubCommand { infoFlags.StringP("user", "u", "", "The user to be shown") infoFlags.StringP("group", "g", "", "The group to be shown") return []*x.SubCommand{ - &cmdUserAdd, &cmdUserDel, &cmdGroupAdd, &cmdGroupDel, &cmdUserMod, + &cmdUserAdd, &cmdPasswd, &cmdUserDel, &cmdGroupAdd, &cmdGroupDel, &cmdUserMod, &cmdChMod, &cmdInfo, } } @@ -191,7 +209,7 @@ func getDgraphClient(conf *viper.Viper) (*dgo.Dgraph, CloseFunc) { opt = options{ dgraph: conf.GetString("dgraph"), } - glog.Infof("Running transaction with dgraph endpoint: %v", opt.dgraph) + fmt.Printf("\nRunning transaction with dgraph endpoint: %v\n", opt.dgraph) if len(opt.dgraph) == 0 { glog.Fatalf("The --dgraph option must be set in order to connect to dgraph") @@ -206,7 +224,7 @@ func getDgraphClient(conf *viper.Viper) (*dgo.Dgraph, CloseFunc) { dc := api.NewDgraphClient(conn) return dgo.NewDgraphClient(dc), func() { if err := conn.Close(); err != nil { - glog.Errorf("Error while closing connection:%v", err) + fmt.Printf("Error while closing connection: %v\n", err) } } } @@ -216,19 +234,19 @@ func info(conf *viper.Viper) error { groupId := conf.GetString("group") if (len(userId) == 0 && len(groupId) == 0) || (len(userId) != 0 && len(groupId) != 0) { - return fmt.Errorf("Either the user or group should be specified, not both") + return fmt.Errorf("either the user or group should be specified, not both") } dc, cancel, err := getClientWithAdminCtx(conf) defer cancel() if err != nil { - return fmt.Errorf("unable to get admin context:%v", err) + return fmt.Errorf("unable to get admin context: %v\n", err) } ctx := context.Background() txn := dc.NewTxn() defer func() { if err := txn.Discard(ctx); err != nil { - glog.Errorf("Unable to discard transaction:%v", err) + fmt.Printf("Unable to discard transaction: %v\n", err) } }() @@ -238,15 +256,12 @@ func info(conf *viper.Viper) error { return err } - var userBuf strings.Builder - userBuf.WriteString(fmt.Sprintf("user %v:\n", userId)) - userBuf.WriteString(fmt.Sprintf("uid:%v\nid:%v\n", user.Uid, user.UserID)) - var groupNames []string + fmt.Println() + fmt.Printf("User : %-5s\n", userId) + fmt.Printf("UID : %-5s\n", user.Uid) for _, group := range user.Groups { - groupNames = append(groupNames, group.GroupID) + fmt.Printf("Group : %-5s\n", group.GroupID) } - userBuf.WriteString(fmt.Sprintf("groups:%v\n", strings.Join(groupNames, " "))) - glog.Infof(userBuf.String()) } if len(groupId) != 0 { @@ -255,30 +270,29 @@ func info(conf *viper.Viper) error { if err != nil { return err } - // build the info string for group - var groupSB strings.Builder - groupSB.WriteString(fmt.Sprintf("group %v:\n", groupId)) - groupSB.WriteString(fmt.Sprintf("uid:%v\nid:%v\n", group.Uid, group.GroupID)) + if group == nil { + fmt.Printf("The group %s does not exist.\n", groupId) + return nil + } + fmt.Printf("Group: %5s\n", groupId) + fmt.Printf("UID : %5s\n", group.Uid) + fmt.Printf("ID : %5s\n", group.GroupID) var userNames []string for _, user := range group.Users { userNames = append(userNames, user.UserID) } - groupSB.WriteString(fmt.Sprintf("users:%v\n", strings.Join(userNames, " "))) + fmt.Printf("Users: %5s\n", strings.Join(userNames, " ")) - var aclStrs []string var acls []Acl if err := json.Unmarshal([]byte(group.Acls), &acls); err != nil { - return fmt.Errorf("Unable to unmarshal the acls associated with the group %v:%v", + return fmt.Errorf("unable to unmarshal the acls associated with the group %v: %v", groupId, err) } for _, acl := range acls { - aclStrs = append(aclStrs, fmt.Sprintf("(predicate:%v,perm:%v)", acl.Predicate, acl.Perm)) + fmt.Printf("ACL : %5v\n", acl) } - groupSB.WriteString(fmt.Sprintf("acls:%v\n", strings.Join(aclStrs, " "))) - - glog.Infof(groupSB.String()) } return nil diff --git a/ee/acl/users.go b/ee/acl/users.go index 1c4174bd200..4b9d2385034 100644 --- a/ee/acl/users.go +++ b/ee/acl/users.go @@ -24,22 +24,84 @@ import ( "github.com/spf13/viper" ) +func userPasswd(conf *viper.Viper) error { + userid := conf.GetString("user") + if len(userid) == 0 { + return fmt.Errorf("the user must not be empty") + } + + // 1. get the dgo client with appropriete access JWT + dc, cancel, err := getClientWithAdminCtx(conf) + if err != nil { + return fmt.Errorf("unable to get dgo client:%v", err) + } + defer cancel() + + // 2. get the new password + newPassword := conf.GetString("new_password") + if len(newPassword) == 0 { + var err error + newPassword, err = askUserPassword(userid, "New", 2) + if err != nil { + return err + } + } + + ctx := context.Background() + txn := dc.NewTxn() + defer func() { + if err := txn.Discard(ctx); err != nil { + glog.Errorf("Unable to discard transaction:%v", err) + } + }() + + // 3. query the user's current uid + user, err := queryUser(ctx, txn, userid) + if err != nil { + return fmt.Errorf("error while querying user:%v", err) + } + if user == nil { + return fmt.Errorf("the user does not exist: %v", userid) + } + + // 4. mutate the user's password + chPdNQuads := []*api.NQuad{ + { + Subject: user.Uid, + Predicate: "dgraph.password", + ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: newPassword}}, + }} + mu := &api.Mutation{ + CommitNow: true, + Set: chPdNQuads, + } + if _, err := txn.Mutate(ctx, mu); err != nil { + return fmt.Errorf("unable to change password for user %v: %v", userid, err) + } + fmt.Printf("Successfully changed password for %v\n", userid) + return nil +} + func userAdd(conf *viper.Viper) error { userid := conf.GetString("user") password := conf.GetString("password") - if len(userid) == 0 { - return fmt.Errorf("The user must not be empty") - } - if len(password) == 0 { - return fmt.Errorf("The password must not be empty") + return fmt.Errorf("the user must not be empty") } dc, cancel, err := getClientWithAdminCtx(conf) - defer cancel() if err != nil { return fmt.Errorf("unable to get admin context:%v", err) } + defer cancel() + + if len(password) == 0 { + var err error + password, err = askUserPassword(userid, "New", 2) + if err != nil { + return err + } + } ctx := context.Background() txn := dc.NewTxn() @@ -51,10 +113,10 @@ func userAdd(conf *viper.Viper) error { user, err := queryUser(ctx, txn, userid) if err != nil { - return fmt.Errorf("Error while querying user:%v", err) + return fmt.Errorf("error while querying user:%v", err) } if user != nil { - return fmt.Errorf("Unable to create user because of conflict: %v", userid) + return fmt.Errorf("unable to create user because of conflict: %v", userid) } createUserNQuads := []*api.NQuad{ @@ -75,10 +137,10 @@ func userAdd(conf *viper.Viper) error { } if _, err := txn.Mutate(ctx, mu); err != nil { - return fmt.Errorf("Unable to create user: %v", err) + return fmt.Errorf("unable to create user: %v", err) } - glog.Infof("Created new user with id %v", userid) + fmt.Printf("Created new user with id %v\n", userid) return nil } @@ -86,14 +148,14 @@ func userDel(conf *viper.Viper) error { userid := conf.GetString("user") // validate the userid if len(userid) == 0 { - return fmt.Errorf("The user id should not be empty") + return fmt.Errorf("the user id should not be empty") } dc, cancel, err := getClientWithAdminCtx(conf) - defer cancel() if err != nil { return fmt.Errorf("unable to get admin context:%v", err) } + defer cancel() ctx := context.Background() txn := dc.NewTxn() @@ -105,11 +167,11 @@ func userDel(conf *viper.Viper) error { user, err := queryUser(ctx, txn, userid) if err != nil { - return fmt.Errorf("Error while querying user:%v", err) + return fmt.Errorf("error while querying user:%v", err) } if user == nil || len(user.Uid) == 0 { - return fmt.Errorf("Unable to delete user because it does not exist: %v", userid) + return fmt.Errorf("unable to delete user because it does not exist: %v", userid) } deleteUserNQuads := []*api.NQuad{ @@ -125,10 +187,10 @@ func userDel(conf *viper.Viper) error { } if _, err = txn.Mutate(ctx, mu); err != nil { - return fmt.Errorf("Unable to delete user: %v", err) + return fmt.Errorf("unable to delete user: %v", err) } - glog.Infof("Deleted user with id %v", userid) + fmt.Printf("Deleted user with id %v\n", userid) return nil } @@ -150,7 +212,7 @@ func queryUser(ctx context.Context, txn *dgo.Txn, userid string) (user *User, er queryResp, err := txn.QueryWithVars(ctx, query, queryVars) if err != nil { - return nil, fmt.Errorf("Error while query user with id %s: %v", userid, err) + return nil, fmt.Errorf("error while query user with id %s: %v", userid, err) } user, err = UnmarshalUser(queryResp, "user") if err != nil { @@ -163,29 +225,29 @@ func userMod(conf *viper.Viper) error { userId := conf.GetString("user") groups := conf.GetString("groups") if len(userId) == 0 { - return fmt.Errorf("The user must not be empty") + return fmt.Errorf("the user must not be empty") } dc, cancel, err := getClientWithAdminCtx(conf) - defer cancel() if err != nil { return fmt.Errorf("unable to get admin context:%v", err) } + defer cancel() ctx := context.Background() txn := dc.NewTxn() defer func() { if err := txn.Discard(ctx); err != nil { - glog.Errorf("Unable to discard transaction:%v", err) + fmt.Printf("Unable to discard transaction: %v\n", err) } }() user, err := queryUser(ctx, txn, userId) if err != nil { - return fmt.Errorf("Error while querying user:%v", err) + return fmt.Errorf("error while querying user:%v", err) } if user == nil { - return fmt.Errorf("The user does not exist: %v", userId) + return fmt.Errorf("the user does not exist: %v", userId) } targetGroupsMap := make(map[string]struct{}) @@ -208,31 +270,31 @@ func userMod(conf *viper.Viper) error { } for _, g := range newGroups { - glog.Infof("Adding user %v to group %v", userId, g) + fmt.Printf("Adding user %v to group %v\n", userId, g) nquad, err := getUserModNQuad(ctx, txn, user.Uid, g) if err != nil { - return fmt.Errorf("Error while getting the user mod nquad:%v", err) + return fmt.Errorf("error while getting the user mod nquad: %v", err) } mu.Set = append(mu.Set, nquad) } for _, g := range groupsToBeDeleted { - glog.Infof("Deleting user %v from group %v", userId, g) + fmt.Printf("Deleting user %v from group %v\n", userId, g) nquad, err := getUserModNQuad(ctx, txn, user.Uid, g) if err != nil { - return fmt.Errorf("Error while getting the user mod nquad:%v", err) + return fmt.Errorf("error while getting the user mod nquad: %v", err) } mu.Del = append(mu.Del, nquad) } if len(mu.Del) == 0 && len(mu.Set) == 0 { - glog.Infof("Nothing needs to be changed for the groups of user:%v", userId) + fmt.Printf("Nothing needs to be changed for the groups of user: %v\n", userId) return nil } if _, err := txn.Mutate(ctx, mu); err != nil { - return fmt.Errorf("Error while mutating the group:%+v", err) + return fmt.Errorf("error while mutating the group: %+v", err) } - glog.Infof("Successfully modified groups for user %v", userId) + fmt.Printf("Successfully modified groups for user %v\n", userId) return nil } @@ -243,7 +305,7 @@ func getUserModNQuad(ctx context.Context, txn *dgo.Txn, userId string, return nil, err } if group == nil { - return nil, fmt.Errorf("The group does not exist:%v", groupId) + return nil, fmt.Errorf("the group does not exist:%v", groupId) } createUserGroupNQuads := &api.NQuad{ diff --git a/ee/acl/utils.go b/ee/acl/utils.go index 60a852b8d82..a399a7de817 100644 --- a/ee/acl/utils.go +++ b/ee/acl/utils.go @@ -16,6 +16,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "syscall" "time" @@ -87,13 +88,20 @@ func UnmarshalUser(resp *api.Response, userKey string) (user *User, err error) { return &users[0], nil } +// an Acl can have either a single predicate or a regex that can be used to +// match multiple predicates +type Acl struct { + Predicate string `json:"predicate"` + Regex string `json:"regex"` + Perm int32 `json:"perm"` +} + // parse the response and check existing of the uid type Group struct { - Uid string `json:"uid"` - GroupID string `json:"dgraph.xid"` - Users []User `json:"~dgraph.user.group"` - Acls string `json:"dgraph.group.acl"` - MappedAcls map[string]int32 // only used in memory for acl enforcement + Uid string `json:"uid"` + GroupID string `json:"dgraph.xid"` + Users []User `json:"~dgraph.user.group"` + Acls string `json:"dgraph.group.acl"` } // Extract the first User pointed by the userKey in the query response @@ -116,21 +124,6 @@ func UnmarshalGroup(input []byte, groupKey string) (group *Group, err error) { return &groups[0], nil } -// convert the acl blob to a map from predicates to permissions -func UnmarshalAcl(aclBytes []byte) (map[string]int32, error) { - var acls []Acl - if len(aclBytes) != 0 { - if err := json.Unmarshal(aclBytes, &acls); err != nil { - return nil, fmt.Errorf("unable to unmarshal the aclBytes: %v", err) - } - } - mappedAcls := make(map[string]int32) - for _, acl := range acls { - mappedAcls[acl.Predicate] = acl.Perm - } - return mappedAcls, nil -} - // Extract a sequence of groups from the input func UnmarshalGroups(input []byte, groupKey string) (group []Group, err error) { m := make(map[string][]Group) @@ -147,15 +140,43 @@ type JwtGroup struct { Group string } -func getClientWithAdminCtx(conf *viper.Viper) (*dgo.Dgraph, CloseFunc, error) { - adminPassword := conf.GetString(gPassword) - if len(adminPassword) == 0 { - fmt.Print("Enter groot password:") - password, err := terminal.ReadPassword(int(syscall.Stdin)) +func askUserPassword(userid string, pwdType string, times int) (string, error) { + x.AssertTrue(times == 1 || times == 2) + x.AssertTrue(pwdType == "Current" || pwdType == "New") + // ask for the user's password + fmt.Printf("%s password for %v:", pwdType, userid) + pd, err := terminal.ReadPassword(int(syscall.Stdin)) + if err != nil { + return "", fmt.Errorf("error while reading password:%v", err) + } + fmt.Println() + password := string(pd) + + if times == 2 { + fmt.Printf("Retype %s password for %v:", strings.ToLower(pwdType), userid) + pd2, err := terminal.ReadPassword(int(syscall.Stdin)) if err != nil { - return nil, func() {}, fmt.Errorf("error while reading password:%v", err) + return "", fmt.Errorf("error while reading password:%v", err) + } + fmt.Println() + + password2 := string(pd2) + if password2 != password { + return "", fmt.Errorf("the two typed passwords do not match") + } + } + return password, nil +} + +func getClientWithUserCtx(userid string, passwordOpt string, conf *viper.Viper) (*dgo.Dgraph, + CloseFunc, error) { + password := conf.GetString(passwordOpt) + if len(password) == 0 { + var err error + password, err = askUserPassword(userid, "Current", 1) + if err != nil { + return nil, nil, err } - adminPassword = string(password) } dc, closeClient := getDgraphClient(conf) @@ -166,10 +187,14 @@ func getClientWithAdminCtx(conf *viper.Viper) (*dgo.Dgraph, CloseFunc, error) { closeClient() } - if err := dc.Login(ctx, x.GrootId, adminPassword); err != nil { - return dc, cleanFunc, fmt.Errorf("unable to login to the groot account %v", err) + if err := dc.Login(ctx, userid, password); err != nil { + return dc, cleanFunc, fmt.Errorf("unable to login to the %v account:%v", userid, err) } - glog.Infof("login successfully to the groot account") + fmt.Println("Login successful.") // update the context so that it has the admin jwt token return dc, cleanFunc, nil } + +func getClientWithAdminCtx(conf *viper.Viper) (*dgo.Dgraph, CloseFunc, error) { + return getClientWithUserCtx(x.GrootId, gPassword, conf) +} diff --git a/ee/backup/systest/backup_test.go b/ee/backup/systest/backup_test.go index 6b452497768..ac1bac3e064 100644 --- a/ee/backup/systest/backup_test.go +++ b/ee/backup/systest/backup_test.go @@ -31,16 +31,14 @@ import ( "github.com/dgraph-io/dgo" "github.com/dgraph-io/dgo/protos/api" "github.com/dgraph-io/dgraph/x" + "github.com/dgraph-io/dgraph/z" "github.com/stretchr/testify/require" - "google.golang.org/grpc" ) func TestBackup(t *testing.T) { wrap := func(fn func(*testing.T, *dgo.Dgraph)) func(*testing.T) { return func(t *testing.T) { - conn, err := grpc.Dial("localhost:9180", grpc.WithInsecure()) - x.Check(err) - dg := dgo.NewDgraphClient(api.NewDgraphClient(conn)) + dg := z.DgraphClient("localhost:9180") fn(t, dg) } } @@ -53,8 +51,6 @@ func TestBackup(t *testing.T) { func BackupSetup(t *testing.T, c *dgo.Dgraph) { ctx := context.Background() - require.NoError(t, c.Alter(ctx, &api.Operation{DropAll: true})) - schema, err := ioutil.ReadFile(`data/goldendata.schema`) require.NoError(t, err) require.NoError(t, c.Alter(ctx, &api.Operation{Schema: string(schema)})) diff --git a/ee/backup/systest/docker-compose.yml b/ee/backup/systest/docker-compose.yml index 46fb58628e9..84a3b0cdd9f 100644 --- a/ee/backup/systest/docker-compose.yml +++ b/ee/backup/systest/docker-compose.yml @@ -13,7 +13,7 @@ services: ports: - 5080:5080 - 6080:6080 - command: /gobin/dgraph zero --my=zero1:5080 --bindall --expose_trace --profile_mode block --block_rate 10 --logtostderr -v=2 --enterprise_features + command: /gobin/dgraph zero --my=zero1:5080 --bindall --expose_trace --profile_mode block --block_rate 10 --logtostderr -v=2 volumes: - type: bind source: $GOPATH/bin diff --git a/x/keys.go b/x/keys.go index c6018ae3d36..7599bd72649 100644 --- a/x/keys.go +++ b/x/keys.go @@ -311,3 +311,14 @@ func IsReservedPredicate(pred string) bool { _, ok := m[strings.ToLower(pred)] return ok } + +func IsAclPredicate(pred string) bool { + var m = map[string]struct{}{ + "dgraph.xid": {}, + "dgraph.password": {}, + "dgraph.user.group": {}, + "dgraph.group.acl": {}, + } + _, ok := m[pred] + return ok +} diff --git a/z/client.go b/z/client.go index 316aeeeef33..603b9c576d5 100644 --- a/z/client.go +++ b/z/client.go @@ -19,7 +19,9 @@ package z import ( "context" "encoding/json" + "strings" "testing" + "time" "github.com/dgraph-io/dgo" "github.com/dgraph-io/dgo/protos/api" @@ -34,7 +36,15 @@ func DgraphClient(serviceAddr string) *dgo.Dgraph { x.Check(err) dg := dgo.NewDgraphClient(api.NewDgraphClient(conn)) - err = dg.Alter(context.Background(), &api.Operation{DropAll: true}) + for { + // keep retrying until we succeed or receive a non-retriable error + err = dg.Alter(context.Background(), &api.Operation{DropAll: true}) + if err == nil || !strings.Contains(err.Error(), "Please retry") { + break + } + time.Sleep(time.Second) + } + x.Check(err) return dg