From 3ca08aed8655da497997ae5ceb979ce78591072b Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Mon, 14 Jan 2019 12:01:09 -0800 Subject: [PATCH 01/15] Enforcing acl --- dgraph/cmd/alpha/run.go | 31 ++- edgraph/access.go | 21 ++ edgraph/access_ee.go | 426 +++++++++++++++++++++++++++--- edgraph/config.go | 20 +- edgraph/server.go | 34 ++- ee/acl/acl_test.go | 275 ++++++++++++++----- ee/acl/docker-compose.yml | 78 ++++++ ee/acl/groups.go | 28 +- ee/acl/hmac-secret | 1 + ee/acl/run_ee.go | 25 +- ee/acl/users.go | 87 +++--- ee/acl/utils.go | 60 ++++- systest/bulk_live_cases_test.go | 6 +- systest/bulk_live_fixture_test.go | 60 ++--- systest/cluster_setup_test.go | 8 +- systest/cluster_test.go | 4 +- systest/loader_test.go | 6 +- systest/queries_test.go | 46 +--- worker/groups.go | 1 + x/x.go | 2 +- 20 files changed, 943 insertions(+), 276 deletions(-) create mode 100644 ee/acl/docker-compose.yml create mode 100644 ee/acl/hmac-secret diff --git a/dgraph/cmd/alpha/run.go b/dgraph/cmd/alpha/run.go index ab05843f149..e4716287804 100644 --- a/dgraph/cmd/alpha/run.go +++ b/dgraph/cmd/alpha/run.go @@ -126,11 +126,14 @@ they form a Raft group and provide synchronous replication. "If set, all Alter requests to Dgraph would need to have this token."+ " 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.Duration("access_jwt_ttl", 6*time.Hour, "The TTL for the access jwt. "+ + 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. "+ "Enterprise feature.") - flag.Duration("refresh_jwt_ttl", 30*24*time.Hour, "The TTL for the refresh jwt. "+ + flag.Duration("acl_cache_ttl", 30*time.Second, "The interval to refresh the acl cache. "+ "Enterprise feature.") flag.Float64P("lru_mb", "l", -1, "Estimated memory the LRU cache can take. "+ @@ -408,14 +411,25 @@ 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 " + + "--enterprise_features option in order to use ACL.") + os.Exit(1) + } + hmacSecret, err := ioutil.ReadFile(secretFile) if err != nil { 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) + } opts.HmacSecret = hmacSecret - opts.AccessJwtTtl = Alpha.Conf.GetDuration("access_jwt_ttl") - opts.RefreshJwtTtl = Alpha.Conf.GetDuration("refresh_jwt_ttl") + opts.AccessJwtTtl = Alpha.Conf.GetDuration("acl_access_ttl") + opts.RefreshJwtTtl = Alpha.Conf.GetDuration("acl_refresh_ttl") + opts.AclRefreshInterval = Alpha.Conf.GetDuration("acl_cache_ttl") glog.Info("HMAC secret loaded successfully.") } @@ -516,7 +530,14 @@ func run() { _ = numShutDownSig // Setup external communication. - go worker.StartRaftNodes(edgraph.State.WALstore, bindall) + go func() { + worker.StartRaftNodes(edgraph.State.WALstore, bindall) + // initialization of the admin account can only be done after raft nodes are running + // and health check passes + edgraph.ResetAcl() + edgraph.RefreshAcls(shutdownCh) + }() + setupServer() glog.Infoln("GRPC and HTTP stopped.") worker.BlockingStop() diff --git a/edgraph/access.go b/edgraph/access.go index f347f489294..81f9212e873 100644 --- a/edgraph/access.go +++ b/edgraph/access.go @@ -32,3 +32,24 @@ func (s *Server) Login(ctx context.Context, glog.Warningf("Login failed: %s", x.ErrNotSupported) return &api.Response{}, x.ErrNotSupported } + +func ResetAcl() { + // do nothing +} + +func RefreshAcls(closeCh <-chan struct{}) { + // do nothing +} + +func authorizeAlter(ctx context.Context, op *api.Operation) error { + return nil +} + +func authorizeMutation(ctx context.Context, mu *api.Mutation) error { + return nil +} + +func authorizeQuery(ctx context.Context, req *api.Request) error { + // always allow access + return nil +} diff --git a/edgraph/access_ee.go b/edgraph/access_ee.go index c4a21a21a41..ff450f2ef7e 100644 --- a/edgraph/access_ee.go +++ b/edgraph/access_ee.go @@ -17,15 +17,21 @@ import ( "encoding/json" "fmt" "strconv" + "sync" "time" "github.com/dgraph-io/dgo/protos/api" "github.com/dgraph-io/dgraph/ee/acl" - "github.com/dgrijalva/jwt-go" + "github.com/dgraph-io/dgraph/gql" + "github.com/dgraph-io/dgraph/schema" + "github.com/dgraph-io/dgraph/x" + jwt "github.com/dgrijalva/jwt-go" "github.com/golang/glog" - "google.golang.org/grpc/peer" - otrace "go.opencensus.io/trace" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/status" ) func (s *Server) Login(ctx context.Context, @@ -43,7 +49,7 @@ func (s *Server) Login(ctx context.Context, }, "client ip for login") } - user, err := s.authenticate(ctx, request) + user, err := s.authenticateLogin(ctx, request) if err != nil { errMsg := fmt.Sprintf("Authentication from address %s failed: %v", addr, err) glog.Errorf(errMsg) @@ -82,49 +88,60 @@ func (s *Server) Login(ctx context.Context, return resp, nil } -func (s *Server) authenticate(ctx context.Context, request *api.LoginRequest) (*acl.User, error) { +// Authenticate the login request using either the refresh token if present, or the +// pair. If authentication passes, query the user's uid and associated groups +// from DB and returns the user object +func (s *Server) authenticateLogin(ctx context.Context, request *api.LoginRequest) (*acl.User, + error) { if err := validateLoginRequest(request); err != nil { return nil, fmt.Errorf("Invalid login request: %v", err) } var user *acl.User if len(request.RefreshToken) > 0 { - userId, err := authenticateRefreshToken(request.RefreshToken) + userData, err := validateToken(request.RefreshToken) + x.AssertTrue(len(userData) > 0) if err != nil { return nil, fmt.Errorf("Unable to authenticate the refresh token %v: %v", request.RefreshToken, err) } - user, err = s.queryUser(ctx, userId, "") + userId := userData[0] + user, err = authorizeUser(ctx, userId, "") if err != nil { - return nil, fmt.Errorf("Error while querying user with id: %v", - request.Userid) + return nil, fmt.Errorf("error while querying user with id %v: %v", userId, err) } if user == nil { - return nil, fmt.Errorf("User not found for id %v", request.Userid) - } - } else { - var err error - user, err = s.queryUser(ctx, request.Userid, request.Password) - if err != nil { - return nil, fmt.Errorf("Error while querying user with id: %v", - request.Userid) + return nil, fmt.Errorf("unable to authenticate through refresh token: "+ + "user not found for id %v", userId) } - if user == nil { - return nil, fmt.Errorf("User not found for id %v", request.Userid) - } - if !user.PasswordMatch { - return nil, fmt.Errorf("Password mismatch for user: %v", request.Userid) - } + return user, nil } + // authorize the user using password + var err error + user, err = authorizeUser(ctx, request.Userid, request.Password) + if err != nil { + return nil, fmt.Errorf("error while querying user with id %v: %v", + request.Userid, err) + } + + if user == nil { + return nil, fmt.Errorf("unable to authenticate through password: "+ + "user not found for id %v", request.Userid) + } + if !user.PasswordMatch { + return nil, fmt.Errorf("password mismatch for user: %v", request.Userid) + } return user, nil } -func authenticateRefreshToken(refreshToken string) (string, error) { - token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) { +// verify signature and expiration of the jwt and if validation passes, +// return the extracted userId, and groupIds encoded in the jwt +func validateToken(jwtStr string) (userData []string, err error) { + token, err := jwt.Parse(jwtStr, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } @@ -132,28 +149,43 @@ func authenticateRefreshToken(refreshToken string) (string, error) { }) if err != nil { - return "", fmt.Errorf("Unable to parse refresh token:%v", err) + return nil, fmt.Errorf("unable to parse jwt token:%v", err) } claims, ok := token.Claims.(jwt.MapClaims) if !ok || !token.Valid { - return "", fmt.Errorf("Claims in refresh token is not map claims:%v", refreshToken) + return nil, fmt.Errorf("claims in jwt token is not map claims") } // by default, the MapClaims.Valid will return true if the exp field is not set // here we enforce the checking to make sure that the refresh token has not expired now := time.Now().Unix() if !claims.VerifyExpiresAt(now, true) { - return "", fmt.Errorf("Refresh token has expired: %v", refreshToken) + return nil, fmt.Errorf("jwt token has expired at %v", now) } userId, ok := claims["userid"].(string) if !ok { - return "", fmt.Errorf("User ID in claims is not a string:%v", userId) + return nil, fmt.Errorf("userid in claims is not a string:%v", userId) } - return userId, nil + + groups, ok := claims["groups"].([]interface{}) + var groupIds []string + if ok { + groupIds = make([]string, 0, len(groups)) + for _, group := range groups { + groupId, ok := group.(string) + if !ok { + return nil, fmt.Errorf("unable to convert group to string:%v", group) + } + + groupIds = append(groupIds, groupId) + } + } + return append([]string{userId}, groupIds...), nil } +// validate that the login request has either the refresh token or the pair func validateLoginRequest(request *api.LoginRequest) error { if request == nil { return fmt.Errorf("The request should not be nil") @@ -173,6 +205,8 @@ func validateLoginRequest(request *api.LoginRequest) error { return nil } +// construct an access jwt with the given userid, groupIds, and expiration ttl specified by +// Config.AccessJwtTtl func getAccessJwt(userId string, groups []acl.Group) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "userid": userId, @@ -189,6 +223,8 @@ func getAccessJwt(userId string, groups []acl.Group) (string, error) { return jwtString, nil } +// construct a refresh jwt with the given userid, and expiration ttl specified by +// Config.RefreshJwtTtl func getRefreshJwt(userId string) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "userid": userId, @@ -216,7 +252,9 @@ const queryUser = ` } }` -func (s *Server) queryUser(ctx context.Context, userid string, password string) (user *acl.User, +// query the user with the given userid, and returns associated uid, acl groups, +// and whether the password stored in DB matches the supplied password +func authorizeUser(ctx context.Context, userid string, password string) (user *acl.User, err error) { queryVars := map[string]string{ "$userid": userid, @@ -227,7 +265,7 @@ func (s *Server) queryUser(ctx context.Context, userid string, password string) Vars: queryVars, } - queryResp, err := s.Query(ctx, &queryRequest) + queryResp, err := (&Server{}).doQuery(ctx, &queryRequest) if err != nil { glog.Errorf("Error while query user with id %s: %v", userid, err) return nil, err @@ -238,3 +276,327 @@ func (s *Server) queryUser(ctx context.Context, userid string, password string) } return user, nil } + +func upsertAdmin(ctx context.Context) error { + // upsert the admin account + + queryVars := map[string]string{ + "$userid": "admin", + "$password": "", + } + queryRequest := api.Request{ + Query: queryUser, + Vars: queryVars, + } + + queryResp, err := (&Server{}).doQuery(ctx, &queryRequest) + if err != nil { + return fmt.Errorf("error while query user with id admin: %v", err) + } + startTs := queryResp.GetTxn().StartTs + glog.Infof("admin txn startTs:%v", startTs) + + adminUser, err := acl.UnmarshalUser(queryResp, "user") + if err != nil { + return fmt.Errorf("error while unmarshaling the admin user: %v", err) + } + + if adminUser != nil { + // the admin user already exists, no need to create + return nil + } + + // insert the admin user + createUserNQuads := []*api.NQuad{ + { + Subject: "_:newuser", + Predicate: "dgraph.xid", + ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: "admin"}}, + }, + { + Subject: "_:newuser", + Predicate: "dgraph.password", + ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: "password"}}, + }} + + mu := &api.Mutation{ + StartTs: startTs, + CommitNow: true, + Set: createUserNQuads, + } + + assigned, err := (&Server{}).doMutate(context.Background(), mu) + if err != nil { + return fmt.Errorf("unable to create admin: %v", err) + } + glog.Infof("admin commitTs:%v", assigned.GetContext().GetCommitTs()) + return nil +} + +func RefreshAcls(closeCh <-chan struct{}) { + if len(Config.HmacSecret) == 0 { + // the acl feature is not turned on + return + } + + ticker := time.NewTicker(Config.AclRefreshInterval) + defer ticker.Stop() + + // 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") + queryRequest := api.Request{ + Query: queryAcls, + } + + ctx := context.Background() + var err error + queryResp, err := (&Server{}).doQuery(ctx, &queryRequest) + if err != nil { + return fmt.Errorf("unable to retrieve acls: %v", err) + } + groups, err := acl.UnmarshalGroups(queryResp.GetJson(), "allAcls") + if err != nil { + 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) + return nil + } + + for { + select { + case <-closeCh: + return + case <-ticker.C: + if err := retrieveAcls(); err != nil { + glog.Errorf("Error while retrieving acls:%v", err) + } + } + } +} + +const queryAcls = ` +{ + allAcls(func: has(dgraph.group.acl)) { + dgraph.xid + dgraph.group.acl + } +} +` + +// the acl cache mapping group names to the corresponding group acls +var aclCache sync.Map + +// clear the aclCache and upsert the admin account +func ResetAcl() { + if len(Config.HmacSecret) == 0 { + // the acl feature is not turned on + return + } + + aclCache = sync.Map{} + if err := upsertAdmin(context.Background()); err != nil { + glog.Infof("Unable to upsert the admin account:%v", err) + } else { + glog.Info("Created the admin account with the default password") + } +} + +// 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") + } + accessJwt := md.Get("accessJwt") + if len(accessJwt) == 0 { + //glog.Infof("no accessJwt available, type is %v", reflect.TypeOf(ctx.Value("accessJwt"))) + return nil, fmt.Errorf("no accessJwt available") + } + + return validateToken(accessJwt[0]) +} + +// parse the Schema in the operation and authorize the operation using the aclCache +func authorizeAlter(ctx context.Context, op *api.Operation) error { + if len(Config.HmacSecret) == 0 { + // the user has not turned on the acl feature + return nil + } + + userData, err := extractUserAndGroups(ctx) + x.AssertTrue(len(userData) > 0) + if err != nil { + return status.Error(codes.Unauthenticated, err.Error()) + } + userId := userData[0] + if userId == "admin" { + // admin is allowed to do anything + return nil + } + + // if we get here, we know the user is not admin + if op.DropAll { + return fmt.Errorf("only the admin is allowed to drop all data") + } + + 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 { + return fmt.Errorf("unauthorized to modify the predicate:%v", err) + } + return nil + } + + updates, err := schema.Parse(op.Schema) + if err != nil { + return err + } + for _, update := range updates { + if err := authorizePredicate(groupIds, update.Predicate, acl.Modify); err != nil { + return fmt.Errorf("unauthorized to modify the predicate %v", err) + } + } + return nil +} + +func parsePredsFromMutation(nquads []*api.NQuad) map[string]struct{} { + preds := make(map[string]struct{}) + for _, nquad := range nquads { + preds[nquad.Predicate] = struct{}{} + } + return preds +} + +// authorize the mutation using the aclCache +func authorizeMutation(ctx context.Context, mu *api.Mutation) error { + if len(Config.HmacSecret) == 0 { + // the user has not turned on the acl feature + return nil + } + + userData, err := extractUserAndGroups(ctx) + x.AssertTrue(len(userData) > 0) + if err != nil { + return status.Error(codes.Unauthenticated, err.Error()) + } + + userId := userData[0] + if userId == "admin" { + // the admin account 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 { + return fmt.Errorf("unauthorized to access the predicate: %v", err) + } + } + return nil +} + +func parsePredsFromQuery(gqls []*gql.GraphQuery) map[string]struct{} { + preds := make(map[string]struct{}) + for _, gq := range gqls { + + if gq.Func != nil { + preds[gq.Func.Attr] = struct{}{} + } + + if len(gq.Attr) > 0 { + preds[gq.Attr] = struct{}{} + } + + for childPred := range parsePredsFromQuery(gq.Children) { + preds[childPred] = struct{}{} + } + } + return preds +} + +// authorize the query using the aclCache +func authorizeQuery(ctx context.Context, req *api.Request) error { + if len(Config.HmacSecret) == 0 { + // the user has not turned on the acl feature + return nil + } + + userData, err := extractUserAndGroups(ctx) + x.AssertTrue(len(userData) > 0) + if err != nil { + return status.Error(codes.Unauthenticated, err.Error()) + } + + userId := userData[0] + if userId == "admin" { + // the admin account has access to everything + return nil + } + + parsedReq, err := gql.Parse(gql.Request{ + Str: req.Query, + Variables: req.Vars, + }) + if err != nil { + return err + } + + groupIds := userData[1:] + for pred := range parsePredsFromQuery(parsedReq.Query) { + if err := authorizePredicate(groupIds, pred, acl.Read); err != nil { + return status.Error(codes.PermissionDenied, + fmt.Sprintf("unauthorized to access 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.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/config.go b/edgraph/config.go index a25232657ce..9144d53c394 100644 --- a/edgraph/config.go +++ b/edgraph/config.go @@ -33,18 +33,18 @@ const ( ) type Options struct { - PostingDir string - BadgerTables string - BadgerVlog string - WALDir string - MutationsMode int - AuthToken string - + PostingDir string + BadgerTables string + BadgerVlog string + WALDir string + MutationsMode int + AuthToken string AllottedMemory float64 - HmacSecret []byte - AccessJwtTtl time.Duration - RefreshJwtTtl time.Duration + HmacSecret []byte + AccessJwtTtl time.Duration + RefreshJwtTtl time.Duration + AclRefreshInterval time.Duration } var Config Options diff --git a/edgraph/server.go b/edgraph/server.go index 40cb5ac9218..b5918e3cad4 100644 --- a/edgraph/server.go +++ b/edgraph/server.go @@ -41,7 +41,6 @@ import ( "github.com/dgraph-io/dgraph/types/facets" "github.com/dgraph-io/dgraph/worker" "github.com/dgraph-io/dgraph/x" - "github.com/golang/glog" otrace "go.opencensus.io/trace" "golang.org/x/net/context" @@ -283,6 +282,7 @@ func (s *Server) Alter(ctx context.Context, op *api.Operation) (*api.Payload, er if err := x.HealthCheck(); err != nil { return empty, err } + if !isMutationAllowed(ctx) { return nil, x.Errorf("No mutations allowed by server.") } @@ -290,6 +290,13 @@ func (s *Server) Alter(ctx context.Context, op *api.Operation) (*api.Payload, er glog.Warningf("Alter denied with error: %v\n", err) return nil, err } + + err := authorizeAlter(ctx, op) + if err != nil { + glog.Warningf("Alter denied with error: %v\n", err) + return nil, err + } + // All checks done. defer glog.Infof("ALTER op: %+v done", op) @@ -299,6 +306,9 @@ func (s *Server) Alter(ctx context.Context, op *api.Operation) (*api.Payload, er if op.DropAll { m.DropAll = true _, err := query.ApplyMutations(ctx, m) + + // recreate the admin account after a drop all operation + ResetAcl() return empty, err } if len(op.DropAttr) > 0 { @@ -317,6 +327,7 @@ func (s *Server) Alter(ctx context.Context, op *api.Operation) (*api.Payload, er _, err = query.ApplyMutations(ctx, m) return empty, err } + updates, err := schema.Parse(op.Schema) if err != nil { return empty, err @@ -333,6 +344,14 @@ func annotateStartTs(span *otrace.Span, ts uint64) { } func (s *Server) Mutate(ctx context.Context, mu *api.Mutation) (resp *api.Assigned, err error) { + if err := authorizeMutation(ctx, mu); err != nil { + return nil, fmt.Errorf("mutation is not authorized: %v", err) + } + + return s.doMutate(ctx, mu) +} + +func (s *Server) doMutate(ctx context.Context, mu *api.Mutation) (resp *api.Assigned, err error) { ctx, span := otrace.StartSpan(ctx, "Server.Mutate") defer span.End() @@ -371,6 +390,7 @@ func (s *Server) Mutate(ctx context.Context, mu *api.Mutation) (resp *api.Assign parseEnd := time.Now() l.Parsing = parseEnd.Sub(l.Start) + defer func() { l.Processing = time.Since(parseEnd) resp.Latency = &api.Latency{ @@ -438,9 +458,18 @@ func (s *Server) Mutate(ctx context.Context, mu *api.Mutation) (resp *api.Assign return resp, nil } +func (s *Server) Query(ctx context.Context, req *api.Request) (*api.Response, error) { + if err := authorizeQuery(ctx, req); err != nil { + return nil, fmt.Errorf("query is not authorized: %v", err) + } + + return s.doQuery(ctx, req) +} + // This method is used to execute the query and return the response to the // client as a protocol buffer message. -func (s *Server) Query(ctx context.Context, req *api.Request) (resp *api.Response, err error) { +func (s *Server) doQuery(ctx context.Context, req *api.Request) (resp *api.Response, + err error) { if glog.V(3) { glog.Infof("Got a query: %+v", req) } @@ -475,7 +504,6 @@ func (s *Server) Query(ctx context.Context, req *api.Request) (resp *api.Respons if err != nil { return resp, err } - if req.StartTs == 0 { req.StartTs = State.getTimestamp(req.ReadOnly) } diff --git a/ee/acl/acl_test.go b/ee/acl/acl_test.go index 806c8fcc78c..ad914d070a6 100644 --- a/ee/acl/acl_test.go +++ b/ee/acl/acl_test.go @@ -13,8 +13,21 @@ package acl import ( + "context" + "fmt" + "log" + "os" "os/exec" + "path/filepath" + "strconv" "testing" + "time" + + "github.com/dgraph-io/dgo" + "github.com/dgraph-io/dgo/protos/api" + "github.com/dgraph-io/dgo/test" + "github.com/golang/glog" + "github.com/stretchr/testify/require" ) const ( @@ -23,11 +36,6 @@ const ( dgraphEndpoint = "localhost:9180" ) -func TestAcl(t *testing.T) { - t.Run("create user", CreateAndDeleteUsers) - // t.Run("login", LogIn) -} - func checkOutput(t *testing.T, cmd *exec.Cmd, shouldFail bool) string { out, err := cmd.CombinedOutput() if (!shouldFail && err != nil) || (shouldFail && err == nil) { @@ -38,85 +46,228 @@ func checkOutput(t *testing.T, cmd *exec.Cmd, shouldFail bool) string { return string(out) } -func CreateAndDeleteUsers(t *testing.T) { +func TestCreateAndDeleteUsers(t *testing.T) { + // clean up the user to allow repeated running of this test + cleanUserCmd := exec.Command("dgraph", "acl", "userdel", "-d", dgraphEndpoint, + "-u", userid, "--adminPassword", "password") + cleanUserCmd.Run() + glog.Infof("cleaned up db user state") + createUserCmd1 := exec.Command("dgraph", "acl", "useradd", "-d", dgraphEndpoint, "-u", userid, - "-p", userpassword) - createUserOutput1 := checkOutput(t, createUserCmd1, false) - t.Logf("Got output when creating user:%v", createUserOutput1) + "-p", userpassword, "--adminPassword", "password") + checkOutput(t, createUserCmd1, false) createUserCmd2 := exec.Command("dgraph", "acl", "useradd", "-d", dgraphEndpoint, "-u", userid, - "-p", userpassword) - + "-p", userpassword, "--adminPassword", "password") // create the user again should fail - createUserOutput2 := checkOutput(t, createUserCmd2, true) - t.Logf("Got output when creating user:%v", createUserOutput2) + checkOutput(t, createUserCmd2, true) // delete the user - deleteUserCmd := exec.Command("dgraph", "acl", "userdel", "-d", dgraphEndpoint, "-u", userid) - deleteUserOutput := checkOutput(t, deleteUserCmd, false) - t.Logf("Got output when deleting user:%v", deleteUserOutput) + deleteUserCmd := exec.Command("dgraph", "acl", "userdel", "-d", dgraphEndpoint, "-u", userid, + "--adminPassword", "password") + checkOutput(t, deleteUserCmd, false) // now we should be able to create the user again createUserCmd3 := exec.Command("dgraph", "acl", "useradd", "-d", dgraphEndpoint, "-u", userid, - "-p", userpassword) - createUserOutput3 := checkOutput(t, createUserCmd3, false) - t.Logf("Got output when creating user:%v", createUserOutput3) + "-p", userpassword, "--adminPassword", "password") + checkOutput(t, createUserCmd3, false) } -// TODO(gitlw): Finish this later. -// func LogIn(t *testing.T) { -// delete and recreate the user to ensure a clean state -/* - deleteUserCmd := exec.Command("dgraph", "acl", "userdel", "-d", dgraphEndpoint, "-u", "lucas") - deleteUserOutput := checkOutput(t, deleteUserCmd, false) - createUserCmd := exec.Command("dgraph", "acl", "useradd", "-d", dgraphEndpoint, "-u", "lucas", - "-p", "haha") - createUserOutput := checkOutput(t, createUserCmd, false) -*/ +func resetUser(t *testing.T) { + // delete and recreate the user to ensure a clean state + deleteUserCmd := exec.Command("dgraph", "acl", "userdel", "-d", dgraphEndpoint, + "-u", userid, "--adminPassword", "password") + deleteUserCmd.Run() + glog.Infof("deleted user") -// now try to login with the wrong password + createUserCmd := exec.Command("dgraph", "acl", "useradd", "-d", dgraphEndpoint, "-u", + userid, "-p", userpassword, "--adminPassword", "password") + checkOutput(t, createUserCmd, false) + glog.Infof("created user") +} -//loginWithWrongPassword(t, ctx, adminClient) -//loginWithCorrectPassword(t, ctx, adminClient) -// } +func TestAuthorization(t *testing.T) { + glog.Infof("testing with port 9180") + dg1, cancel := test.GetDgraphClientOnPort(9180) + defer cancel() + testAuthorization(t, dg1) + glog.Infof("done") -/* -func loginWithCorrectPassword(t *testing.T, ctx context.Context, - adminClient api.DgraphAccessClient) { - loginRequest := api.LogInRequest{ - Userid: userid, - Password: userpassword, + glog.Infof("testing with port 9182") + dg2, cancel := test.GetDgraphClientOnPort(9182) + defer cancel() + testAuthorization(t, dg2) + glog.Infof("done") +} + +func testAuthorization(t *testing.T, dg *dgo.Dgraph) { + createAccountAndData(t, dg) + queryPredicateWithUserAccount(t, dg, true) + mutatePredicateWithUserAccount(t, dg, true) + alterPredicateWithUserAccount(t, dg, true) + createGroupAndAcls(t) + // wait for 35 seconds to ensure the new acl have reached all acl caches + // on all alpha servers, this also tests that the automatic login with refresh + // jwt works after the access jwt expires in 30 seconds + log.Println("Sleeping for 35 seconds for acl to catch up") + time.Sleep(35 * time.Second) + + queryPredicateWithUserAccount(t, dg, false) + mutatePredicateWithUserAccount(t, dg, false) + alterPredicateWithUserAccount(t, dg, false) +} + +var predicateToRead = "predicate_to_read" +var queryAttr = "name" +var predicateToWrite = "predicate_to_write" +var predicateToAlter = "predicate_to_alter" +var group = "dev" +var rootDir = filepath.Join(os.TempDir(), "acl_test") + +func queryPredicateWithUserAccount(t *testing.T, dg *dgo.Dgraph, shouldFail bool) { + // login with alice's account + ctx := context.Background() + if err := dg.Login(ctx, userid, userpassword); err != nil { + t.Fatalf("unable to login using the account %v", userid) } - response2, err := adminClient.LogIn(ctx, &loginRequest) - require.NoError(t, err) - if response2.Code != api.AclResponseCode_OK { - t.Errorf("Login with the correct password should result in the code %v", - api.AclResponseCode_OK) + + txn := dg.NewTxn() + query := fmt.Sprintf(` + { + q(func: eq(%s, "SF")) { + %s + } + }`, predicateToRead, queryAttr) + txn = dg.NewTxn() + _, err := txn.Query(ctx, query) + + if shouldFail { + require.Error(t, err, "the query should have failed") + } else { + require.NoError(t, err, "the query should have succeeded") } - jwt := acl.Jwt{} - jwt.DecodeString(response2.Context.Jwt, false, nil) - if jwt.Payload.Userid != userid { - t.Errorf("the jwt token should have the user id encoded") +} + +func mutatePredicateWithUserAccount(t *testing.T, dg *dgo.Dgraph, shouldFail bool) { + ctx := context.Background() + if err := dg.Login(ctx, userid, userpassword); err != nil { + t.Fatalf("unable to login using the account %v", userid) } - jwtTime := time.Unix(jwt.Payload.Exp, 0) - jwtValidDays := jwtTime.Sub(time.Now()).Round(time.Hour).Hours() / 24 - if jwtValidDays != 30.0 { - t.Errorf("The jwt token should be valid for 30 days, received %v days", jwtValidDays) + + txn := dg.NewTxn() + _, err := txn.Mutate(ctx, &api.Mutation{ + CommitNow: true, + SetNquads: []byte(fmt.Sprintf(`_:a <%s> "string" .`, predicateToWrite)), + }) + + if shouldFail { + require.Error(t, err, "the mutation should have failed") + } else { + require.NoError(t, err, "the mutation should have succeeded") } } -func loginWithWrongPassword(t *testing.T, ctx context.Context, - adminClient api.DgraphAccessClient) { - loginRequestWithWrongPassword := api.LogInRequest{ - Userid: userid, - Password: userpassword + "123", +func alterPredicateWithUserAccount(t *testing.T, dg *dgo.Dgraph, shouldFail bool) { + ctx := context.Background() + if err := dg.Login(ctx, userid, userpassword); err != nil { + t.Fatalf("unable to login using the account %v", userid) } - response, err := adminClient.LogIn(ctx, &loginRequestWithWrongPassword) - require.NoError(t, err) - if response.Code != api.AclResponseCode_UNAUTHENTICATED { - t.Errorf("Login with the wrong password should result in the code %v", api.AclResponseCode_UNAUTHENTICATED) + err := dg.Alter(ctx, &api.Operation{ + Schema: fmt.Sprintf(`%s: int .`, predicateToAlter), + }) + if shouldFail { + require.Error(t, err, "the alter should have failed") + } else { + require.NoError(t, err, "the alter should have succeeded") } } -*/ +func createAccountAndData(t *testing.T, dg *dgo.Dgraph) { + // use the admin account to clean the database + ctx := context.Background() + if err := dg.Login(ctx, "admin", "password"); err != nil { + t.Fatalf("unable to login using the admin account:%v", err) + } + op := api.Operation{ + DropAll: true, + } + if err := dg.Alter(ctx, &op); err != nil { + t.Fatalf("Unable to cleanup db:%v", err) + } + + resetUser(t) + + // create some data, e.g. user with name alice + require.NoError(t, dg.Alter(ctx, &api.Operation{ + Schema: fmt.Sprintf(`%s: string @index(exact) .`, predicateToRead), + })) + + txn := dg.NewTxn() + _, err := txn.Mutate(ctx, &api.Mutation{ + SetNquads: []byte(fmt.Sprintf("_:a <%s> \"SF\" .", predicateToRead)), + }) + require.NoError(t, err) + require.NoError(t, txn.Commit(ctx)) +} + +func createGroupAndAcls(t *testing.T) { + // create a new group + createGroupCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), + "acl", "groupadd", + "-d", dgraphEndpoint, + "-g", group, "--adminPassword", "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, "--adminPassword", "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 + addReadPermCmd1 := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), + "acl", "chmod", + "-d", dgraphEndpoint, + "-g", group, "-p", predicateToRead, "-P", strconv.Itoa(int(Read.Code)), "--adminPassword", + "password") + if err := addReadPermCmd1.Run(); err != nil { + t.Fatalf("Unable to add READ permission on %s to group %s:%v", + predicateToRead, group, err) + } + + // also add read permission to the attribute queryAttr, which is used inside the query block + addReadPermCmd2 := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), + "acl", "chmod", + "-d", dgraphEndpoint, + "-g", group, "-p", queryAttr, "-P", strconv.Itoa(int(Read.Code)), "--adminPassword", + "password") + if err := addReadPermCmd2.Run(); err != nil { + t.Fatalf("Unable to add READ permission on %s to group %s:%v", queryAttr, group, err) + } + + // add WRITE permission on the predicateToWrite + addWritePermCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), + "acl", "chmod", + "-d", dgraphEndpoint, + "-g", group, "-p", predicateToWrite, "-P", strconv.Itoa(int(Write.Code)), "--adminPassword", + "password") + if err := addWritePermCmd.Run(); err != nil { + t.Fatalf("Unable to add permission on %s to group %s:%v", predicateToWrite, group, err) + } + + // add MODIFY permission on the predicateToAlter + addModifyPermCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), + "acl", "chmod", + "-d", dgraphEndpoint, + "-g", group, "-p", predicateToAlter, "-P", strconv.Itoa(int(Modify.Code)), "--adminPassword", + "password") + if err := addModifyPermCmd.Run(); err != nil { + t.Fatalf("Unable to add permission on %s to group %s:%v", predicateToAlter, group, err) + } +} diff --git a/ee/acl/docker-compose.yml b/ee/acl/docker-compose.yml new file mode 100644 index 00000000000..76e482331ea --- /dev/null +++ b/ee/acl/docker-compose.yml @@ -0,0 +1,78 @@ +# Docker compose file for testing. Use it with: +# docker-compose up --force-recreate +# This would pick up dgraph binary from $GOPATH. + +version: "3.5" +services: + zero1: + image: dgraph/dgraph:latest + container_name: acl-dg0.1 + working_dir: /data/dg0.1 + ports: + - 5080:5080 + - 6080:6080 + command: /gobin/dgraph zero --my=zero1:5080 --replicas 1 --idx 1 --bindall --expose_trace --profile_mode block --block_rate 10 --logtostderr -v=2 + volumes: + - type: bind + source: $GOPATH/bin + target: /gobin + read_only: true + labels: + cluster: test + + dg1: + image: dgraph/dgraph:latest + container_name: acl-dg1 + working_dir: /data/dg1 + volumes: + - type: bind + source: $GOPATH/bin + target: /gobin + read_only: true + - type: bind + source: $GOPATH/src/github.com/dgraph-io/dgraph/ee/acl + target: /dgraph-acl + ports: + - 8180:8180 + - 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 30s --jaeger.collector http://jaeger:14268 + labels: + cluster: test + + dg2: + image: dgraph/dgraph:latest + container_name: acl-dg2 + working_dir: /data/dg2 + volumes: + - type: bind + source: $GOPATH/bin + target: /gobin + read_only: true + - type: bind + source: $GOPATH/src/github.com/dgraph-io/dgraph/ee/acl + target: /dgraph-acl + ports: + - 8182:8182 + - 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 30s --jaeger.collector http://jaeger:14268 + labels: + cluster: test + + jaeger: + image: jaegertracing/all-in-one:latest + container_name: jaeger + hostname: jaeger + ports: + - "5775:5775/udp" + - "6831:6831/udp" + - "6832:6832/udp" + - "5778:5778" + - "16686:16686" + - "14268:14268" + - "9411:9411" + environment: + - COLLECTOR_ZIPKIN_HTTP_PORT=9411 diff --git a/ee/acl/groups.go b/ee/acl/groups.go index dd7fa66e179..2ebffea497e 100644 --- a/ee/acl/groups.go +++ b/ee/acl/groups.go @@ -17,7 +17,6 @@ import ( "encoding/json" "fmt" "strings" - "time" "github.com/dgraph-io/dgo" "github.com/dgraph-io/dgo/protos/api" @@ -32,10 +31,13 @@ func groupAdd(conf *viper.Viper) error { return fmt.Errorf("The group id should not be empty") } - dc, close := getDgraphClient(conf) - defer close() - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + dc, cancel, err := getClientWithAdminCtx(conf) defer cancel() + if err != nil { + 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 { @@ -77,10 +79,13 @@ func groupDel(conf *viper.Viper) error { return fmt.Errorf("The group id should not be empty") } - dc, close := getDgraphClient(conf) - defer close() - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + dc, cancel, err := getClientWithAdminCtx(conf) defer cancel() + if err != nil { + 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 { @@ -156,10 +161,13 @@ func chMod(conf *viper.Viper) error { return fmt.Errorf("The predicate must not be empty") } - dc, close := getDgraphClient(conf) - defer close() - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + dc, cancel, err := getClientWithAdminCtx(conf) defer cancel() + if err != nil { + 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 { diff --git a/ee/acl/hmac-secret b/ee/acl/hmac-secret new file mode 100644 index 00000000000..2add0c574b7 --- /dev/null +++ b/ee/acl/hmac-secret @@ -0,0 +1 @@ +1234567890123456789012345678901 diff --git a/ee/acl/run_ee.go b/ee/acl/run_ee.go index 68de7e1caf4..d299dd7696f 100644 --- a/ee/acl/run_ee.go +++ b/ee/acl/run_ee.go @@ -78,6 +78,7 @@ func initSubcommands() []*x.SubCommand { }, } userAddFlags := cmdUserAdd.Cmd.Flags() + userAddFlags.String("adminPassword", "", "The admin password used to authorize this operation") userAddFlags.StringP("user", "u", "", "The user id to be created") userAddFlags.StringP("password", "p", "", "The password for the user") @@ -94,24 +95,9 @@ func initSubcommands() []*x.SubCommand { }, } userDelFlags := cmdUserDel.Cmd.Flags() + userDelFlags.String("adminPassword", "", "The admin password used to authorize this operation") userDelFlags.StringP("user", "u", "", "The user id to be deleted") - // login command - var cmdLogIn x.SubCommand - cmdLogIn.Cmd = &cobra.Command{ - Use: "login", - Short: "Login to dgraph in order to get a jwt token", - Run: func(cmd *cobra.Command, args []string) { - if err := userLogin(cmdLogIn.Conf); err != nil { - glog.Errorf("Unable to login:%v", err) - os.Exit(1) - } - }, - } - loginFlags := cmdLogIn.Cmd.Flags() - loginFlags.StringP("user", "u", "", "The user id to be created") - loginFlags.StringP("password", "p", "", "The password for the user") - // group creation command var cmdGroupAdd x.SubCommand cmdGroupAdd.Cmd = &cobra.Command{ @@ -125,6 +111,7 @@ func initSubcommands() []*x.SubCommand { }, } groupAddFlags := cmdGroupAdd.Cmd.Flags() + groupAddFlags.String("adminPassword", "", "The admin password used to authorize this operation") groupAddFlags.StringP("group", "g", "", "The group id to be created") // group deletion command @@ -140,6 +127,7 @@ func initSubcommands() []*x.SubCommand { }, } groupDelFlags := cmdGroupDel.Cmd.Flags() + groupDelFlags.String("adminPassword", "", "The admin password used to authorize this operation") groupDelFlags.StringP("group", "g", "", "The group id to be deleted") // the usermod command used to set a user's groups @@ -155,6 +143,7 @@ func initSubcommands() []*x.SubCommand { }, } userModFlags := cmdUserMod.Cmd.Flags() + userModFlags.String("adminPassword", "", "The admin password used to authorize this operation") userModFlags.StringP("user", "u", "", "The user id to be changed") userModFlags.StringP("groups", "g", "", "The groups to be set for the user") @@ -171,6 +160,7 @@ func initSubcommands() []*x.SubCommand { }, } chModFlags := cmdChMod.Cmd.Flags() + chModFlags.String("adminPassword", "", "The admin password used to authorize this operation") chModFlags.StringP("group", "g", "", "The group whose permission "+ "is to be changed") chModFlags.StringP("pred", "p", "", "The predicates whose acls"+ @@ -190,10 +180,11 @@ func initSubcommands() []*x.SubCommand { }, } infoFlags := cmdInfo.Cmd.Flags() + infoFlags.String("adminPassword", "", "The admin password used to authorize this operation") infoFlags.StringP("user", "u", "", "The user to be shown") infoFlags.StringP("group", "g", "", "The group to be shown") return []*x.SubCommand{ - &cmdUserAdd, &cmdUserDel, &cmdLogIn, &cmdGroupAdd, &cmdGroupDel, &cmdUserMod, + &cmdUserAdd, &cmdUserDel, &cmdGroupAdd, &cmdGroupDel, &cmdUserMod, &cmdChMod, &cmdInfo, } } diff --git a/ee/acl/users.go b/ee/acl/users.go index f83b9d396d5..88d43911df6 100644 --- a/ee/acl/users.go +++ b/ee/acl/users.go @@ -16,6 +16,7 @@ import ( "context" "fmt" "strings" + "syscall" "time" "github.com/dgraph-io/dgo" @@ -23,8 +24,36 @@ import ( "github.com/dgraph-io/dgraph/x" "github.com/golang/glog" "github.com/spf13/viper" + "golang.org/x/crypto/ssh/terminal" ) +func getClientWithAdminCtx(conf *viper.Viper) (*dgo.Dgraph, CloseFunc, error) { + adminPassword := conf.GetString("adminPassword") + if len(adminPassword) == 0 { + fmt.Print("Enter admin password:") + password, err := terminal.ReadPassword(int(syscall.Stdin)) + if err != nil { + return nil, func() {}, fmt.Errorf("error while reading password:%v", err) + } + adminPassword = string(password) + } + + dc, closeClient := getDgraphClient(conf) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + + cleanFunc := func() { + cancel() + closeClient() + } + + if err := dc.Login(ctx, "admin", adminPassword); err != nil { + return dc, cleanFunc, fmt.Errorf("unable to login with the admin account %v", err) + } + glog.Infof("login successfully with the admin account") + // update the context so that it has the admin jwt token + return dc, cleanFunc, nil +} + func userAdd(conf *viper.Viper) error { userid := conf.GetString("user") password := conf.GetString("password") @@ -36,10 +65,13 @@ func userAdd(conf *viper.Viper) error { return fmt.Errorf("The password must not be empty") } - dc, close := getDgraphClient(conf) - defer close() - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + dc, cancel, err := getClientWithAdminCtx(conf) defer cancel() + if err != nil { + 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 { @@ -87,10 +119,13 @@ func userDel(conf *viper.Viper) error { return fmt.Errorf("The user id should not be empty") } - dc, close := getDgraphClient(conf) - defer close() - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + dc, cancel, err := getClientWithAdminCtx(conf) defer cancel() + if err != nil { + 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 { @@ -127,37 +162,6 @@ func userDel(conf *viper.Viper) error { return nil } -func userLogin(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") - } - - dc, close := getDgraphClient(conf) - defer close() - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - txn := dc.NewTxn() - defer func() { - if err := txn.Discard(ctx); err != nil { - glog.Errorf("Unable to discard transaction:%v", err) - } - }() - - if err := dc.Login(ctx, userid, password); err != nil { - return fmt.Errorf("Unable to login:%v", err) - } - updatedContext := dc.GetContext(ctx) - glog.Infof("Login successfully.\naccess jwt:\n%v\nrefresh jwt:\n%v", - updatedContext.Value("accessJwt"), updatedContext.Value("refreshJwt")) - return nil -} - func queryUser(ctx context.Context, txn *dgo.Txn, userid string) (user *User, err error) { query := ` query search($userid: string){ @@ -192,10 +196,13 @@ func userMod(conf *viper.Viper) error { return fmt.Errorf("The user must not be empty") } - dc, close := getDgraphClient(conf) - defer close() - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + dc, cancel, err := getClientWithAdminCtx(conf) defer cancel() + if err != nil { + 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 { diff --git a/ee/acl/utils.go b/ee/acl/utils.go index 173ab28e673..0e4d506fce1 100644 --- a/ee/acl/utils.go +++ b/ee/acl/utils.go @@ -34,6 +34,26 @@ func GetGroupIDs(groups []Group) []string { return jwtGroups } +type Operation struct { + Code int32 + Name string +} + +var ( + Read = &Operation{ + Code: 4, + Name: "Read", + } + Write = &Operation{ + Code: 2, + Name: "Write", + } + Modify = &Operation{ + Code: 1, + Name: "Modify", + } +) + type User struct { Uid string `json:"uid"` UserID string `json:"dgraph.xid"` @@ -63,10 +83,11 @@ 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"` + 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 } // Extract the first User pointed by the userKey in the query response @@ -84,11 +105,40 @@ func UnmarshalGroup(input []byte, groupKey string) (group *Group, err error) { return nil, nil } if len(groups) > 1 { - return nil, x.Errorf("Found multiple groups: %s", input) + return nil, fmt.Errorf("found multiple groups: %s", input) } + 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) + + err = json.Unmarshal(input, &m) + if err != nil { + glog.Errorf("Unable to unmarshal the query group response:%v", err) + return nil, err + } + groups := m[groupKey] + return groups, nil +} + type JwtGroup struct { Group string } diff --git a/systest/bulk_live_cases_test.go b/systest/bulk_live_cases_test.go index 2b84e8124cd..0cd257473f9 100644 --- a/systest/bulk_live_cases_test.go +++ b/systest/bulk_live_cases_test.go @@ -20,8 +20,6 @@ import ( "os" "testing" "time" - - "github.com/dgraph-io/dgraph/x" ) // TODO: This test was used just to make sure some really basic examples work. @@ -285,8 +283,8 @@ func DONOTRUNTestGoldenData(t *testing.T) { err := matchExportCount(matchExport{ expectedRDF: 1120879, expectedSchema: 10, - dir: s.liveCluster.dir, - port: s.liveCluster.dgraphPortOffset + x.PortHTTP, + //dir: s.liveCluster.dir, + //port: s.liveCluster.dgraphPortOffset + x.PortHTTP, }) if err != nil { t.Fatal(err) diff --git a/systest/bulk_live_fixture_test.go b/systest/bulk_live_fixture_test.go index b7579142e28..7d09532fb75 100644 --- a/systest/bulk_live_fixture_test.go +++ b/systest/bulk_live_fixture_test.go @@ -30,6 +30,9 @@ import ( "testing" "time" + "github.com/dgraph-io/dgo/protos/api" + "github.com/dgraph-io/dgo/test" + "github.com/pkg/errors" ) @@ -45,12 +48,19 @@ var rootDir = filepath.Join(os.TempDir(), "dgraph_systest") type suite struct { t *testing.T - - liveCluster *DgraphCluster - bulkCluster *DgraphCluster } func newSuite(t *testing.T, schema, rdfs string) *suite { + dg, close := test.GetDgraphClient() + defer close() + + err := dg.Alter(context.Background(), &api.Operation{ + DropAll: true, + }) + if err != nil { + t.Fatalf("Could not drop old data: %v", err) + } + if testing.Short() { t.Skip("Skipping system test with long runtime.") } @@ -83,44 +93,31 @@ func (s *suite) setup(schemaFile, rdfFile string) { makeDirEmpty(liveDir), ) - s.bulkCluster = NewDgraphCluster(bulkDir) - s.checkFatal(s.bulkCluster.StartZeroOnly()) - bulkCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), "bulk", "-r", rdfFile, "-s", schemaFile, "--http", ":"+strconv.Itoa(freePort(0)), - "-z", ":"+s.bulkCluster.zeroPort, + //"-z", ":"+s.bulkCluster.zeroPort, "-j=1", "-x=true", ) - bulkCmd.Stdout = os.Stdout - bulkCmd.Stderr = os.Stdout bulkCmd.Dir = bulkDir if err := bulkCmd.Run(); err != nil { s.cleanup() s.t.Fatalf("Bulkloader didn't run: %v\n", err) } - s.bulkCluster.zero.Process.Kill() - s.bulkCluster.zero.Wait() + s.checkFatal(os.Rename( filepath.Join(bulkDir, "out", "0", "p"), filepath.Join(bulkDir, "p"), )) - s.liveCluster = NewDgraphCluster(liveDir) - s.checkFatal(s.liveCluster.Start()) - s.checkFatal(s.bulkCluster.Start()) - liveCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), "live", "--rdfs", rdfFile, "--schema", schemaFile, - "--dgraph", ":"+s.liveCluster.dgraphPort, - "--zero", ":"+s.liveCluster.zeroPort, + "--dgraph", ":9180", ) liveCmd.Dir = liveDir - liveCmd.Stdout = os.Stdout - liveCmd.Stderr = os.Stdout if err := liveCmd.Run(); err != nil { s.cleanup() s.t.Fatalf("Live Loader didn't run: %v\n", err) @@ -137,27 +134,22 @@ func makeDirEmpty(dir string) error { func (s *suite) cleanup() { // NOTE: Shouldn't raise any errors here or fail a test, since this is // called when we detect an error (don't want to mask the original problem). - if s.liveCluster != nil { - s.liveCluster.Close() - } - if s.bulkCluster != nil { - s.bulkCluster.Close() - } _ = os.RemoveAll(rootDir) } func (s *suite) testCase(query, wantResult string) func(*testing.T) { return func(t *testing.T) { - for _, cluster := range []*DgraphCluster{s.bulkCluster, s.liveCluster} { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - txn := cluster.client.NewTxn() - resp, err := txn.Query(ctx, query) - if err != nil { - t.Fatalf("Could not query: %v", err) - } - CompareJSON(t, wantResult, string(resp.GetJson())) + dg, close := test.GetDgraphClient() + defer close() + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + txn := dg.NewTxn() + resp, err := txn.Query(ctx, query) + if err != nil { + t.Fatalf("Could not query: %v", err) } + CompareJSON(t, wantResult, string(resp.GetJson())) } } diff --git a/systest/cluster_setup_test.go b/systest/cluster_setup_test.go index 2b2e849bffc..2feaed18925 100644 --- a/systest/cluster_setup_test.go +++ b/systest/cluster_setup_test.go @@ -64,8 +64,8 @@ func (d *DgraphCluster) StartZeroOnly() error { "--replicas", "3", ) d.zero.Dir = d.dir - d.zero.Stdout = os.Stdout - d.zero.Stderr = os.Stderr + //d.zero.Stdout = os.Stdout + //d.zero.Stderr = os.Stderr if err := d.zero.Start(); err != nil { return err @@ -89,8 +89,8 @@ func (d *DgraphCluster) Start() error { "--custom_tokenizers", d.TokenizerPluginsArg, ) d.dgraph.Dir = d.dir - d.dgraph.Stdout = os.Stdout - d.dgraph.Stderr = os.Stderr + //d.dgraph.Stdout = os.Stdout + //d.dgraph.Stderr = os.Stderr if err := d.dgraph.Start(); err != nil { return err } diff --git a/systest/cluster_test.go b/systest/cluster_test.go index 8be02422a90..a11cd24f7c3 100644 --- a/systest/cluster_test.go +++ b/systest/cluster_test.go @@ -180,8 +180,8 @@ func DONOTRUNTestClusterSnapshot(t *testing.T) { "--zero", ":"+cluster.zeroPort, ) liveCmd.Dir = tmpDir - liveCmd.Stdout = os.Stdout - liveCmd.Stderr = os.Stdout + //liveCmd.Stdout = os.Stdout + //liveCmd.Stderr = os.Stdout if err := liveCmd.Run(); err != nil { cluster.Close() t.Fatalf("Live Loader didn't run: %v\n", err) diff --git a/systest/loader_test.go b/systest/loader_test.go index 75e70b07b0d..5d27d2cff32 100644 --- a/systest/loader_test.go +++ b/systest/loader_test.go @@ -44,8 +44,8 @@ func TestLoaderXidmap(t *testing.T) { "-x", "x", ) liveCmd.Dir = tmpDir - liveCmd.Stdout = os.Stdout - liveCmd.Stderr = os.Stdout + //liveCmd.Stdout = os.Stdout + //liveCmd.Stderr = os.Stdout if err := liveCmd.Run(); err != nil { cluster.Close() t.Fatalf("Live Loader didn't run: %v\n", err) @@ -114,6 +114,6 @@ func TestLoaderXidmap(t *testing.T) { if string(out) != expected { cluster.Close() - t.Fatalf("Export is not as expected.") + t.Fatalf("Export is not as expected. Want:%v\nGot:%v\n", expected, string(out)) } } diff --git a/systest/queries_test.go b/systest/queries_test.go index 264cd4897c4..5f33358e4d2 100644 --- a/systest/queries_test.go +++ b/systest/queries_test.go @@ -335,28 +335,7 @@ func SchemaQueryTest(t *testing.T, c *dgo.Dgraph) { "type": "string", "list": true }, - { - "predicate": "dgraph.group.acl", - "type": "string" - }, - { - "predicate": "dgraph.password", - "type": "password" - }, - { - "predicate": "dgraph.user.group", - "type": "uid", - "reverse": true, - "list": true - }, - { - "predicate": "dgraph.xid", - "type": "string", - "index": true, - "tokenizer": [ - "exact" - ] - }, +` + x.AclPredsJson + `, { "predicate": "name", "type": "string", @@ -541,28 +520,7 @@ func SchemaQueryTestHTTP(t *testing.T, c *dgo.Dgraph) { "type": "string", "list": true }, - { - "predicate": "dgraph.group.acl", - "type": "string" - }, - { - "predicate": "dgraph.password", - "type": "password" - }, - { - "predicate": "dgraph.user.group", - "type": "uid", - "reverse": true, - "list": true - }, - { - "predicate": "dgraph.xid", - "type": "string", - "index": true, - "tokenizer": [ - "exact" - ] - }, +` + x.AclPredsJson + `, { "predicate": "name", "type": "string", diff --git a/worker/groups.go b/worker/groups.go index 787f59b68b8..b98ed2682be 100644 --- a/worker/groups.go +++ b/worker/groups.go @@ -158,6 +158,7 @@ func (g *groupi) proposeInitialSchema() { Predicate: "dgraph.xid", ValueType: pb.Posting_STRING, Directive: pb.SchemaUpdate_INDEX, + Upsert: true, Tokenizer: []string{"exact"}, }) diff --git a/x/x.go b/x/x.go index c01fd24aa34..408302102db 100644 --- a/x/x.go +++ b/x/x.go @@ -89,7 +89,7 @@ var ( {"predicate":"dgraph.group.acl", "type":"string"}, {"predicate":"dgraph.password", "type":"password"}, {"reverse":true, "predicate":"dgraph.user.group", "type":"uid", "list":true}, -{"index":true, "tokenizer":["exact"], "predicate":"dgraph.xid", "type":"string"} +{"index":true, "tokenizer":["exact"], "predicate":"dgraph.xid", "type":"string", "upsert":true} ` Nilbyte []byte ) From 19f1f29dcf993d6087889d5b74adafc6cf66a967 Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Mon, 14 Jan 2019 14:11:43 -0800 Subject: [PATCH 02/15] checking length after error --- edgraph/access_ee.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/edgraph/access_ee.go b/edgraph/access_ee.go index ff450f2ef7e..c5c18aebffe 100644 --- a/edgraph/access_ee.go +++ b/edgraph/access_ee.go @@ -100,11 +100,11 @@ func (s *Server) authenticateLogin(ctx context.Context, request *api.LoginReques var user *acl.User if len(request.RefreshToken) > 0 { userData, err := validateToken(request.RefreshToken) - x.AssertTrue(len(userData) > 0) if err != nil { - return nil, fmt.Errorf("Unable to authenticate the refresh token %v: %v", + return nil, fmt.Errorf("unable to authenticate the refresh token %v: %v", request.RefreshToken, err) } + x.AssertTrue(len(userData) > 0) userId := userData[0] user, err = authorizeUser(ctx, userId, "") @@ -440,10 +440,10 @@ func authorizeAlter(ctx context.Context, op *api.Operation) error { } userData, err := extractUserAndGroups(ctx) - x.AssertTrue(len(userData) > 0) if err != nil { return status.Error(codes.Unauthenticated, err.Error()) } + x.AssertTrue(len(userData) > 0) userId := userData[0] if userId == "admin" { // admin is allowed to do anything @@ -492,11 +492,10 @@ func authorizeMutation(ctx context.Context, mu *api.Mutation) error { } userData, err := extractUserAndGroups(ctx) - x.AssertTrue(len(userData) > 0) if err != nil { return status.Error(codes.Unauthenticated, err.Error()) } - + x.AssertTrue(len(userData) > 0) userId := userData[0] if userId == "admin" { // the admin account has access to everything @@ -544,11 +543,10 @@ func authorizeQuery(ctx context.Context, req *api.Request) error { } userData, err := extractUserAndGroups(ctx) - x.AssertTrue(len(userData) > 0) if err != nil { return status.Error(codes.Unauthenticated, err.Error()) } - + x.AssertTrue(len(userData) > 0) userId := userData[0] if userId == "admin" { // the admin account has access to everything From d00223e6ec900c4a17cf45ae27135e33ad8f556d Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Mon, 14 Jan 2019 14:13:33 -0800 Subject: [PATCH 03/15] removed jaeger from the ee/acl/docker-compose --- ee/acl/docker-compose.yml | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/ee/acl/docker-compose.yml b/ee/acl/docker-compose.yml index 76e482331ea..dcd86409a4b 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 30s --jaeger.collector http://jaeger:14268 + 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 30s labels: cluster: test @@ -58,21 +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 30s --jaeger.collector http://jaeger:14268 + 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 30s labels: cluster: test - - jaeger: - image: jaegertracing/all-in-one:latest - container_name: jaeger - hostname: jaeger - ports: - - "5775:5775/udp" - - "6831:6831/udp" - - "6832:6832/udp" - - "5778:5778" - - "16686:16686" - - "14268:14268" - - "9411:9411" - environment: - - COLLECTOR_ZIPKIN_HTTP_PORT=9411 From e630e99ba95408c8d91e3ff65ebc02d34cc782b1 Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Mon, 14 Jan 2019 14:55:25 -0800 Subject: [PATCH 04/15] cleaned up the pr --- contrib/scripts/functions.sh | 5 +- edgraph/access_ee.go | 109 +++++++++++++++--------------- edgraph/server.go | 3 +- systest/bulk_live_cases_test.go | 2 - systest/bulk_live_fixture_test.go | 1 - systest/cluster_setup_test.go | 2 - systest/cluster_test.go | 2 - systest/loader_test.go | 2 - 8 files changed, 56 insertions(+), 70 deletions(-) diff --git a/contrib/scripts/functions.sh b/contrib/scripts/functions.sh index 2283f342ba2..177c9612a98 100755 --- a/contrib/scripts/functions.sh +++ b/contrib/scripts/functions.sh @@ -14,9 +14,8 @@ function restartCluster { basedir=$GOPATH/src/github.com/dgraph-io/dgraph pushd $basedir/dgraph >/dev/null go build . && go install . && md5sum dgraph $GOPATH/bin/dgraph - docker ps --filter label="cluster=test" --format "{{.Names}}" \ - | xargs -r docker stop | sed 's/^/Stopped /' - docker-compose -f $compose_file -p dgraph up --force-recreate --remove-orphans --detach + docker ps --filter label="cluster=test" --format "{{.Names}}" | xargs -r docker rm -f + docker-compose -f $compose_file up --force-recreate --remove-orphans --detach popd >/dev/null $basedir/contrib/wait-for-it.sh -t 60 localhost:6080 || exit 1 diff --git a/edgraph/access_ee.go b/edgraph/access_ee.go index c5c18aebffe..462c3a9843a 100644 --- a/edgraph/access_ee.go +++ b/edgraph/access_ee.go @@ -277,62 +277,6 @@ func authorizeUser(ctx context.Context, userid string, password string) (user *a return user, nil } -func upsertAdmin(ctx context.Context) error { - // upsert the admin account - - queryVars := map[string]string{ - "$userid": "admin", - "$password": "", - } - queryRequest := api.Request{ - Query: queryUser, - Vars: queryVars, - } - - queryResp, err := (&Server{}).doQuery(ctx, &queryRequest) - if err != nil { - return fmt.Errorf("error while query user with id admin: %v", err) - } - startTs := queryResp.GetTxn().StartTs - glog.Infof("admin txn startTs:%v", startTs) - - adminUser, err := acl.UnmarshalUser(queryResp, "user") - if err != nil { - return fmt.Errorf("error while unmarshaling the admin user: %v", err) - } - - if adminUser != nil { - // the admin user already exists, no need to create - return nil - } - - // insert the admin user - createUserNQuads := []*api.NQuad{ - { - Subject: "_:newuser", - Predicate: "dgraph.xid", - ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: "admin"}}, - }, - { - Subject: "_:newuser", - Predicate: "dgraph.password", - ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: "password"}}, - }} - - mu := &api.Mutation{ - StartTs: startTs, - CommitNow: true, - Set: createUserNQuads, - } - - assigned, err := (&Server{}).doMutate(context.Background(), mu) - if err != nil { - return fmt.Errorf("unable to create admin: %v", err) - } - glog.Infof("admin commitTs:%v", assigned.GetContext().GetCommitTs()) - return nil -} - func RefreshAcls(closeCh <-chan struct{}) { if len(Config.HmacSecret) == 0 { // the acl feature is not turned on @@ -408,6 +352,59 @@ func ResetAcl() { return } + upsertAdmin := func(ctx context.Context) error { + // upsert the admin account + + queryVars := map[string]string{ + "$userid": "admin", + "$password": "", + } + queryRequest := api.Request{ + Query: queryUser, + Vars: queryVars, + } + + queryResp, err := (&Server{}).doQuery(ctx, &queryRequest) + if err != nil { + return fmt.Errorf("error while query user with id admin: %v", err) + } + startTs := queryResp.GetTxn().StartTs + + adminUser, err := acl.UnmarshalUser(queryResp, "user") + if err != nil { + return fmt.Errorf("error while unmarshaling the admin user: %v", err) + } + + if adminUser != nil { + // the admin user already exists, no need to create + return nil + } + + // insert the admin user + createUserNQuads := []*api.NQuad{ + { + Subject: "_:newuser", + Predicate: "dgraph.xid", + ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: "admin"}}, + }, + { + Subject: "_:newuser", + Predicate: "dgraph.password", + ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: "password"}}, + }} + + mu := &api.Mutation{ + StartTs: startTs, + CommitNow: true, + Set: createUserNQuads, + } + + if _, err := (&Server{}).doMutate(context.Background(), mu); err != nil { + return fmt.Errorf("unable to create admin: %v", err) + } + return nil + } + aclCache = sync.Map{} if err := upsertAdmin(context.Background()); err != nil { glog.Infof("Unable to upsert the admin account:%v", err) diff --git a/edgraph/server.go b/edgraph/server.go index b5918e3cad4..8280f4844ce 100644 --- a/edgraph/server.go +++ b/edgraph/server.go @@ -291,8 +291,7 @@ func (s *Server) Alter(ctx context.Context, op *api.Operation) (*api.Payload, er return nil, err } - err := authorizeAlter(ctx, op) - if err != nil { + if err := authorizeAlter(ctx, op); err != nil { glog.Warningf("Alter denied with error: %v\n", err) return nil, err } diff --git a/systest/bulk_live_cases_test.go b/systest/bulk_live_cases_test.go index 0cd257473f9..d2de1b83509 100644 --- a/systest/bulk_live_cases_test.go +++ b/systest/bulk_live_cases_test.go @@ -283,8 +283,6 @@ func DONOTRUNTestGoldenData(t *testing.T) { err := matchExportCount(matchExport{ expectedRDF: 1120879, expectedSchema: 10, - //dir: s.liveCluster.dir, - //port: s.liveCluster.dgraphPortOffset + x.PortHTTP, }) if err != nil { t.Fatal(err) diff --git a/systest/bulk_live_fixture_test.go b/systest/bulk_live_fixture_test.go index 7d09532fb75..df77552b07f 100644 --- a/systest/bulk_live_fixture_test.go +++ b/systest/bulk_live_fixture_test.go @@ -97,7 +97,6 @@ func (s *suite) setup(schemaFile, rdfFile string) { "-r", rdfFile, "-s", schemaFile, "--http", ":"+strconv.Itoa(freePort(0)), - //"-z", ":"+s.bulkCluster.zeroPort, "-j=1", "-x=true", ) diff --git a/systest/cluster_setup_test.go b/systest/cluster_setup_test.go index 2feaed18925..5ddb077d1bf 100644 --- a/systest/cluster_setup_test.go +++ b/systest/cluster_setup_test.go @@ -89,8 +89,6 @@ func (d *DgraphCluster) Start() error { "--custom_tokenizers", d.TokenizerPluginsArg, ) d.dgraph.Dir = d.dir - //d.dgraph.Stdout = os.Stdout - //d.dgraph.Stderr = os.Stderr if err := d.dgraph.Start(); err != nil { return err } diff --git a/systest/cluster_test.go b/systest/cluster_test.go index a11cd24f7c3..0dc0638c909 100644 --- a/systest/cluster_test.go +++ b/systest/cluster_test.go @@ -180,8 +180,6 @@ func DONOTRUNTestClusterSnapshot(t *testing.T) { "--zero", ":"+cluster.zeroPort, ) liveCmd.Dir = tmpDir - //liveCmd.Stdout = os.Stdout - //liveCmd.Stderr = os.Stdout if err := liveCmd.Run(); err != nil { cluster.Close() t.Fatalf("Live Loader didn't run: %v\n", err) diff --git a/systest/loader_test.go b/systest/loader_test.go index 5d27d2cff32..c0b91b8f21a 100644 --- a/systest/loader_test.go +++ b/systest/loader_test.go @@ -44,8 +44,6 @@ func TestLoaderXidmap(t *testing.T) { "-x", "x", ) liveCmd.Dir = tmpDir - //liveCmd.Stdout = os.Stdout - //liveCmd.Stderr = os.Stdout if err := liveCmd.Run(); err != nil { cluster.Close() t.Fatalf("Live Loader didn't run: %v\n", err) From 47b996e909d368525bd5c87df67ef18139d6dd99 Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Mon, 14 Jan 2019 15:43:57 -0800 Subject: [PATCH 05/15] polishing the pr --- edgraph/access_ee.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/edgraph/access_ee.go b/edgraph/access_ee.go index 462c3a9843a..e68a78a74d2 100644 --- a/edgraph/access_ee.go +++ b/edgraph/access_ee.go @@ -88,9 +88,9 @@ func (s *Server) Login(ctx context.Context, return resp, nil } -// Authenticate the login request using either the refresh token if present, or the -// pair. If authentication passes, query the user's uid and associated groups -// from DB and returns the user object +// authenticateLogin authenticates the login request using either the refresh token if present, or +// the pair. If authentication passes, it queries the user's uid and associated +// groups from DB and returns the user object func (s *Server) authenticateLogin(ctx context.Context, request *api.LoginRequest) (*acl.User, error) { if err := validateLoginRequest(request); err != nil { @@ -138,9 +138,10 @@ func (s *Server) authenticateLogin(ctx context.Context, request *api.LoginReques return user, nil } -// verify signature and expiration of the jwt and if validation passes, -// return the extracted userId, and groupIds encoded in the jwt -func validateToken(jwtStr string) (userData []string, err error) { +// validateToken verifies the signature and expiration of the jwt, and if validation passes, +// returns a slice of strings, where the first element is the extracted userId +// and the rest are groupIds encoded in the jwt. +func validateToken(jwtStr string) ([]string, error) { token, err := jwt.Parse(jwtStr, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) @@ -254,8 +255,8 @@ const queryUser = ` // query the user with the given userid, and returns associated uid, acl groups, // and whether the password stored in DB matches the supplied password -func authorizeUser(ctx context.Context, userid string, password string) (user *acl.User, - err error) { +func authorizeUser(ctx context.Context, userid string, password string) (*acl.User, + error) { queryVars := map[string]string{ "$userid": userid, "$password": password, @@ -270,7 +271,7 @@ func authorizeUser(ctx context.Context, userid string, password string) (user *a glog.Errorf("Error while query user with id %s: %v", userid, err) return nil, err } - user, err = acl.UnmarshalUser(queryResp, "user") + user, err := acl.UnmarshalUser(queryResp, "user") if err != nil { return nil, err } From 828f10c60f43c830ef096b3f1a4819f074c745c4 Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Mon, 14 Jan 2019 18:19:02 -0800 Subject: [PATCH 06/15] polishing pr --- dgraph/cmd/alpha/run.go | 6 +++- edgraph/access_ee.go | 75 +++++++++++++++++++++-------------------- edgraph/server.go | 9 +++-- ee/acl/acl_test.go | 7 ++-- ee/acl/utils.go | 6 ++-- 5 files changed, 53 insertions(+), 50 deletions(-) diff --git a/dgraph/cmd/alpha/run.go b/dgraph/cmd/alpha/run.go index e4716287804..a2576b8b8d9 100644 --- a/dgraph/cmd/alpha/run.go +++ b/dgraph/cmd/alpha/run.go @@ -33,6 +33,8 @@ import ( "syscall" "time" + "github.com/dgraph-io/badger/y" + "github.com/dgraph-io/dgo/protos/api" "github.com/dgraph-io/dgraph/edgraph" "github.com/dgraph-io/dgraph/posting" @@ -530,16 +532,18 @@ func run() { _ = numShutDownSig // Setup external communication. + aclCloser := y.NewCloser(1) go func() { worker.StartRaftNodes(edgraph.State.WALstore, bindall) // initialization of the admin account can only be done after raft nodes are running // and health check passes edgraph.ResetAcl() - edgraph.RefreshAcls(shutdownCh) + edgraph.RefreshAcls(aclCloser) }() setupServer() glog.Infoln("GRPC and HTTP stopped.") + aclCloser.SignalAndWait() worker.BlockingStop() glog.Infoln("Server shutdown. Bye!") } diff --git a/edgraph/access_ee.go b/edgraph/access_ee.go index e68a78a74d2..228065a7739 100644 --- a/edgraph/access_ee.go +++ b/edgraph/access_ee.go @@ -20,6 +20,8 @@ import ( "sync" "time" + "github.com/dgraph-io/badger/y" + "github.com/dgraph-io/dgo/protos/api" "github.com/dgraph-io/dgraph/ee/acl" "github.com/dgraph-io/dgraph/gql" @@ -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) } @@ -59,14 +61,14 @@ func (s *Server) Login(ctx context.Context, resp := &api.Response{} accessJwt, err := getAccessJwt(request.Userid, user.Groups) if err != nil { - errMsg := fmt.Sprintf("Unable to get access jwt (userid=%s,addr=%s):%v", + errMsg := fmt.Sprintf("unable to get access jwt (userid=%s,addr=%s):%v", request.Userid, addr, err) glog.Errorf(errMsg) return nil, fmt.Errorf(errMsg) } refreshJwt, err := getRefreshJwt(request.Userid) if err != nil { - errMsg := fmt.Sprintf("Unable to get refresh jwt (userid=%s,addr=%s):%v", + errMsg := fmt.Sprintf("unable to get refresh jwt (userid=%s,addr=%s):%v", request.Userid, addr, err) glog.Errorf(errMsg) return nil, fmt.Errorf(errMsg) @@ -79,7 +81,7 @@ func (s *Server) Login(ctx context.Context, jwtBytes, err := loginJwt.Marshal() if err != nil { - errMsg := fmt.Sprintf("Unable to marshal jwt (userid=%s,addr=%s):%v", + errMsg := fmt.Sprintf("unable to marshal jwt (userid=%s,addr=%s):%v", request.Userid, addr, err) glog.Errorf(errMsg) return nil, fmt.Errorf(errMsg) @@ -94,7 +96,7 @@ func (s *Server) Login(ctx context.Context, func (s *Server) authenticateLogin(ctx context.Context, request *api.LoginRequest) (*acl.User, error) { if err := validateLoginRequest(request); err != nil { - return nil, fmt.Errorf("Invalid login request: %v", err) + return nil, fmt.Errorf("invalid login request: %v", err) } var user *acl.User @@ -144,7 +146,7 @@ func (s *Server) authenticateLogin(ctx context.Context, request *api.LoginReques func validateToken(jwtStr string) ([]string, error) { token, err := jwt.Parse(jwtStr, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return Config.HmacSecret, nil }) @@ -186,10 +188,11 @@ func validateToken(jwtStr string) ([]string, error) { return append([]string{userId}, groupIds...), nil } -// validate that the login request has either the refresh token or the pair +// validateLoginRequest validates that the login request has either the refresh token or the +// pair func validateLoginRequest(request *api.LoginRequest) error { if request == nil { - return fmt.Errorf("The request should not be nil") + return fmt.Errorf("the request should not be nil") } // we will use the refresh token for authentication if it's set if len(request.RefreshToken) > 0 { @@ -198,16 +201,16 @@ func validateLoginRequest(request *api.LoginRequest) error { // otherwise make sure both userid and password are set if len(request.Userid) == 0 { - return fmt.Errorf("The userid should not be empty") + return fmt.Errorf("the userid should not be empty") } if len(request.Password) == 0 { - return fmt.Errorf("The password should not be empty") + return fmt.Errorf("the password should not be empty") } return nil } -// construct an access jwt with the given userid, groupIds, and expiration ttl specified by -// Config.AccessJwtTtl +// getAccessJwt constructs an access jwt with the given user id, groupIds, +// and expiration TTL specified by Config.AccessJwtTtl func getAccessJwt(userId string, groups []acl.Group) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "userid": userId, @@ -219,12 +222,12 @@ func getAccessJwt(userId string, groups []acl.Group) (string, error) { jwtString, err := token.SignedString(Config.HmacSecret) if err != nil { - return "", fmt.Errorf("Unable to encode jwt to string: %v", err) + return "", fmt.Errorf("unable to encode jwt to string: %v", err) } return jwtString, nil } -// construct a refresh jwt with the given userid, and expiration ttl specified by +// getRefreshJwt constructs a refresh jwt with the given user id, and expiration ttl specified by // Config.RefreshJwtTtl func getRefreshJwt(userId string) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ @@ -236,7 +239,7 @@ func getRefreshJwt(userId string) (string, error) { jwtString, err := token.SignedString(Config.HmacSecret) if err != nil { - return "", fmt.Errorf("Unable to encode jwt to string: %v", err) + return "", fmt.Errorf("unable to encode jwt to string: %v", err) } return jwtString, nil } @@ -253,8 +256,8 @@ const queryUser = ` } }` -// query the user with the given userid, and returns associated uid, acl groups, -// and whether the password stored in DB matches the supplied password +// authorizeUser queries the user with the given user id, and returns the associated uid, +// acl groups, and whether the password stored in DB matches the supplied password func authorizeUser(ctx context.Context, userid string, password string) (*acl.User, error) { queryVars := map[string]string{ @@ -278,7 +281,8 @@ func authorizeUser(ctx context.Context, userid string, password string) (*acl.Us return user, nil } -func RefreshAcls(closeCh <-chan struct{}) { +func RefreshAcls(closer *y.Closer) { + defer closer.Done() if len(Config.HmacSecret) == 0 { // the acl feature is not turned on return @@ -324,7 +328,7 @@ func RefreshAcls(closeCh <-chan struct{}) { for { select { - case <-closeCh: + case <-closer.HasBeenClosed(): return case <-ticker.C: if err := retrieveAcls(); err != nil { @@ -423,14 +427,13 @@ func extractUserAndGroups(ctx context.Context) ([]string, error) { } accessJwt := md.Get("accessJwt") if len(accessJwt) == 0 { - //glog.Infof("no accessJwt available, type is %v", reflect.TypeOf(ctx.Value("accessJwt"))) return nil, fmt.Errorf("no accessJwt available") } return validateToken(accessJwt[0]) } -// parse the Schema in the operation and authorize the operation using the aclCache +//authorizeAlter parses the Schema in the operation and authorizes the operation using the aclCache func authorizeAlter(ctx context.Context, op *api.Operation) error { if len(Config.HmacSecret) == 0 { // the user has not turned on the acl feature @@ -442,9 +445,7 @@ func authorizeAlter(ctx context.Context, op *api.Operation) error { return status.Error(codes.Unauthenticated, err.Error()) } x.AssertTrue(len(userData) > 0) - userId := userData[0] - if userId == "admin" { - // admin is allowed to do anything + if isAdmin(userData) { return nil } @@ -474,6 +475,7 @@ func authorizeAlter(ctx context.Context, op *api.Operation) error { return nil } +// parsePredsFromMutation returns a union set of all the predicate names in the input nquads func parsePredsFromMutation(nquads []*api.NQuad) map[string]struct{} { preds := make(map[string]struct{}) for _, nquad := range nquads { @@ -482,7 +484,7 @@ func parsePredsFromMutation(nquads []*api.NQuad) map[string]struct{} { return preds } -// authorize the mutation using the aclCache +// authorizeMutation authorizes the mutation using the aclCache func authorizeMutation(ctx context.Context, mu *api.Mutation) error { if len(Config.HmacSecret) == 0 { // the user has not turned on the acl feature @@ -494,8 +496,7 @@ func authorizeMutation(ctx context.Context, mu *api.Mutation) error { return status.Error(codes.Unauthenticated, err.Error()) } x.AssertTrue(len(userData) > 0) - userId := userData[0] - if userId == "admin" { + if isAdmin(userData) { // the admin account has access to everything return nil } @@ -533,7 +534,15 @@ func parsePredsFromQuery(gqls []*gql.GraphQuery) map[string]struct{} { return preds } -// authorize the query using the aclCache +func isAdmin(userData []string) bool { + if len(userData) == 0 { + return false + } + + return userData[0] == "admin" +} + +//authorizeQuery authorizes the query using the aclCache func authorizeQuery(ctx context.Context, req *api.Request) error { if len(Config.HmacSecret) == 0 { // the user has not turned on the acl feature @@ -544,12 +553,6 @@ func authorizeQuery(ctx context.Context, req *api.Request) error { if err != nil { return status.Error(codes.Unauthenticated, err.Error()) } - x.AssertTrue(len(userData) > 0) - userId := userData[0] - if userId == "admin" { - // the admin account has access to everything - return nil - } parsedReq, err := gql.Parse(gql.Request{ Str: req.Query, @@ -588,7 +591,7 @@ func hasAccess(groupId string, predicate string, operation *acl.Operation) error aclGroup := entry.(*acl.Group) perm, found := aclGroup.MappedAcls[predicate] allowed := found && (perm&operation.Code) != 0 - glog.Infof("authorizing group %v on predicate %v for %s, allowed %v", groupId, + 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", diff --git a/edgraph/server.go b/edgraph/server.go index 8280f4844ce..b5cd83d6db8 100644 --- a/edgraph/server.go +++ b/edgraph/server.go @@ -467,8 +467,7 @@ func (s *Server) Query(ctx context.Context, req *api.Request) (*api.Response, er // This method is used to execute the query and return the response to the // client as a protocol buffer message. -func (s *Server) doQuery(ctx context.Context, req *api.Request) (resp *api.Response, - err error) { +func (s *Server) doQuery(ctx context.Context, req *api.Request) (*api.Response, error) { if glog.V(3) { glog.Infof("Got a query: %+v", req) } @@ -476,17 +475,17 @@ func (s *Server) doQuery(ctx context.Context, req *api.Request) (resp *api.Respo defer span.End() if err := x.HealthCheck(); err != nil { - return resp, err + return nil, err } x.PendingQueries.Add(1) x.NumQueries.Add(1) defer x.PendingQueries.Add(-1) if ctx.Err() != nil { - return resp, ctx.Err() + return nil, ctx.Err() } - resp = new(api.Response) + resp := new(api.Response) if len(req.Query) == 0 { span.Annotate(nil, "Empty query") return resp, fmt.Errorf("Empty query") diff --git a/ee/acl/acl_test.go b/ee/acl/acl_test.go index ad914d070a6..0344fb14289 100644 --- a/ee/acl/acl_test.go +++ b/ee/acl/acl_test.go @@ -195,14 +195,13 @@ func createAccountAndData(t *testing.T, dg *dgo.Dgraph) { if err := dg.Alter(ctx, &op); err != nil { t.Fatalf("Unable to cleanup db:%v", err) } - - resetUser(t) - - // create some data, e.g. user with name alice require.NoError(t, dg.Alter(ctx, &api.Operation{ Schema: fmt.Sprintf(`%s: string @index(exact) .`, predicateToRead), })) + // create some data, e.g. user with name alice + resetUser(t) + txn := dg.NewTxn() _, err := txn.Mutate(ctx, &api.Mutation{ SetNquads: []byte(fmt.Sprintf("_:a <%s> \"SF\" .", predicateToRead)), diff --git a/ee/acl/utils.go b/ee/acl/utils.go index 0e4d506fce1..1ebe7786f6d 100644 --- a/ee/acl/utils.go +++ b/ee/acl/utils.go @@ -94,8 +94,7 @@ type Group struct { func UnmarshalGroup(input []byte, groupKey string) (group *Group, err error) { m := make(map[string][]Group) - err = json.Unmarshal(input, &m) - if err != nil { + if err = json.Unmarshal(input, &m); err != nil { glog.Errorf("Unable to unmarshal the query group response:%v", err) return nil, err } @@ -130,8 +129,7 @@ func UnmarshalAcl(aclBytes []byte) (map[string]int32, error) { func UnmarshalGroups(input []byte, groupKey string) (group []Group, err error) { m := make(map[string][]Group) - err = json.Unmarshal(input, &m) - if err != nil { + if err = json.Unmarshal(input, &m); err != nil { glog.Errorf("Unable to unmarshal the query group response:%v", err) return nil, err } From dee4b8031aca9b20b2244c8ca1a7d2a2834c05bd Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Mon, 14 Jan 2019 18:24:39 -0800 Subject: [PATCH 07/15] fixed issue for oss --- edgraph/access.go | 3 ++- ee/acl/utils.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/edgraph/access.go b/edgraph/access.go index 81f9212e873..c99d4720592 100644 --- a/edgraph/access.go +++ b/edgraph/access.go @@ -21,6 +21,7 @@ package edgraph import ( "context" + "github.com/dgraph-io/badger/y" "github.com/dgraph-io/dgo/protos/api" "github.com/dgraph-io/dgraph/x" "github.com/golang/glog" @@ -37,7 +38,7 @@ func ResetAcl() { // do nothing } -func RefreshAcls(closeCh <-chan struct{}) { +func RefreshAcls(closer *y.Closer) { // do nothing } diff --git a/ee/acl/utils.go b/ee/acl/utils.go index 1ebe7786f6d..5821b012e76 100644 --- a/ee/acl/utils.go +++ b/ee/acl/utils.go @@ -68,7 +68,7 @@ func UnmarshalUser(resp *api.Response, userKey string) (user *User, err error) { err = json.Unmarshal(resp.GetJson(), &m) if err != nil { - return nil, fmt.Errorf("Unable to unmarshal the query user response for user:%v", err) + return nil, fmt.Errorf("unable to unmarshal the query user response:%v", err) } users := m[userKey] if len(users) == 0 { From 8affabfdd62cdf4dd747a443b1620fdad280035a Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Tue, 15 Jan 2019 11:56:52 -0800 Subject: [PATCH 08/15] allowing admin to perform any queries --- edgraph/access_ee.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/edgraph/access_ee.go b/edgraph/access_ee.go index 228065a7739..3e8cb80543a 100644 --- a/edgraph/access_ee.go +++ b/edgraph/access_ee.go @@ -553,6 +553,10 @@ func authorizeQuery(ctx context.Context, req *api.Request) error { if err != nil { return status.Error(codes.Unauthenticated, err.Error()) } + x.AssertTrue(len(userData) > 0) + if isAdmin(userData) { + return nil + } parsedReq, err := gql.Parse(gql.Request{ Str: req.Query, From e4ba07a136339dd18f96d182b833d42414ed5ec4 Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Tue, 15 Jan 2019 11:58:16 -0800 Subject: [PATCH 09/15] fixed typo --- types/password.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/password.go b/types/password.go index 526bc4802f5..f40902978fd 100644 --- a/types/password.go +++ b/types/password.go @@ -28,7 +28,7 @@ const ( func Encrypt(plain string) (string, error) { if len(plain) < pwdLenLimit { - return "", x.Errorf("Password too short, i.e. should has at least 6 chars") + return "", x.Errorf("Password too short, i.e. should have at least 6 chars") } encrypted, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost) From 5e81c7f96c43ada5880e8dcc76e6577035e9e948 Mon Sep 17 00:00:00 2001 From: Manish R Jain Date: Wed, 16 Jan 2019 11:59:28 -0800 Subject: [PATCH 10/15] Manish's Review --- edgraph/access.go | 2 ++ edgraph/access_ee.go | 69 +++++++++++++++++++++----------------------- ee/acl/acl_test.go | 26 ++++++++--------- ee/acl/run_ee.go | 19 +++++------- ee/acl/users.go | 10 +++---- x/x.go | 2 ++ 6 files changed, 63 insertions(+), 65 deletions(-) diff --git a/edgraph/access.go b/edgraph/access.go index c99d4720592..b5317704f8b 100644 --- a/edgraph/access.go +++ b/edgraph/access.go @@ -40,6 +40,8 @@ func ResetAcl() { func RefreshAcls(closer *y.Closer) { // do nothing + <-closer.HasBeenClosed() + closer.Done() } func authorizeAlter(ctx context.Context, op *api.Operation) error { diff --git a/edgraph/access_ee.go b/edgraph/access_ee.go index 3e8cb80543a..0171503649c 100644 --- a/edgraph/access_ee.go +++ b/edgraph/access_ee.go @@ -106,7 +106,6 @@ func (s *Server) authenticateLogin(ctx context.Context, request *api.LoginReques return nil, fmt.Errorf("unable to authenticate the refresh token %v: %v", request.RefreshToken, err) } - x.AssertTrue(len(userData) > 0) userId := userData[0] user, err = authorizeUser(ctx, userId, "") @@ -179,6 +178,7 @@ func validateToken(jwtStr string) ([]string, error) { for _, group := range groups { groupId, ok := group.(string) if !ok { + // This shouldn't happen. So, no need to make the client try to refresh the tokens. return nil, fmt.Errorf("unable to convert group to string:%v", group) } @@ -322,7 +322,7 @@ func RefreshAcls(closer *y.Closer) { storedEntries++ aclCache.Store(group.GroupID, &group) } - glog.V(1).Infof("updated the ACL cache with %d entries", storedEntries) + glog.V(1).Infof("Updated the ACL cache with %d entries", storedEntries) return nil } @@ -350,18 +350,16 @@ const queryAcls = ` // the acl cache mapping group names to the corresponding group acls var aclCache sync.Map -// clear the aclCache and upsert the admin account +// clear the aclCache and upsert the Groot account. func ResetAcl() { if len(Config.HmacSecret) == 0 { // the acl feature is not turned on return } - upsertAdmin := func(ctx context.Context) error { - // upsert the admin account - + upsertGroot := func(ctx context.Context) error { queryVars := map[string]string{ - "$userid": "admin", + "$userid": x.GrootId, "$password": "", } queryRequest := api.Request{ @@ -371,26 +369,25 @@ func ResetAcl() { queryResp, err := (&Server{}).doQuery(ctx, &queryRequest) if err != nil { - return fmt.Errorf("error while query user with id admin: %v", err) + return fmt.Errorf("error while querying user with id %s: %v", x.GrootId, err) } startTs := queryResp.GetTxn().StartTs - adminUser, err := acl.UnmarshalUser(queryResp, "user") + rootUser, err := acl.UnmarshalUser(queryResp, "user") if err != nil { - return fmt.Errorf("error while unmarshaling the admin user: %v", err) + return fmt.Errorf("error while unmarshaling the root user: %v", err) } - - if adminUser != nil { - // the admin user already exists, no need to create + if rootUser != nil { + // the user already exists, no need to create return nil } - // insert the admin user + // Insert Groot. createUserNQuads := []*api.NQuad{ { Subject: "_:newuser", Predicate: "dgraph.xid", - ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: "admin"}}, + ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: x.GrootId}}, }, { Subject: "_:newuser", @@ -399,22 +396,25 @@ func ResetAcl() { }} mu := &api.Mutation{ - StartTs: startTs, - CommitNow: true, - Set: createUserNQuads, + StartTs: startTs, + Set: createUserNQuads, } if _, err := (&Server{}).doMutate(context.Background(), mu); err != nil { - return fmt.Errorf("unable to create admin: %v", err) + return err } return nil } aclCache = sync.Map{} - if err := upsertAdmin(context.Background()); err != nil { - glog.Infof("Unable to upsert the admin account:%v", err) - } else { - glog.Info("Created the admin account with the default password") + for { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + if err := upsertGroot(ctx); err != nil { + glog.Infof("Unable to upsert the groot account. Error: %v", err) + } else { + return + } } } @@ -444,14 +444,13 @@ func authorizeAlter(ctx context.Context, op *api.Operation) error { if err != nil { return status.Error(codes.Unauthenticated, err.Error()) } - x.AssertTrue(len(userData) > 0) - if isAdmin(userData) { + if isGroot(userData) { return nil } - // if we get here, we know the user is not admin + // if we get here, we know the user is not Groot. if op.DropAll { - return fmt.Errorf("only the admin is allowed to drop all data") + return fmt.Errorf("only Groot is allowed to drop all data") } groupIds := userData[1:] @@ -495,9 +494,8 @@ func authorizeMutation(ctx context.Context, mu *api.Mutation) error { if err != nil { return status.Error(codes.Unauthenticated, err.Error()) } - x.AssertTrue(len(userData) > 0) - if isAdmin(userData) { - // the admin account has access to everything + if isGroot(userData) { + // Groot has access to everything. return nil } @@ -534,12 +532,12 @@ func parsePredsFromQuery(gqls []*gql.GraphQuery) map[string]struct{} { return preds } -func isAdmin(userData []string) bool { +func isGroot(userData []string) bool { if len(userData) == 0 { return false } - return userData[0] == "admin" + return userData[0] == x.GrootId } //authorizeQuery authorizes the query using the aclCache @@ -553,8 +551,7 @@ func authorizeQuery(ctx context.Context, req *api.Request) error { if err != nil { return status.Error(codes.Unauthenticated, err.Error()) } - x.AssertTrue(len(userData) > 0) - if isAdmin(userData) { + if isGroot(userData) { return nil } @@ -595,10 +592,10 @@ func hasAccess(groupId string, predicate string, operation *acl.Operation) error 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, + 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", + return fmt.Errorf("group %s not allowed to do %s on predicate %s", groupId, operation.Name, predicate) } return nil diff --git a/ee/acl/acl_test.go b/ee/acl/acl_test.go index 0344fb14289..738a0b236a8 100644 --- a/ee/acl/acl_test.go +++ b/ee/acl/acl_test.go @@ -49,39 +49,39 @@ func checkOutput(t *testing.T, cmd *exec.Cmd, shouldFail bool) string { func TestCreateAndDeleteUsers(t *testing.T) { // clean up the user to allow repeated running of this test cleanUserCmd := exec.Command("dgraph", "acl", "userdel", "-d", dgraphEndpoint, - "-u", userid, "--adminPassword", "password") + "-u", userid, "-x", "password") cleanUserCmd.Run() glog.Infof("cleaned up db user state") createUserCmd1 := exec.Command("dgraph", "acl", "useradd", "-d", dgraphEndpoint, "-u", userid, - "-p", userpassword, "--adminPassword", "password") + "-p", userpassword, "-x", "password") checkOutput(t, createUserCmd1, false) createUserCmd2 := exec.Command("dgraph", "acl", "useradd", "-d", dgraphEndpoint, "-u", userid, - "-p", userpassword, "--adminPassword", "password") + "-p", userpassword, "-x", "password") // create the user again should fail checkOutput(t, createUserCmd2, true) // delete the user deleteUserCmd := exec.Command("dgraph", "acl", "userdel", "-d", dgraphEndpoint, "-u", userid, - "--adminPassword", "password") + "-x", "password") checkOutput(t, deleteUserCmd, false) // now we should be able to create the user again createUserCmd3 := exec.Command("dgraph", "acl", "useradd", "-d", dgraphEndpoint, "-u", userid, - "-p", userpassword, "--adminPassword", "password") + "-p", userpassword, "-x", "password") checkOutput(t, createUserCmd3, false) } func resetUser(t *testing.T) { // delete and recreate the user to ensure a clean state deleteUserCmd := exec.Command("dgraph", "acl", "userdel", "-d", dgraphEndpoint, - "-u", userid, "--adminPassword", "password") + "-u", userid, "-x", "password") deleteUserCmd.Run() glog.Infof("deleted user") createUserCmd := exec.Command("dgraph", "acl", "useradd", "-d", dgraphEndpoint, "-u", - userid, "-p", userpassword, "--adminPassword", "password") + userid, "-p", userpassword, "-x", "password") checkOutput(t, createUserCmd, false) glog.Infof("created user") } @@ -215,7 +215,7 @@ func createGroupAndAcls(t *testing.T) { createGroupCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), "acl", "groupadd", "-d", dgraphEndpoint, - "-g", group, "--adminPassword", "password") + "-g", group, "-x", "password") if err := createGroupCmd.Run(); err != nil { t.Fatalf("Unable to create group:%v", err) } @@ -224,7 +224,7 @@ func createGroupAndAcls(t *testing.T) { addUserToGroupCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), "acl", "usermod", "-d", dgraphEndpoint, - "-u", userid, "-g", group, "--adminPassword", "password") + "-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) } @@ -233,7 +233,7 @@ func createGroupAndAcls(t *testing.T) { addReadPermCmd1 := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), "acl", "chmod", "-d", dgraphEndpoint, - "-g", group, "-p", predicateToRead, "-P", strconv.Itoa(int(Read.Code)), "--adminPassword", + "-g", group, "-p", predicateToRead, "-P", strconv.Itoa(int(Read.Code)), "-x", "password") if err := addReadPermCmd1.Run(); err != nil { t.Fatalf("Unable to add READ permission on %s to group %s:%v", @@ -244,7 +244,7 @@ func createGroupAndAcls(t *testing.T) { addReadPermCmd2 := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), "acl", "chmod", "-d", dgraphEndpoint, - "-g", group, "-p", queryAttr, "-P", strconv.Itoa(int(Read.Code)), "--adminPassword", + "-g", group, "-p", queryAttr, "-P", strconv.Itoa(int(Read.Code)), "-x", "password") if err := addReadPermCmd2.Run(); err != nil { t.Fatalf("Unable to add READ permission on %s to group %s:%v", queryAttr, group, err) @@ -254,7 +254,7 @@ func createGroupAndAcls(t *testing.T) { addWritePermCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), "acl", "chmod", "-d", dgraphEndpoint, - "-g", group, "-p", predicateToWrite, "-P", strconv.Itoa(int(Write.Code)), "--adminPassword", + "-g", group, "-p", predicateToWrite, "-P", strconv.Itoa(int(Write.Code)), "-x", "password") if err := addWritePermCmd.Run(); err != nil { t.Fatalf("Unable to add permission on %s to group %s:%v", predicateToWrite, group, err) @@ -264,7 +264,7 @@ func createGroupAndAcls(t *testing.T) { addModifyPermCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"), "acl", "chmod", "-d", dgraphEndpoint, - "-g", group, "-p", predicateToAlter, "-P", strconv.Itoa(int(Modify.Code)), "--adminPassword", + "-g", group, "-p", predicateToAlter, "-P", strconv.Itoa(int(Modify.Code)), "-x", "password") if err := addModifyPermCmd.Run(); err != nil { t.Fatalf("Unable to add permission on %s to group %s:%v", predicateToAlter, group, err) diff --git a/ee/acl/run_ee.go b/ee/acl/run_ee.go index d299dd7696f..c4282d7838e 100644 --- a/ee/acl/run_ee.go +++ b/ee/acl/run_ee.go @@ -32,10 +32,13 @@ type options struct { dgraph string } -var opt options -var tlsConf x.TLSHelperConfig +var ( + opt options + tlsConf x.TLSHelperConfig + CmdAcl x.SubCommand +) -var CmdAcl x.SubCommand +const gPassword = "gpassword" func init() { CmdAcl.Cmd = &cobra.Command{ @@ -44,7 +47,8 @@ func init() { } flag := CmdAcl.Cmd.PersistentFlags() - flag.StringP("dgraph", "d", "127.0.0.1:9080", "Dgraph gRPC server address") + flag.StringP("dgraph", "d", "127.0.0.1:9080", "Dgraph Alpha gRPC server address") + flag.StringP(gPassword, "x", "", "Groot password to authorize this operation") // TLS configuration x.RegisterTLSFlags(flag) @@ -78,7 +82,6 @@ func initSubcommands() []*x.SubCommand { }, } userAddFlags := cmdUserAdd.Cmd.Flags() - userAddFlags.String("adminPassword", "", "The admin password used to authorize this operation") userAddFlags.StringP("user", "u", "", "The user id to be created") userAddFlags.StringP("password", "p", "", "The password for the user") @@ -95,7 +98,6 @@ func initSubcommands() []*x.SubCommand { }, } userDelFlags := cmdUserDel.Cmd.Flags() - userDelFlags.String("adminPassword", "", "The admin password used to authorize this operation") userDelFlags.StringP("user", "u", "", "The user id to be deleted") // group creation command @@ -111,7 +113,6 @@ func initSubcommands() []*x.SubCommand { }, } groupAddFlags := cmdGroupAdd.Cmd.Flags() - groupAddFlags.String("adminPassword", "", "The admin password used to authorize this operation") groupAddFlags.StringP("group", "g", "", "The group id to be created") // group deletion command @@ -127,7 +128,6 @@ func initSubcommands() []*x.SubCommand { }, } groupDelFlags := cmdGroupDel.Cmd.Flags() - groupDelFlags.String("adminPassword", "", "The admin password used to authorize this operation") groupDelFlags.StringP("group", "g", "", "The group id to be deleted") // the usermod command used to set a user's groups @@ -143,7 +143,6 @@ func initSubcommands() []*x.SubCommand { }, } userModFlags := cmdUserMod.Cmd.Flags() - userModFlags.String("adminPassword", "", "The admin password used to authorize this operation") userModFlags.StringP("user", "u", "", "The user id to be changed") userModFlags.StringP("groups", "g", "", "The groups to be set for the user") @@ -160,7 +159,6 @@ func initSubcommands() []*x.SubCommand { }, } chModFlags := cmdChMod.Cmd.Flags() - chModFlags.String("adminPassword", "", "The admin password used to authorize this operation") chModFlags.StringP("group", "g", "", "The group whose permission "+ "is to be changed") chModFlags.StringP("pred", "p", "", "The predicates whose acls"+ @@ -180,7 +178,6 @@ func initSubcommands() []*x.SubCommand { }, } infoFlags := cmdInfo.Cmd.Flags() - infoFlags.String("adminPassword", "", "The admin password used to authorize this operation") infoFlags.StringP("user", "u", "", "The user to be shown") infoFlags.StringP("group", "g", "", "The group to be shown") return []*x.SubCommand{ diff --git a/ee/acl/users.go b/ee/acl/users.go index 88d43911df6..cdc8d0047be 100644 --- a/ee/acl/users.go +++ b/ee/acl/users.go @@ -28,9 +28,9 @@ import ( ) func getClientWithAdminCtx(conf *viper.Viper) (*dgo.Dgraph, CloseFunc, error) { - adminPassword := conf.GetString("adminPassword") + adminPassword := conf.GetString(gPassword) if len(adminPassword) == 0 { - fmt.Print("Enter admin password:") + fmt.Print("Enter groot password:") password, err := terminal.ReadPassword(int(syscall.Stdin)) if err != nil { return nil, func() {}, fmt.Errorf("error while reading password:%v", err) @@ -46,10 +46,10 @@ func getClientWithAdminCtx(conf *viper.Viper) (*dgo.Dgraph, CloseFunc, error) { closeClient() } - if err := dc.Login(ctx, "admin", adminPassword); err != nil { - return dc, cleanFunc, fmt.Errorf("unable to login with the admin account %v", err) + if err := dc.Login(ctx, x.GrootId, adminPassword); err != nil { + return dc, cleanFunc, fmt.Errorf("unable to login to the groot account %v", err) } - glog.Infof("login successfully with the admin account") + glog.Infof("login successfully to the groot account") // update the context so that it has the admin jwt token return dc, cleanFunc, nil } diff --git a/x/x.go b/x/x.go index 408302102db..58c19c74592 100644 --- a/x/x.go +++ b/x/x.go @@ -73,6 +73,8 @@ const ( TlsClientCert = "client.crt" TlsClientKey = "client.key" + + GrootId = "groot" ) var ( From e3048e065ffa0e9b99ed2240b23be8455da2522c Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Wed, 16 Jan 2019 13:15:39 -0800 Subject: [PATCH 11/15] use commitNow for upsert groot and sleep in between upsert retries login using the new name groot moved GetDgraphClient from dgo to x --- edgraph/access_ee.go | 6 ++++-- ee/acl/acl_test.go | 12 ++++++------ systest/bulk_live_fixture_test.go | 6 +++--- x/x.go | 25 +++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/edgraph/access_ee.go b/edgraph/access_ee.go index 0171503649c..7d2d6611e96 100644 --- a/edgraph/access_ee.go +++ b/edgraph/access_ee.go @@ -396,8 +396,9 @@ func ResetAcl() { }} mu := &api.Mutation{ - StartTs: startTs, - Set: createUserNQuads, + StartTs: startTs, + CommitNow: true, + Set: createUserNQuads, } if _, err := (&Server{}).doMutate(context.Background(), mu); err != nil { @@ -412,6 +413,7 @@ func ResetAcl() { defer cancel() if err := upsertGroot(ctx); err != nil { glog.Infof("Unable to upsert the groot account. Error: %v", err) + time.Sleep(100 * time.Millisecond) } else { return } diff --git a/ee/acl/acl_test.go b/ee/acl/acl_test.go index 738a0b236a8..e2ce604c749 100644 --- a/ee/acl/acl_test.go +++ b/ee/acl/acl_test.go @@ -25,7 +25,7 @@ import ( "github.com/dgraph-io/dgo" "github.com/dgraph-io/dgo/protos/api" - "github.com/dgraph-io/dgo/test" + "github.com/dgraph-io/dgraph/x" "github.com/golang/glog" "github.com/stretchr/testify/require" ) @@ -88,13 +88,13 @@ func resetUser(t *testing.T) { func TestAuthorization(t *testing.T) { glog.Infof("testing with port 9180") - dg1, cancel := test.GetDgraphClientOnPort(9180) + dg1, cancel := x.GetDgraphClientOnPort(9180) defer cancel() testAuthorization(t, dg1) glog.Infof("done") glog.Infof("testing with port 9182") - dg2, cancel := test.GetDgraphClientOnPort(9182) + dg2, cancel := x.GetDgraphClientOnPort(9182) defer cancel() testAuthorization(t, dg2) glog.Infof("done") @@ -184,10 +184,10 @@ func alterPredicateWithUserAccount(t *testing.T, dg *dgo.Dgraph, shouldFail bool } func createAccountAndData(t *testing.T, dg *dgo.Dgraph) { - // use the admin account to clean the database + // use the groot account to clean the database ctx := context.Background() - if err := dg.Login(ctx, "admin", "password"); err != nil { - t.Fatalf("unable to login using the admin account:%v", err) + if err := dg.Login(ctx, x.GrootId, "password"); err != nil { + t.Fatalf("unable to login using the groot account:%v", err) } op := api.Operation{ DropAll: true, diff --git a/systest/bulk_live_fixture_test.go b/systest/bulk_live_fixture_test.go index df77552b07f..058aead39a6 100644 --- a/systest/bulk_live_fixture_test.go +++ b/systest/bulk_live_fixture_test.go @@ -31,7 +31,7 @@ import ( "time" "github.com/dgraph-io/dgo/protos/api" - "github.com/dgraph-io/dgo/test" + "github.com/dgraph-io/dgraph/x" "github.com/pkg/errors" ) @@ -51,7 +51,7 @@ type suite struct { } func newSuite(t *testing.T, schema, rdfs string) *suite { - dg, close := test.GetDgraphClient() + dg, close := x.GetDgraphClient() defer close() err := dg.Alter(context.Background(), &api.Operation{ @@ -138,7 +138,7 @@ func (s *suite) cleanup() { func (s *suite) testCase(query, wantResult string) func(*testing.T) { return func(t *testing.T) { - dg, close := test.GetDgraphClient() + dg, close := x.GetDgraphClient() defer close() ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() diff --git a/x/x.go b/x/x.go index 58c19c74592..2943e59cf06 100644 --- a/x/x.go +++ b/x/x.go @@ -21,6 +21,7 @@ import ( "bytes" "encoding/json" "fmt" + "log" "math" "math/rand" "net" @@ -32,6 +33,8 @@ import ( "strings" "time" + "github.com/dgraph-io/dgo" + "github.com/dgraph-io/dgo/protos/api" "go.opencensus.io/trace" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -490,3 +493,25 @@ func SpanTimer(span *trace.Span, name string) func() { span.Annotatef(attrs, "End. Took %s", time.Since(start)) } } + +type CancelFunc func() + +const DgraphAlphaPort = 9180 + +func GetDgraphClient() (*dgo.Dgraph, CancelFunc) { + return GetDgraphClientOnPort(DgraphAlphaPort) +} + +func GetDgraphClientOnPort(alphaPort int) (*dgo.Dgraph, CancelFunc) { + conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%d", alphaPort), grpc.WithInsecure()) + if err != nil { + log.Fatal("While trying to dial gRPC") + } + + dc := api.NewDgraphClient(conn) + return dgo.NewDgraphClient(dc), func() { + if err := conn.Close(); err != nil { + log.Printf("Error while closing connection:%v", err) + } + } +} From 8581efb041659c5fcc56537f29940af0c4c2c809 Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Wed, 16 Jan 2019 14:26:38 -0800 Subject: [PATCH 12/15] fixed issues around automatic login using refresh token --- edgraph/access_ee.go | 7 ++++--- edgraph/server.go | 4 ++-- ee/acl/acl_test.go | 9 +++++---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/edgraph/access_ee.go b/edgraph/access_ee.go index 7d2d6611e96..d912be3be59 100644 --- a/edgraph/access_ee.go +++ b/edgraph/access_ee.go @@ -118,6 +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) return user, nil } @@ -163,7 +164,7 @@ func validateToken(jwtStr string) ([]string, error) { // here we enforce the checking to make sure that the refresh token has not expired now := time.Now().Unix() if !claims.VerifyExpiresAt(now, true) { - return nil, fmt.Errorf("jwt token has expired at %v", now) + return nil, fmt.Errorf("Token is expired") // the same error msg that's used inside jwt-go } userId, ok := claims["userid"].(string) @@ -470,7 +471,7 @@ func authorizeAlter(ctx context.Context, op *api.Operation) error { } for _, update := range updates { if err := authorizePredicate(groupIds, update.Predicate, acl.Modify); err != nil { - return fmt.Errorf("unauthorized to modify the predicate %v", err) + return fmt.Errorf("unauthorized to modify the predicate: %v", err) } } return nil @@ -569,7 +570,7 @@ func authorizeQuery(ctx context.Context, req *api.Request) error { for pred := range parsePredsFromQuery(parsedReq.Query) { if err := authorizePredicate(groupIds, pred, acl.Read); err != nil { return status.Error(codes.PermissionDenied, - fmt.Sprintf("unauthorized to access the predicate %v", err)) + fmt.Sprintf("unauthorized to access the predicate: %v", err)) } } return nil diff --git a/edgraph/server.go b/edgraph/server.go index b5cd83d6db8..c8b78f5d253 100644 --- a/edgraph/server.go +++ b/edgraph/server.go @@ -344,7 +344,7 @@ func annotateStartTs(span *otrace.Span, ts uint64) { func (s *Server) Mutate(ctx context.Context, mu *api.Mutation) (resp *api.Assigned, err error) { if err := authorizeMutation(ctx, mu); err != nil { - return nil, fmt.Errorf("mutation is not authorized: %v", err) + return nil, err } return s.doMutate(ctx, mu) @@ -459,7 +459,7 @@ func (s *Server) doMutate(ctx context.Context, mu *api.Mutation) (resp *api.Assi func (s *Server) Query(ctx context.Context, req *api.Request) (*api.Response, error) { if err := authorizeQuery(ctx, req); err != nil { - return nil, fmt.Errorf("query is not authorized: %v", err) + return nil, err } return s.doQuery(ctx, req) diff --git a/ee/acl/acl_test.go b/ee/acl/acl_test.go index e2ce604c749..7a169ae7f74 100644 --- a/ee/acl/acl_test.go +++ b/ee/acl/acl_test.go @@ -102,6 +102,11 @@ func TestAuthorization(t *testing.T) { func testAuthorization(t *testing.T, dg *dgo.Dgraph) { createAccountAndData(t, dg) + ctx := context.Background() + if err := dg.Login(ctx, userid, userpassword); err != nil { + t.Fatalf("unable to login using the account %v", userid) + } + queryPredicateWithUserAccount(t, dg, true) mutatePredicateWithUserAccount(t, dg, true) alterPredicateWithUserAccount(t, dg, true) @@ -127,10 +132,6 @@ var rootDir = filepath.Join(os.TempDir(), "acl_test") func queryPredicateWithUserAccount(t *testing.T, dg *dgo.Dgraph, shouldFail bool) { // login with alice's account ctx := context.Background() - if err := dg.Login(ctx, userid, userpassword); err != nil { - t.Fatalf("unable to login using the account %v", userid) - } - txn := dg.NewTxn() query := fmt.Sprintf(` { From e06ed42e996013e3b33d9358908c05ec8ab99593 Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Wed, 16 Jan 2019 15:58:57 -0800 Subject: [PATCH 13/15] fixed issues for testing auto login --- edgraph/access_ee.go | 33 ++++++++++++++++----------------- ee/acl/acl_test.go | 17 ++++++----------- ee/acl/docker-compose.yml | 4 ++-- 3 files changed, 24 insertions(+), 30 deletions(-) diff --git a/edgraph/access_ee.go b/edgraph/access_ee.go index d912be3be59..337957415b2 100644 --- a/edgraph/access_ee.go +++ b/edgraph/access_ee.go @@ -14,9 +14,7 @@ package edgraph import ( "context" - "encoding/json" "fmt" - "strconv" "sync" "time" @@ -59,17 +57,17 @@ func (s *Server) Login(ctx context.Context, } resp := &api.Response{} - accessJwt, err := getAccessJwt(request.Userid, user.Groups) + accessJwt, err := getAccessJwt(user.UserID, user.Groups) if err != nil { errMsg := fmt.Sprintf("unable to get access jwt (userid=%s,addr=%s):%v", - request.Userid, addr, err) + user.UserID, addr, err) glog.Errorf(errMsg) return nil, fmt.Errorf(errMsg) } - refreshJwt, err := getRefreshJwt(request.Userid) + refreshJwt, err := getRefreshJwt(user.UserID) if err != nil { errMsg := fmt.Sprintf("unable to get refresh jwt (userid=%s,addr=%s):%v", - request.Userid, addr, err) + user.UserID, addr, err) glog.Errorf(errMsg) return nil, fmt.Errorf(errMsg) } @@ -82,7 +80,7 @@ func (s *Server) Login(ctx context.Context, jwtBytes, err := loginJwt.Marshal() if err != nil { errMsg := fmt.Sprintf("unable to marshal jwt (userid=%s,addr=%s):%v", - request.Userid, addr, err) + user.UserID, addr, err) glog.Errorf(errMsg) return nil, fmt.Errorf(errMsg) } @@ -217,8 +215,7 @@ func getAccessJwt(userId string, groups []acl.Group) (string, error) { "userid": userId, "groups": acl.GetGroupIDs(groups), // set the jwt exp according to the ttl - "exp": json.Number( - strconv.FormatInt(time.Now().Add(Config.AccessJwtTtl).Unix(), 10)), + "exp": time.Now().Add(Config.AccessJwtTtl).Unix(), }) jwtString, err := token.SignedString(Config.HmacSecret) @@ -233,9 +230,7 @@ func getAccessJwt(userId string, groups []acl.Group) (string, error) { func getRefreshJwt(userId string) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "userid": userId, - // set the jwt exp according to the ttl - "exp": json.Number( - strconv.FormatInt(time.Now().Add(Config.RefreshJwtTtl).Unix(), 10)), + "exp": time.Now().Add(Config.RefreshJwtTtl).Unix(), }) jwtString, err := token.SignedString(Config.HmacSecret) @@ -249,6 +244,7 @@ const queryUser = ` query search($userid: string, $password: string){ user(func: eq(dgraph.xid, $userid)) { uid + dgraph.xid password_match: checkpwd(dgraph.password, $password) dgraph.user.group { uid @@ -453,14 +449,15 @@ func authorizeAlter(ctx context.Context, op *api.Operation) error { // if we get here, we know the user is not Groot. if op.DropAll { - return fmt.Errorf("only Groot is allowed to drop all data") + return fmt.Errorf("only Groot is allowed to drop all data, current user is %s", 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 { - return fmt.Errorf("unauthorized to modify the predicate:%v", err) + return status.Error(codes.PermissionDenied, + fmt.Sprintf("unauthorized to alter the predicate:%v", err)) } return nil } @@ -471,7 +468,8 @@ func authorizeAlter(ctx context.Context, op *api.Operation) error { } for _, update := range updates { if err := authorizePredicate(groupIds, update.Predicate, acl.Modify); err != nil { - return fmt.Errorf("unauthorized to modify the predicate: %v", err) + return status.Error(codes.PermissionDenied, + fmt.Sprintf("unauthorized to alter the predicate: %v", err)) } } return nil @@ -510,7 +508,8 @@ func authorizeMutation(ctx context.Context, mu *api.Mutation) error { groupIds := userData[1:] for pred := range parsePredsFromMutation(gmu.Set) { if err := authorizePredicate(groupIds, pred, acl.Write); err != nil { - return fmt.Errorf("unauthorized to access the predicate: %v", err) + return status.Error(codes.PermissionDenied, + fmt.Sprintf("unauthorized to mutate the predicate: %v", err)) } } return nil @@ -570,7 +569,7 @@ func authorizeQuery(ctx context.Context, req *api.Request) error { for pred := range parsePredsFromQuery(parsedReq.Query) { if err := authorizePredicate(groupIds, pred, acl.Read); err != nil { return status.Error(codes.PermissionDenied, - fmt.Sprintf("unauthorized to access the predicate: %v", err)) + fmt.Sprintf("unauthorized to query the predicate: %v", err)) } } return nil diff --git a/ee/acl/acl_test.go b/ee/acl/acl_test.go index 7a169ae7f74..c08391b726a 100644 --- a/ee/acl/acl_test.go +++ b/ee/acl/acl_test.go @@ -112,13 +112,16 @@ 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 - // on all alpha servers, this also tests that the automatic login with refresh - // jwt works after the access jwt expires in 30 seconds log.Println("Sleeping for 35 seconds for acl to catch up") time.Sleep(35 * time.Second) - 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") + time.Sleep(12 * time.Second) mutatePredicateWithUserAccount(t, dg, false) + log.Println("Sleeping for 12 seconds for accessJwt to expire") + time.Sleep(12 * time.Second) alterPredicateWithUserAccount(t, dg, false) } @@ -151,10 +154,6 @@ func queryPredicateWithUserAccount(t *testing.T, dg *dgo.Dgraph, shouldFail bool func mutatePredicateWithUserAccount(t *testing.T, dg *dgo.Dgraph, shouldFail bool) { ctx := context.Background() - if err := dg.Login(ctx, userid, userpassword); err != nil { - t.Fatalf("unable to login using the account %v", userid) - } - txn := dg.NewTxn() _, err := txn.Mutate(ctx, &api.Mutation{ CommitNow: true, @@ -170,10 +169,6 @@ func mutatePredicateWithUserAccount(t *testing.T, dg *dgo.Dgraph, shouldFail boo func alterPredicateWithUserAccount(t *testing.T, dg *dgo.Dgraph, shouldFail bool) { ctx := context.Background() - if err := dg.Login(ctx, userid, userpassword); err != nil { - t.Fatalf("unable to login using the account %v", userid) - } - err := dg.Alter(ctx, &api.Operation{ Schema: fmt.Sprintf(`%s: int .`, predicateToAlter), }) diff --git a/ee/acl/docker-compose.yml b/ee/acl/docker-compose.yml index dcd86409a4b..efc15c85ca6 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 30s + 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 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 30s + 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 labels: cluster: test From 0594494e20df4aa87a27604e438aab3b6e6772b9 Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Mon, 21 Jan 2019 15:02:40 -0800 Subject: [PATCH 14/15] adding admin password support to the info subcommand --- ee/acl/run_ee.go | 11 ++++++----- ee/acl/users.go | 30 ------------------------------ ee/acl/utils.go | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/ee/acl/run_ee.go b/ee/acl/run_ee.go index c4282d7838e..445134f83fd 100644 --- a/ee/acl/run_ee.go +++ b/ee/acl/run_ee.go @@ -18,7 +18,6 @@ import ( "fmt" "os" "strings" - "time" "github.com/dgraph-io/dgo" "github.com/dgraph-io/dgo/protos/api" @@ -219,11 +218,13 @@ func info(conf *viper.Viper) error { (len(userId) != 0 && len(groupId) != 0) { return fmt.Errorf("Either the user or group should be specified, not both") } - - dc, close := getDgraphClient(conf) - defer close() - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + dc, cancel, err := getClientWithAdminCtx(conf) defer cancel() + if err != nil { + 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 { diff --git a/ee/acl/users.go b/ee/acl/users.go index cdc8d0047be..1c4174bd200 100644 --- a/ee/acl/users.go +++ b/ee/acl/users.go @@ -16,44 +16,14 @@ import ( "context" "fmt" "strings" - "syscall" - "time" "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" - "golang.org/x/crypto/ssh/terminal" ) -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)) - if err != nil { - return nil, func() {}, fmt.Errorf("error while reading password:%v", err) - } - adminPassword = string(password) - } - - dc, closeClient := getDgraphClient(conf) - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - - cleanFunc := func() { - cancel() - 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) - } - glog.Infof("login successfully to the groot account") - // update the context so that it has the admin jwt token - return dc, cleanFunc, nil -} - func userAdd(conf *viper.Viper) error { userid := conf.GetString("user") password := conf.GetString("password") diff --git a/ee/acl/utils.go b/ee/acl/utils.go index 5821b012e76..60a852b8d82 100644 --- a/ee/acl/utils.go +++ b/ee/acl/utils.go @@ -13,12 +13,18 @@ package acl import ( + "context" "encoding/json" "fmt" + "syscall" + "time" + "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" + "golang.org/x/crypto/ssh/terminal" ) func GetGroupIDs(groups []Group) []string { @@ -140,3 +146,30 @@ func UnmarshalGroups(input []byte, groupKey string) (group []Group, err error) { 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)) + if err != nil { + return nil, func() {}, fmt.Errorf("error while reading password:%v", err) + } + adminPassword = string(password) + } + + dc, closeClient := getDgraphClient(conf) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + + cleanFunc := func() { + cancel() + 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) + } + glog.Infof("login successfully to the groot account") + // update the context so that it has the admin jwt token + return dc, cleanFunc, nil +} From 697967889a97cc23e53e0342e11ec528ae8b72e7 Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Mon, 21 Jan 2019 15:19:34 -0800 Subject: [PATCH 15/15] removing non-running containers as well --- contrib/scripts/functions.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/scripts/functions.sh b/contrib/scripts/functions.sh index 177c9612a98..51577821105 100755 --- a/contrib/scripts/functions.sh +++ b/contrib/scripts/functions.sh @@ -14,7 +14,7 @@ function restartCluster { basedir=$GOPATH/src/github.com/dgraph-io/dgraph pushd $basedir/dgraph >/dev/null go build . && go install . && md5sum dgraph $GOPATH/bin/dgraph - docker ps --filter label="cluster=test" --format "{{.Names}}" | xargs -r docker rm -f + docker ps -a --filter label="cluster=test" --format "{{.Names}}" | xargs docker rm -f docker-compose -f $compose_file up --force-recreate --remove-orphans --detach popd >/dev/null