github.com/unigraph-dev/dgraph@v1.1.1-0.20200923154953-8b52b426f765/ee/acl/acl_test.go (about)

     1  // +build !oss
     2  
     3  /*
     4   * Copyright 2018 Dgraph Labs, Inc. and Contributors
     5   *
     6   * Licensed under the Dgraph Community License (the "License"); you
     7   * may not use this file except in compliance with the License. You
     8   * may obtain a copy of the License at
     9   *
    10   *     https://github.com/dgraph-io/dgraph/blob/master/licenses/DCL.txt
    11   */
    12  
    13  package acl
    14  
    15  import (
    16  	"context"
    17  	"fmt"
    18  	"os"
    19  	"os/exec"
    20  	"path/filepath"
    21  	"strconv"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/dgraph-io/dgo"
    26  	"github.com/dgraph-io/dgo/protos/api"
    27  	"github.com/dgraph-io/dgraph/testutil"
    28  	"github.com/dgraph-io/dgraph/x"
    29  	"github.com/golang/glog"
    30  	"github.com/stretchr/testify/require"
    31  )
    32  
    33  var (
    34  	userid         = "alice"
    35  	userpassword   = "simplepassword"
    36  	dgraphEndpoint = testutil.SockAddr
    37  )
    38  
    39  func checkOutput(t *testing.T, cmd *exec.Cmd, shouldFail bool) string {
    40  	out, err := cmd.CombinedOutput()
    41  	if (!shouldFail && err != nil) || (shouldFail && err == nil) {
    42  		t.Errorf("Error output from command:%v", string(out))
    43  		t.Fatal(err)
    44  	}
    45  
    46  	return string(out)
    47  }
    48  
    49  func TestCreateAndDeleteUsers(t *testing.T) {
    50  	// clean up the user to allow repeated running of this test
    51  	cleanUserCmd := exec.Command("dgraph", "acl", "del", "-a", dgraphEndpoint,
    52  		"-u", userid, "-x", "password")
    53  	cleanUserCmd.Run()
    54  	glog.Infof("cleaned up db user state")
    55  
    56  	createUserCmd1 := exec.Command("dgraph", "acl", "add", "-a", dgraphEndpoint, "-u", userid,
    57  		"-p", userpassword, "-x", "password")
    58  	checkOutput(t, createUserCmd1, false)
    59  
    60  	createUserCmd2 := exec.Command("dgraph", "acl", "add", "-a", dgraphEndpoint, "-u", userid,
    61  		"-p", userpassword, "-x", "password")
    62  	// create the user again should fail
    63  	checkOutput(t, createUserCmd2, true)
    64  
    65  	// delete the user
    66  	deleteUserCmd := exec.Command("dgraph", "acl", "del", "-a", dgraphEndpoint, "-u", userid,
    67  		"-x", "password")
    68  	checkOutput(t, deleteUserCmd, false)
    69  
    70  	// now we should be able to create the user again
    71  	createUserCmd3 := exec.Command("dgraph", "acl", "add", "-a", dgraphEndpoint, "-u", userid,
    72  		"-p", userpassword, "-x", "password")
    73  	checkOutput(t, createUserCmd3, false)
    74  }
    75  
    76  func resetUser(t *testing.T) {
    77  	// delete and recreate the user to ensure a clean state
    78  	deleteUserCmd := exec.Command("dgraph", "acl", "del", "-a", dgraphEndpoint,
    79  		"-u", userid, "-x", "password")
    80  	deleteUserCmd.Run()
    81  	glog.Infof("deleted user")
    82  
    83  	createUserCmd := exec.Command("dgraph", "acl", "add", "-a", dgraphEndpoint, "-u",
    84  		userid, "-p", userpassword, "-x", "password")
    85  	checkOutput(t, createUserCmd, false)
    86  	glog.Infof("created user")
    87  }
    88  
    89  func TestReservedPredicates(t *testing.T) {
    90  	// This test uses the groot account to ensure that reserved predicates
    91  	// cannot be altered even if the permissions allow it.
    92  	ctx := context.Background()
    93  
    94  	dg1 := testutil.DgraphClientWithGroot(testutil.SockAddr)
    95  	alterReservedPredicates(t, dg1)
    96  
    97  	dg2 := testutil.DgraphClientWithGroot(testutil.SockAddr)
    98  	if err := dg2.Login(ctx, x.GrootId, "password"); err != nil {
    99  		t.Fatalf("unable to login using the groot account:%v", err)
   100  	}
   101  	alterReservedPredicates(t, dg2)
   102  }
   103  
   104  func TestAuthorization(t *testing.T) {
   105  	if testing.Short() {
   106  		t.Skip("skipping because -short=true")
   107  	}
   108  
   109  	glog.Infof("testing with port 9180")
   110  	dg1 := testutil.DgraphClientWithGroot(testutil.SockAddr)
   111  	testAuthorization(t, dg1)
   112  	glog.Infof("done")
   113  
   114  	glog.Infof("testing with port 9182")
   115  	dg2 := testutil.DgraphClientWithGroot(":9182")
   116  	testAuthorization(t, dg2)
   117  	glog.Infof("done")
   118  }
   119  
   120  func testAuthorization(t *testing.T, dg *dgo.Dgraph) {
   121  	createAccountAndData(t, dg)
   122  	ctx := context.Background()
   123  	if err := dg.Login(ctx, userid, userpassword); err != nil {
   124  		t.Fatalf("unable to login using the account %v", userid)
   125  	}
   126  
   127  	// initially the query, mutate and alter operations should all succeed
   128  	// when there are no rules defined on the predicates (the fail open approach)
   129  	queryPredicateWithUserAccount(t, dg, false)
   130  	mutatePredicateWithUserAccount(t, dg, false)
   131  	alterPredicateWithUserAccount(t, dg, false)
   132  	createGroupAndAcls(t, unusedGroup, false)
   133  	// wait for 6 seconds to ensure the new acl have reached all acl caches
   134  	glog.Infof("Sleeping for 6 seconds for acl caches to be refreshed")
   135  	time.Sleep(6 * time.Second)
   136  
   137  	// now all these operations should fail since there are rules defined on the unusedGroup
   138  	queryPredicateWithUserAccount(t, dg, true)
   139  	mutatePredicateWithUserAccount(t, dg, true)
   140  	alterPredicateWithUserAccount(t, dg, true)
   141  	// create the dev group and add the user to it
   142  	createGroupAndAcls(t, devGroup, true)
   143  
   144  	// wait for 6 seconds to ensure the new acl have reached all acl caches
   145  	glog.Infof("Sleeping for 6 seconds for acl caches to be refreshed")
   146  	time.Sleep(6 * time.Second)
   147  
   148  	// now the operations should succeed again through the devGroup
   149  	queryPredicateWithUserAccount(t, dg, false)
   150  	// sleep long enough (10s per the docker-compose.yml)
   151  	// for the accessJwt to expire in order to test auto login through refresh jwt
   152  	glog.Infof("Sleeping for 4 seconds for accessJwt to expire")
   153  	time.Sleep(4 * time.Second)
   154  	mutatePredicateWithUserAccount(t, dg, false)
   155  	glog.Infof("Sleeping for 4 seconds for accessJwt to expire")
   156  	time.Sleep(4 * time.Second)
   157  	alterPredicateWithUserAccount(t, dg, false)
   158  }
   159  
   160  var predicateToRead = "predicate_to_read"
   161  var queryAttr = "name"
   162  var predicateToWrite = "predicate_to_write"
   163  var predicateToAlter = "predicate_to_alter"
   164  var devGroup = "dev"
   165  var unusedGroup = "unusedGroup"
   166  var rootDir = filepath.Join(os.TempDir(), "acl_test")
   167  var query = fmt.Sprintf(`
   168  	{
   169  		q(func: eq(%s, "SF")) {
   170  			%s
   171  		}
   172  	}`, predicateToRead, queryAttr)
   173  
   174  func alterReservedPredicates(t *testing.T, dg *dgo.Dgraph) {
   175  	ctx := context.Background()
   176  
   177  	// Test that alter requests are allowed if the new update is the same as
   178  	// the initial update for a reserved predicate.
   179  	err := dg.Alter(ctx, &api.Operation{
   180  		Schema: "dgraph.xid: string @index(exact) @upsert .",
   181  	})
   182  	require.NoError(t, err)
   183  
   184  	err = dg.Alter(ctx, &api.Operation{
   185  		Schema: "dgraph.xid: int .",
   186  	})
   187  	require.Error(t, err)
   188  	require.Contains(t, err.Error(),
   189  		"predicate dgraph.xid is reserved and is not allowed to be modified")
   190  
   191  	err = dg.Alter(ctx, &api.Operation{
   192  		DropAttr: "dgraph.xid",
   193  	})
   194  	require.Error(t, err)
   195  	require.Contains(t, err.Error(),
   196  		"predicate dgraph.xid is reserved and is not allowed to be dropped")
   197  
   198  	// Test that reserved predicates act as case-insensitive.
   199  	err = dg.Alter(ctx, &api.Operation{
   200  		Schema: "dgraph.XID: int .",
   201  	})
   202  	require.Error(t, err)
   203  	require.Contains(t, err.Error(),
   204  		"predicate dgraph.XID is reserved and is not allowed to be modified")
   205  }
   206  
   207  func queryPredicateWithUserAccount(t *testing.T, dg *dgo.Dgraph, shouldFail bool) {
   208  	// login with alice's account
   209  	ctx := context.Background()
   210  	txn := dg.NewTxn()
   211  	_, err := txn.Query(ctx, query)
   212  
   213  	if shouldFail {
   214  		require.Error(t, err, "the query should have failed")
   215  	} else {
   216  		require.NoError(t, err, "the query should have succeeded")
   217  	}
   218  }
   219  
   220  func mutatePredicateWithUserAccount(t *testing.T, dg *dgo.Dgraph, shouldFail bool) {
   221  	ctx := context.Background()
   222  	txn := dg.NewTxn()
   223  	_, err := txn.Mutate(ctx, &api.Mutation{
   224  		CommitNow: true,
   225  		SetNquads: []byte(fmt.Sprintf(`_:a <%s>  "string" .`, predicateToWrite)),
   226  	})
   227  
   228  	if shouldFail {
   229  		require.Error(t, err, "the mutation should have failed")
   230  	} else {
   231  		require.NoError(t, err, "the mutation should have succeeded")
   232  	}
   233  }
   234  
   235  func alterPredicateWithUserAccount(t *testing.T, dg *dgo.Dgraph, shouldFail bool) {
   236  	ctx := context.Background()
   237  	err := dg.Alter(ctx, &api.Operation{
   238  		Schema: fmt.Sprintf(`%s: int .`, predicateToAlter),
   239  	})
   240  	if shouldFail {
   241  		require.Error(t, err, "the alter should have failed")
   242  	} else {
   243  		require.NoError(t, err, "the alter should have succeeded")
   244  	}
   245  }
   246  
   247  func createAccountAndData(t *testing.T, dg *dgo.Dgraph) {
   248  	// use the groot account to clean the database
   249  	ctx := context.Background()
   250  	if err := dg.Login(ctx, x.GrootId, "password"); err != nil {
   251  		t.Fatalf("unable to login using the groot account:%v", err)
   252  	}
   253  	op := api.Operation{
   254  		DropAll: true,
   255  	}
   256  	if err := dg.Alter(ctx, &op); err != nil {
   257  		t.Fatalf("Unable to cleanup db:%v", err)
   258  	}
   259  	require.NoError(t, dg.Alter(ctx, &api.Operation{
   260  		Schema: fmt.Sprintf(`%s: string @index(exact) .`, predicateToRead),
   261  	}))
   262  	// wait for 6 seconds to ensure the new acl have reached all acl caches
   263  	glog.Infof("Sleeping for 6 seconds for acl caches to be refreshed")
   264  	time.Sleep(6 * time.Second)
   265  
   266  	// create some data, e.g. user with name alice
   267  	resetUser(t)
   268  
   269  	txn := dg.NewTxn()
   270  	_, err := txn.Mutate(ctx, &api.Mutation{
   271  		SetNquads: []byte(fmt.Sprintf("_:a <%s> \"SF\" .", predicateToRead)),
   272  	})
   273  	require.NoError(t, err)
   274  	require.NoError(t, txn.Commit(ctx))
   275  }
   276  
   277  func createGroupAndAcls(t *testing.T, group string, addUserToGroup bool) {
   278  	// create a new group
   279  	createGroupCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"),
   280  		"acl", "add",
   281  		"-a", dgraphEndpoint,
   282  		"-g", group, "-x", "password")
   283  	if errOutput, err := createGroupCmd.CombinedOutput(); err != nil {
   284  		t.Fatalf("Unable to create group: %v", string(errOutput))
   285  	}
   286  
   287  	// add the user to the group
   288  	if addUserToGroup {
   289  		addUserToGroupCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"),
   290  			"acl", "mod",
   291  			"-a", dgraphEndpoint,
   292  			"-u", userid, "--group_list", group, "-x", "password")
   293  		if errOutput, err := addUserToGroupCmd.CombinedOutput(); err != nil {
   294  			t.Fatalf("Unable to add user %s to group %s:%v", userid, group, string(errOutput))
   295  		}
   296  	}
   297  
   298  	// add READ permission on the predicateToRead to the group
   299  	addReadPermCmd1 := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"),
   300  		"acl", "mod",
   301  		"-a", dgraphEndpoint,
   302  		"-g", group, "-p", predicateToRead, "-m", strconv.Itoa(int(Read.Code)), "-x",
   303  		"password")
   304  	if errOutput, err := addReadPermCmd1.CombinedOutput(); err != nil {
   305  		t.Fatalf("Unable to add READ permission on %s to group %s: %v",
   306  			predicateToRead, group, string(errOutput))
   307  	}
   308  
   309  	// also add read permission to the attribute queryAttr, which is used inside the query block
   310  	addReadPermCmd2 := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"),
   311  		"acl", "mod",
   312  		"-a", dgraphEndpoint,
   313  		"-g", group, "-p", queryAttr, "-m", strconv.Itoa(int(Read.Code)), "-x",
   314  		"password")
   315  	if errOutput, err := addReadPermCmd2.CombinedOutput(); err != nil {
   316  		t.Fatalf("Unable to add READ permission on %s to group %s: %v", queryAttr, group,
   317  			string(errOutput))
   318  	}
   319  
   320  	// add WRITE permission on the predicateToWrite
   321  	addWritePermCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"),
   322  		"acl", "mod",
   323  		"-a", dgraphEndpoint,
   324  		"-g", group, "-p", predicateToWrite, "-m", strconv.Itoa(int(Write.Code)), "-x",
   325  		"password")
   326  	if errOutput, err := addWritePermCmd.CombinedOutput(); err != nil {
   327  		t.Fatalf("Unable to add permission on %s to group %s: %v", predicateToWrite, group,
   328  			string(errOutput))
   329  	}
   330  
   331  	// add MODIFY permission on the predicateToAlter
   332  	addModifyPermCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"),
   333  		"acl", "mod",
   334  		"-a", dgraphEndpoint,
   335  		"-g", group, "-p", predicateToAlter, "-m", strconv.Itoa(int(Modify.Code)), "-x",
   336  		"password")
   337  	if errOutput, err := addModifyPermCmd.CombinedOutput(); err != nil {
   338  		t.Fatalf("Unable to add permission on %s to group %s: %v", predicateToAlter, group,
   339  			string(errOutput))
   340  	}
   341  }
   342  
   343  func TestPredicateRegex(t *testing.T) {
   344  	if testing.Short() {
   345  		t.Skip("skipping because -short=true")
   346  	}
   347  
   348  	glog.Infof("testing with port 9180")
   349  	dg := testutil.DgraphClientWithGroot(testutil.SockAddr)
   350  	createAccountAndData(t, dg)
   351  	ctx := context.Background()
   352  	err := dg.Login(ctx, userid, userpassword)
   353  	require.NoError(t, err, "Logging in with the current password should have succeeded")
   354  
   355  	// the operations should be allowed when no rule is defined (the fail open approach)
   356  	queryPredicateWithUserAccount(t, dg, false)
   357  	mutatePredicateWithUserAccount(t, dg, false)
   358  	alterPredicateWithUserAccount(t, dg, false)
   359  	createGroupAndAcls(t, unusedGroup, false)
   360  
   361  	// wait for 6 seconds to ensure the new acl have reached all acl caches
   362  	glog.Infof("Sleeping for 6 seconds for acl caches to be refreshed")
   363  	time.Sleep(6 * time.Second)
   364  	// the operations should all fail when there is a rule defined, but the current user is not
   365  	// allowed
   366  	queryPredicateWithUserAccount(t, dg, true)
   367  	mutatePredicateWithUserAccount(t, dg, true)
   368  	alterPredicateWithUserAccount(t, dg, true)
   369  
   370  	// create a new group
   371  	createGroupCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"),
   372  		"acl", "add",
   373  		"-a", dgraphEndpoint,
   374  		"-g", devGroup, "-x", "password")
   375  	if errOutput, err := createGroupCmd.CombinedOutput(); err != nil {
   376  		t.Fatalf("Unable to create group:%v", string(errOutput))
   377  	}
   378  
   379  	// add the user to the group
   380  	addUserToGroupCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"),
   381  		"acl", "mod",
   382  		"-a", dgraphEndpoint,
   383  		"-u", userid, "--group_list", devGroup, "-x", "password")
   384  	if errOutput, err := addUserToGroupCmd.CombinedOutput(); err != nil {
   385  		t.Fatalf("Unable to add user %s to group %s:%v", userid, devGroup, string(errOutput))
   386  	}
   387  
   388  	addReadToNameCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"),
   389  		"acl", "mod",
   390  		"-a", dgraphEndpoint,
   391  		"-g", devGroup, "--pred", "name", "-m", strconv.Itoa(int(Read.Code)|int(Write.Code)),
   392  		"-x",
   393  		"password")
   394  	if errOutput, err := addReadToNameCmd.CombinedOutput(); err != nil {
   395  		t.Fatalf("Unable to add READ permission on %s to group %s:%v",
   396  			"name", devGroup, string(errOutput))
   397  	}
   398  
   399  	// add READ+WRITE permission on the regex ^predicate_to(.*)$ pred filter to the group
   400  	predRegex := "^predicate_to(.*)$"
   401  	addReadWriteToRegexPermCmd := exec.Command(os.ExpandEnv("$GOPATH/bin/dgraph"),
   402  		"acl", "mod",
   403  		"-a", dgraphEndpoint,
   404  		"-g", devGroup, "-P", predRegex, "-m",
   405  		strconv.Itoa(int(Read.Code)|int(Write.Code)), "-x", "password")
   406  	if errOutput, err := addReadWriteToRegexPermCmd.CombinedOutput(); err != nil {
   407  		t.Fatalf("Unable to add READ+WRITE permission on %s to group %s:%v",
   408  			predRegex, devGroup, string(errOutput))
   409  	}
   410  
   411  	glog.Infof("Sleeping for 6 seconds for acl caches to be refreshed")
   412  	time.Sleep(6 * time.Second)
   413  	queryPredicateWithUserAccount(t, dg, false)
   414  	mutatePredicateWithUserAccount(t, dg, false)
   415  	// the alter operation should still fail since the regex pred does not have the Modify
   416  	// permission
   417  	alterPredicateWithUserAccount(t, dg, true)
   418  }
   419  
   420  func TestAccessWithoutLoggingIn(t *testing.T) {
   421  	dg := testutil.DgraphClientWithGroot(testutil.SockAddr)
   422  
   423  	createAccountAndData(t, dg)
   424  	// without logging in,
   425  	// the anonymous user should be evaluated as if the user does not belong to any group,
   426  	// and access should be granted if there is no ACL rule defined for a predicate (fail open)
   427  	queryPredicateWithUserAccount(t, dg, false)
   428  	mutatePredicateWithUserAccount(t, dg, false)
   429  	alterPredicateWithUserAccount(t, dg, false)
   430  }