Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cloud): add shared-instance flag in limit superflag in alpha #7770

Merged
merged 12 commits into from
May 5, 2021
6 changes: 5 additions & 1 deletion dgraph/cmd/alpha/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ they form a Raft group and provide synchronous replication.
"worker in a failed state. Use -1 to retry infinitely.").
Flag("txn-abort-after", "Abort any pending transactions older than this duration."+
" The liveness of a transaction is determined by its last mutation.").
Flag("shared-instance", "When set to true, it disables ACLs for non-galaxy users. "+
"It expects the access JWT to be contructed outside dgraph for those users as even "+
"login is denied to them. Additionally, this disables access to environment variables"+
"for minio, aws, etc.").
String())

flag.String("graphql", worker.GraphQLDefaults, z.NewSuperFlagHelp(worker.GraphQLDefaults).
Expand Down Expand Up @@ -623,7 +627,6 @@ func run() {
pstoreBlockCacheSize, pstoreIndexCacheSize)
bopts := badger.DefaultOptions("").FromSuperFlag(worker.BadgerDefaults + cacheOpts).
FromSuperFlag(Alpha.Conf.GetString("badger"))

security := z.NewSuperFlag(Alpha.Conf.GetString("security")).MergeAndCheckDefault(
worker.SecurityDefaults)
conf := audit.GetAuditConf(Alpha.Conf.GetString("audit"))
Expand Down Expand Up @@ -712,6 +715,7 @@ func run() {
x.Config.LimitNormalizeNode = int(x.Config.Limit.GetInt64("normalize-node"))
x.Config.QueryTimeout = x.Config.Limit.GetDuration("query-timeout")
x.Config.MaxRetries = x.Config.Limit.GetInt64("max-retries")
x.Config.SharedInstance = x.Config.Limit.GetBool("shared-instance")

x.Config.GraphQL = z.NewSuperFlag(Alpha.Conf.GetString("graphql")).MergeAndCheckDefault(
worker.GraphQLDefaults)
Expand Down
78 changes: 53 additions & 25 deletions edgraph/access_ee.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ type predsAndvars struct {
func (s *Server) Login(ctx context.Context,
request *api.LoginRequest) (*api.Response, error) {

if !shouldAllowAcls(request.GetNamespace()) {
return nil, errors.New("operation is not allowed in cloud mode")
}

if err := x.HealthCheck(); err != nil {
return nil, err
}
Expand Down Expand Up @@ -605,14 +609,11 @@ type authPredResult struct {
}

func authorizePreds(ctx context.Context, userData *userData, preds []string,
aclOp *acl.Operation) (*authPredResult, error) {
aclOp *acl.Operation) *authPredResult {
NamanJain8 marked this conversation as resolved.
Show resolved Hide resolved

ns, err := x.ExtractNamespace(ctx)
if err != nil {
return nil, errors.Wrapf(err, "While authorizing preds")
}
userId := userData.userId
groupIds := userData.groupIds
ns := userData.namespace
blockedPreds := make(map[string]struct{})
for _, pred := range preds {
nsPred := x.NamespaceAttr(ns, pred)
Expand All @@ -638,7 +639,7 @@ func authorizePreds(ctx context.Context, userData *userData, preds []string,
}
}
aclCachePtr.RUnlock()
return &authPredResult{allowed: allowedPreds, blocked: blockedPreds}, nil
return &authPredResult{allowed: allowedPreds, blocked: blockedPreds}
}

// authorizeAlter parses the Schema in the operation and authorizes the operation
Expand Down Expand Up @@ -693,10 +694,7 @@ func authorizeAlter(ctx context.Context, op *api.Operation) error {
"only guardians are allowed to drop all data, but the current user is %s", userId)
}

result, err := authorizePreds(ctx, userData, preds, acl.Modify)
if err != nil {
return nil
}
result := authorizePreds(ctx, userData, preds, acl.Modify)
if len(result.blocked) > 0 {
var msg strings.Builder
for key := range result.blocked {
Expand Down Expand Up @@ -805,12 +803,17 @@ func authorizeMutation(ctx context.Context, gmu *gql.Mutation) error {
case isAclPredMutation(gmu.Del):
return errors.Errorf("ACL predicates can't be deleted")
}
if !shouldAllowAcls(userData.namespace) {
for _, pred := range preds {
if x.IsAclPredicate(pred) {
return status.Errorf(codes.PermissionDenied,
"unauthorized to mutate acl predicates: %s\n", pred)
}
}
}
return nil
}
result, err := authorizePreds(ctx, userData, preds, acl.Write)
if err != nil {
return err
}
result := authorizePreds(ctx, userData, preds, acl.Write)
if len(result.blocked) > 0 {
var msg strings.Builder
for key := range result.blocked {
Expand Down Expand Up @@ -918,7 +921,12 @@ func logAccess(log *accessEntry) {
}
}

//authorizeQuery authorizes the query using the aclCachePtr. It will silently drop all
// With shared instance enabled, we don't allow ACL operations from any of the non-galaxy namespace.
func shouldAllowAcls(ns uint64) bool {
return !x.Config.SharedInstance || ns == x.GalaxyNamespace
}

// authorizeQuery authorizes the query using the aclCachePtr. It will silently drop all
// unauthorized predicates from query.
// At this stage, namespace is not attached in the predicates.
func authorizeQuery(ctx context.Context, parsedReq *gql.Result, graphql bool) error {
Expand All @@ -929,6 +937,7 @@ func authorizeQuery(ctx context.Context, parsedReq *gql.Result, graphql bool) er

var userId string
var groupIds []string
var namespace uint64
predsAndvars := parsePredsFromQuery(parsedReq.Query)
preds := predsAndvars.preds
varsToPredMap := predsAndvars.vars
Expand All @@ -948,14 +957,24 @@ func authorizeQuery(ctx context.Context, parsedReq *gql.Result, graphql bool) er

userId = userData.userId
groupIds = userData.groupIds
namespace = userData.namespace

if x.IsGuardian(groupIds) {
// Members of guardian groups are allowed to query anything.
return nil, nil, nil
if shouldAllowAcls(userData.namespace) {
// Members of guardian groups are allowed to query anything.
return nil, nil, nil
}
blocked := make(map[string]struct{})
for _, pred := range preds {
if x.IsAclPredicate(pred) {
blocked[pred] = struct{}{}
}
}
return blocked, nil, nil
}

result, err := authorizePreds(ctx, userData, preds, acl.Read)
return result.blocked, result.allowed, err
result := authorizePreds(ctx, userData, preds, acl.Read)
return result.blocked, result.allowed, nil
}

blockedPreds, allowedPreds, err := doAuthorizeQuery()
Expand All @@ -976,7 +995,7 @@ func authorizeQuery(ctx context.Context, parsedReq *gql.Result, graphql bool) er
if len(blockedPreds) != 0 {
// For GraphQL requests, we allow filtered access to the ACL predicates.
// Filter for user_id and group_id is applied for the currently logged in user.
if graphql {
if graphql && shouldAllowAcls(namespace) {
for _, gq := range parsedReq.Query {
addUserFilterToQuery(gq, userId, groupIds)
}
Expand Down Expand Up @@ -1037,11 +1056,20 @@ func authorizeSchemaQuery(ctx context.Context, er *query.ExecutionResult) error

groupIds := userData.groupIds
if x.IsGuardian(groupIds) {
// Members of guardian groups are allowed to query anything.
return nil, nil
if shouldAllowAcls(userData.namespace) {
// Members of guardian groups are allowed to query anything.
return nil, nil
}
blocked := make(map[string]struct{})
for _, pred := range preds {
if x.IsAclPredicate(pred) {
blocked[pred] = struct{}{}
}
}
return blocked, nil
}
result, err := authorizePreds(ctx, userData, preds, acl.Read)
return result.blocked, err
result := authorizePreds(ctx, userData, preds, acl.Read)
return result.blocked, nil
}

// find the predicates which are blocked for the schema query
Expand Down Expand Up @@ -1083,7 +1111,7 @@ func AuthGuardianOfTheGalaxy(ctx context.Context) error {
}
ns, err := x.ExtractJWTNamespace(ctx)
if err != nil {
return errors.Wrap(err, "Authorize guradian of the galaxy, extracting jwt token, error:")
return errors.Wrap(err, "Authorize guardian of the galaxy, extracting jwt token, error:")
}
if ns != 0 {
return errors.New("Only guardian of galaxy is allowed to do this operation")
Expand Down
184 changes: 184 additions & 0 deletions systest/cloud/cloud_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* Copyright 2021 Dgraph Labs, Inc. and Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package main

import (
"context"
"fmt"
"io/ioutil"
"net/http"
"os"
"testing"
"time"

"github.com/dgraph-io/dgo/v210/protos/api"
"github.com/dgraph-io/dgraph/graphql/e2e/common"
"github.com/dgraph-io/dgraph/testutil"
"github.com/dgraph-io/dgraph/x"
"github.com/stretchr/testify/require"
)

func prepare(t *testing.T) {
dc := testutil.DgClientWithLogin(t, "groot", "password", x.GalaxyNamespace)
require.NoError(t, dc.Alter(context.Background(), &api.Operation{DropAll: true}))
}

func readFile(t *testing.T, path string) []byte {
data, err := ioutil.ReadFile(path)
require.NoError(t, err)
return data
}

func getHttpToken(t *testing.T, user, password string, ns uint64) *testutil.HttpToken {
jwt := testutil.GetAccessJwt(t, testutil.JwtParams{
User: user,
Groups: []string{"guardians"},
Ns: ns,
Exp: time.Hour,
Secret: readFile(t, "../../ee/acl/hmac-secret"),
})

return &testutil.HttpToken{
UserId: user,
Password: password,
AccessJwt: jwt,
}
}

func graphqlHelper(t *testing.T, query string, headers http.Header,
expectedResult string) {
params := &common.GraphQLParams{
Query: query,
Headers: headers,
}
queryResult := params.ExecuteAsPost(t, common.GraphqlURL)
common.RequireNoGQLErrors(t, queryResult)
testutil.CompareJSON(t, expectedResult, string(queryResult.Data))
}

func TestDisallowNonGalaxy(t *testing.T) {
prepare(t)

galaxyToken := getHttpToken(t, "groot", "password", x.GalaxyNamespace)
// Create a new namespace
ns, err := testutil.CreateNamespaceWithRetry(t, galaxyToken)
require.NoError(t, err)
require.Greater(t, int(ns), 0)

nsToken := getHttpToken(t, "groot", "password", ns)
header := http.Header{}
header.Set("X-Dgraph-AccessToken", nsToken.AccessJwt)

// User from namespace ns should be able to query/mutate.
schema := `
type Author {
id: ID!
name: String
}`
common.SafelyUpdateGQLSchema(t, common.Alpha1HTTP, schema, header)

graphqlHelper(t, `
mutation {
addAuthor(input:{name: "Alice"}) {
author{
name
}
}
}`, header,
`{
"addAuthor": {
"author":[{
"name":"Alice"
}]
}
}`)

query := `
query {
queryAuthor {
name
}
}`
graphqlHelper(t, query, header,
`{
"queryAuthor": [
{
"name":"Alice"
}
]
}`)

// Login to namespace 1 via groot and create new user alice. Non-galaxy namespace user should
// not be able to do so in cloud mode.
_, err = testutil.HttpLogin(&testutil.LoginParams{
Endpoint: testutil.AdminUrl(),
UserID: "groot",
Passwd: "password",
Namespace: ns,
})
require.Error(t, err)
require.Contains(t, err.Error(), "operation is not allowed in cloud mode")

// Ns guardian should not be able to create user.
resp := testutil.CreateUser(t, nsToken, "alice", "newpassword")
require.Greater(t, len(resp.Errors), 0)
require.Contains(t, resp.Errors.Error(), "unauthorized to mutate acl predicates")

// Galaxy guardian should be able to create user.
resp = testutil.CreateUser(t, galaxyToken, "alice", "newpassword")
require.Equal(t, 0, len(resp.Errors))
}

func TestEnvironmentAccess(t *testing.T) {
prepare(t)

galaxyToken := getHttpToken(t, "groot", "password", x.GalaxyNamespace)
// Create a new namespace
ns, err := testutil.CreateNamespaceWithRetry(t, galaxyToken)
require.NoError(t, err)
require.Greater(t, int(ns), 0)

nsToken := getHttpToken(t, "groot", "password", ns)
header := http.Header{}
header.Set("X-Dgraph-AccessToken", nsToken.AccessJwt)

// Create a minio bucket.
bucketname := "dgraph-export"
mc, err := testutil.NewMinioClient()
require.NoError(t, err)
require.NoError(t, mc.MakeBucket(bucketname, ""))
minioDest := "minio://minio:9001/dgraph-export?secure=false"

// Export without the minio creds should fail for non-galaxy.
resp := testutil.Export(t, nsToken, minioDest, "", "")
require.Greater(t, len(resp.Errors), 0)
require.Contains(t, resp.Errors.Error(), "an error occurred, please check logs")

// Export without the minio creds should work for non-galaxy.
resp = testutil.Export(t, nsToken, minioDest, "accesskey", "secretkey")
require.Zero(t, len(resp.Errors))

// Galaxy guardian should provide the crednetials as well.
resp = testutil.Export(t, galaxyToken, minioDest, "accesskey", "secretkey")
require.Zero(t, len(resp.Errors))

}

func TestMain(m *testing.M) {
fmt.Printf("Using adminEndpoint : %s for cloud test.\n", testutil.AdminUrl())
os.Exit(m.Run())
}
Loading