Skip to content

Commit

Permalink
feat(cloud): add shared-instance flag in limit superflag in alpha (#7770
Browse files Browse the repository at this point in the history
) (#7778)

This PR adds a shared-instance flag to --limit superflag.
When set to true (false by default), it will:

- Restrict access to any of the ACL operations like Login, add/remove/update user from non-galaxy namespaces.
- Prevent the leaking of environment variables for minio and aws.

(cherry picked from commit 5f3cece)
  • Loading branch information
NamanJain8 authored May 5, 2021
1 parent 7e2e860 commit eeb7bea
Show file tree
Hide file tree
Showing 11 changed files with 403 additions and 61 deletions.
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("ludicrous", worker.LudicrousDefaults, z.NewSuperFlagHelp(worker.LudicrousDefaults).
Expand Down Expand Up @@ -631,7 +635,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 @@ -725,6 +728,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 {

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(), "task failed")

// 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

0 comments on commit eeb7bea

Please sign in to comment.