diff --git a/dgraph/cmd/alpha/run.go b/dgraph/cmd/alpha/run.go index f7d55a264a5..b5bbfce7b36 100644 --- a/dgraph/cmd/alpha/run.go +++ b/dgraph/cmd/alpha/run.go @@ -443,9 +443,8 @@ func run() { secretFile := Alpha.Conf.GetString("hmac_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 +452,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/edgraph/access_ee.go b/edgraph/access_ee.go index 337957415b2..f37293b4e64 100644 --- a/edgraph/access_ee.go +++ b/edgraph/access_ee.go @@ -14,7 +14,9 @@ package edgraph import ( "context" + "encoding/json" "fmt" + "regexp" "sync" "time" @@ -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") @@ -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 } @@ -278,6 +280,45 @@ func authorizeUser(ctx context.Context, userid string, password string) (*acl.Us return user, nil } +type PredRegexRule struct { + PredRegex *regexp.Regexp + Perms int32 +} + +// UnmarshalAcl converts the acl blob to two data sets: +// the first one being a map from the single predicates to permissions; +// and the second one being a slice of predicate regex rules +func UnmarshalAcl(aclBytes []byte) (map[string]int32, []*PredRegexRule, error) { + var acls []acl.Acl + if len(aclBytes) == 0 { + return nil, nil, fmt.Errorf("the acl bytes are empty") + } + if err := json.Unmarshal(aclBytes, &acls); err != nil { + return nil, nil, fmt.Errorf("unable to unmarshal the aclBytes: %v", err) + } + + predPerms := make(map[string]int32) + var predRegexPerms []*PredRegexRule + for _, acl := range acls { + if acl.PredFilter.IsRegex { + predRegex, err := regexp.Compile(acl.PredFilter.Regex) + if err != nil { + glog.Errorf("Unable to compile the predicate regex %v to create an ACL rule", + acl.PredFilter.Regex) + continue + } + + predRegexPerms = append(predRegexPerms, &PredRegexRule{ + PredRegex: predRegex, + Perms: acl.Perm, + }) + } else { + predPerms[acl.PredFilter.Predicate] = acl.Perm + } + } + return predPerms, predRegexPerms, nil +} + func RefreshAcls(closer *y.Closer) { defer closer.Done() if len(Config.HmacSecret) == 0 { @@ -291,7 +332,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, } @@ -310,16 +351,26 @@ func RefreshAcls(closer *y.Closer) { storedEntries := 0 for _, group := range groups { // convert the serialized acl into a map for easy lookups - group.MappedAcls, err = acl.UnmarshalAcl([]byte(group.Acls)) + predPerms, predRegexPerms, err := 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) + if len(predPerms) > 0 { + aclCache.predPerms.Store(group.GroupID, predPerms) + } else { + aclCache.predPerms.Delete(group.GroupID) + } + + if len(predRegexPerms) > 0 { + aclCache.predRegexPerms.Store(group.GroupID, predRegexPerms) + } else { + aclCache.predRegexPerms.Delete(group.GroupID) + } + storedEntries += len(predPerms) + len(predRegexPerms) } - glog.V(1).Infof("Updated the ACL cache with %d entries", storedEntries) + glog.V(3).Infof("Updated the ACL cache with %d entries", storedEntries) return nil } @@ -345,7 +396,12 @@ const queryAcls = ` ` // the acl cache mapping group names to the corresponding group acls -var aclCache sync.Map +type AclCache struct { + predPerms sync.Map + predRegexPerms sync.Map +} + +var aclCache AclCache // clear the aclCache and upsert the Groot account. func ResetAcl() { @@ -404,7 +460,10 @@ func ResetAcl() { return nil } - aclCache = sync.Map{} + aclCache = AclCache{ + predPerms: sync.Map{}, + predRegexPerms: sync.Map{}, + } for { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -452,10 +511,15 @@ func authorizeAlter(ctx context.Context, op *api.Operation) error { return fmt.Errorf("only Groot is allowed to drop all data, current user is %s", userData[0]) } + userId := userData[0] 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 { + if err := authorizePredicate(userId, groupIds, + &AccessInfo{ + predicate: op.DropAttr, + operation: acl.Modify, + }); err != nil { return status.Error(codes.PermissionDenied, fmt.Sprintf("unauthorized to alter the predicate:%v", err)) } @@ -467,7 +531,11 @@ func authorizeAlter(ctx context.Context, op *api.Operation) error { return err } for _, update := range updates { - if err := authorizePredicate(groupIds, update.Predicate, acl.Modify); err != nil { + if err := authorizePredicate(userId, groupIds, + &AccessInfo{ + predicate: update.Predicate, + operation: acl.Modify, + }); err != nil { return status.Error(codes.PermissionDenied, fmt.Sprintf("unauthorized to alter the predicate: %v", err)) } @@ -504,10 +572,14 @@ func authorizeMutation(ctx context.Context, mu *api.Mutation) error { if err != nil { return err } - + userId := userData[0] groupIds := userData[1:] for pred := range parsePredsFromMutation(gmu.Set) { - if err := authorizePredicate(groupIds, pred, acl.Write); err != nil { + if err := authorizePredicate(userId, groupIds, + &AccessInfo{ + predicate: pred, + operation: acl.Write, + }); err != nil { return status.Error(codes.PermissionDenied, fmt.Sprintf("unauthorized to mutate the predicate: %v", err)) } @@ -542,6 +614,11 @@ func isGroot(userData []string) bool { return userData[0] == x.GrootId } +type AccessInfo struct { + predicate string + operation *acl.Operation +} + //authorizeQuery authorizes the query using the aclCache func authorizeQuery(ctx context.Context, req *api.Request) error { if len(Config.HmacSecret) == 0 { @@ -565,9 +642,14 @@ func authorizeQuery(ctx context.Context, req *api.Request) error { return err } + userId := userData[0] groupIds := userData[1:] for pred := range parsePredsFromQuery(parsedReq.Query) { - if err := authorizePredicate(groupIds, pred, acl.Read); err != nil { + if err := authorizePredicate(userId, groupIds, + &AccessInfo{ + predicate: pred, + operation: acl.Read, + }); err != nil { return status.Error(codes.PermissionDenied, fmt.Sprintf("unauthorized to query the predicate: %v", err)) } @@ -575,30 +657,57 @@ func authorizeQuery(ctx context.Context, req *api.Request) error { return nil } -func authorizePredicate(groups []string, predicate string, operation *acl.Operation) error { +func authorizePredicate(userId string, groups []string, accessInfo *AccessInfo) error { for _, group := range groups { - if err := hasAccess(group, predicate, operation); err == nil { + err := hasAccess(group, accessInfo.predicate, accessInfo.operation) + glog.V(1).Infof("ACL-LOG Authorizing user %v in group %v on predicate %v "+ + "for %s, allowed %v", + userId, group, accessInfo.predicate, accessInfo.operation.Name, err != nil) + + if err == nil { + // the operation is allowed as soon as one group has the required permissions return nil } } - return fmt.Errorf("unauthorized to do %s on predicate %s", operation.Name, predicate) + return fmt.Errorf("unauthorized to do %s on predicate %s", + accessInfo.operation.Name, accessInfo.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) + // First, try to evaluate the request using the predicate -> permission map. + predPerms, found := aclCache.predPerms.Load(groupId) + if found { + predPermsMap := predPerms.(map[string]int32) + perm, permFound := predPermsMap[predicate] + if permFound && (perm&operation.Code) != 0 { + glog.V(1).Infof("matched request (group:%v,predicate:%v,operation:%v) "+ + "with rule (group:%v,predicate:%v,perm:%v)", + groupId, predicate, operation.Name, groupId, predicate, perm) + return nil + } } - return nil + + // if we get here, try to evaluate the request using the regex pred filters + predRegex, predRegexFound := aclCache.predRegexPerms.Load(groupId) + if predRegexFound { + // the predRegex is slice of pred regular expressions and permissions, e.g. + // ^user(.*)name$, 4 + // ^friend, 6 + predRegexRules := predRegex.([]*PredRegexRule) + // iterate through all the regex predicate filters and see if any one can match the given + // predicate + for _, predRegex := range predRegexRules { + if predRegex.PredRegex.MatchString(predicate) && + predRegex.Perms&operation.Code != 0 { + glog.V(1).Infof("matched request (group:%v,predicate:%v,operation:%v) "+ + "with rule (group:%v,predicate regex:%v,perm:%v)", + groupId, predicate, operation.Name, groupId, predRegex.PredRegex, predRegex.Perms) + return nil + } + } + } + + return fmt.Errorf("unable to find a matching rule") } diff --git a/ee/acl/acl_test.go b/ee/acl/acl_test.go index c08391b726a..229aec0e87b 100644 --- a/ee/acl/acl_test.go +++ b/ee/acl/acl_test.go @@ -112,7 +112,7 @@ func testAuthorization(t *testing.T, dg *dgo.Dgraph) { alterPredicateWithUserAccount(t, dg, 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") + log.Println("Sleeping for 35 seconds for acl caches to be refreshed") time.Sleep(35 * time.Second) queryPredicateWithUserAccount(t, dg, false) // sleep long enough (10s per the docker-compose.yml in this directory) @@ -266,3 +266,92 @@ 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") + + 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", group, "-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", group, "-x", "password") + if err := addUserToGroupCmd.Run(); err != nil { + t.Fatalf("Unable to add user %s to group %s:%v", userid, group, err) + } + + addReadToNameCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), + "acl", "chmod", + "-d", dgraphEndpoint, + "-g", group, "--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", group, 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", group, "--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, group, err) + } + + log.Println("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) +} diff --git a/ee/acl/groups.go b/ee/acl/groups.go index 2ebffea497e..63c8447c689 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", 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", 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", 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", 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 query group with id %s: %v", groupid, err) return nil, err } group, err = UnmarshalGroup(queryResp.GetJson(), "group") @@ -145,65 +145,99 @@ func queryGroup(ctx context.Context, txn *dgo.Txn, groupid string, return group, nil } +// an entity can be either a single predicate or a regex that can be used to +// match multiple predicates +type PredFilter struct { + IsRegex bool + Predicate string + Regex string +} + type Acl struct { - Predicate string `json:"predicate"` - Perm int32 `json:"perm"` + PredFilter PredFilter + 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("one of --pred or --pred_regex must be specified") + } + if len(predicate) == 0 && len(predRegex) == 0 { + return fmt.Errorf("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", 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", 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{ + PredFilter: PredFilter{ + IsRegex: false, + Predicate: predicate, + }, + Perm: int32(perm), + } + } else { + newAcl = Acl{ + PredFilter: PredFilter{ + IsRegex: true, + 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", 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 +251,25 @@ 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", groupId, predicate, perm) return nil } +func isSameFilter(filter1 *PredFilter, filter2 *PredFilter) bool { + return (!filter1.IsRegex && !filter2.IsRegex && filter1.Predicate == filter2.Predicate) || + (filter1.IsRegex && filter2.IsRegex && filter1.Regex == filter2.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 isSameFilter(&aclEntry.PredFilter, &newAcl.PredFilter) { 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..bb1746458c4 100644 --- a/ee/acl/groups_test.go +++ b/ee/acl/groups_test.go @@ -20,44 +20,63 @@ import ( func TestUpdateAcl(t *testing.T) { var currenAcls []Acl newAcl := Acl{ - Predicate: "friend", - Perm: 4, + PredFilter: PredFilter{ + IsRegex: false, + 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, + PredFilter: PredFilter{ + IsRegex: false, + 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, + PredFilter: PredFilter{ + IsRegex: false, + 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].PredFilter.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..f59f73f9f84 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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 { @@ -256,29 +271,29 @@ func info(conf *viper.Viper) error { 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)) + + 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)) + aclStrs = append(aclStrs, fmt.Sprintf("(predicate filter: %v, perm: %v)", + acl.PredFilter, acl.Perm)) } - groupSB.WriteString(fmt.Sprintf("acls:%v\n", strings.Join(aclStrs, " "))) - - glog.Infof(groupSB.String()) + fmt.Sprintf("ACLs : %5s\n", strings.Join(aclStrs, " ")) } return nil diff --git a/ee/acl/users.go b/ee/acl/users.go index 1c4174bd200..3d3b8a38229 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, 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, 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..75b52eb00e3 100644 --- a/ee/acl/utils.go +++ b/ee/acl/utils.go @@ -89,11 +89,10 @@ func UnmarshalUser(resp *api.Response, userKey string) (user *User, err error) { // 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 +115,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 +131,42 @@ 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, times int) (string, error) { + x.AssertTrue(times == 1 || times == 2) + // ask for the user's password + fmt.Printf("Password for %v:", 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 password for %v:", 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, 1) + if err != nil { + return nil, nil, err } - adminPassword = string(password) } dc, closeClient := getDgraphClient(conf) @@ -166,10 +177,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) +}