github.com/unigraph-dev/dgraph@v1.1.1-0.20200923154953-8b52b426f765/ee/acl/acl.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  	"encoding/json"
    18  	"fmt"
    19  	"regexp"
    20  	"strings"
    21  	"time"
    22  
    23  	"github.com/dgraph-io/dgo"
    24  	"github.com/dgraph-io/dgo/protos/api"
    25  	"github.com/dgraph-io/dgraph/x"
    26  	"github.com/golang/glog"
    27  	"github.com/pkg/errors"
    28  	"github.com/spf13/viper"
    29  )
    30  
    31  func getUserAndGroup(conf *viper.Viper) (userId string, groupId string, err error) {
    32  	userId = conf.GetString("user")
    33  	groupId = conf.GetString("group")
    34  	if (len(userId) == 0 && len(groupId) == 0) ||
    35  		(len(userId) != 0 && len(groupId) != 0) {
    36  		return "", "", errors.Errorf("one of the --user or --group must be specified, but not both")
    37  	}
    38  	return userId, groupId, nil
    39  }
    40  
    41  func checkForbiddenOpts(conf *viper.Viper, forbiddenOpts []string) error {
    42  	for _, opt := range forbiddenOpts {
    43  		var isSet bool
    44  		switch conf.Get(opt).(type) {
    45  		case string:
    46  			if opt == "group_list" {
    47  				// handle group_list specially since the default value is not an empty string
    48  				isSet = conf.GetString(opt) != defaultGroupList
    49  			} else {
    50  				isSet = len(conf.GetString(opt)) > 0
    51  			}
    52  		case int:
    53  			isSet = conf.GetInt(opt) > 0
    54  		case bool:
    55  			isSet = conf.GetBool(opt)
    56  		default:
    57  			return errors.Errorf("unexpected option type for %s", opt)
    58  		}
    59  		if isSet {
    60  			return errors.Errorf("the option --%s should not be set", opt)
    61  		}
    62  	}
    63  
    64  	return nil
    65  }
    66  
    67  func add(conf *viper.Viper) error {
    68  	userId, groupId, err := getUserAndGroup(conf)
    69  	if err != nil {
    70  		return err
    71  	}
    72  	password := conf.GetString("password")
    73  	if len(userId) != 0 {
    74  		return userAdd(conf, userId, password)
    75  	}
    76  
    77  	// if we are adding a group, then the password should not have been set
    78  	if err := checkForbiddenOpts(conf, []string{"password"}); err != nil {
    79  		return err
    80  	}
    81  	return groupAdd(conf, groupId)
    82  }
    83  
    84  func userAdd(conf *viper.Viper, userid string, password string) error {
    85  	dc, cancel, err := getClientWithAdminCtx(conf)
    86  	if err != nil {
    87  		return errors.Wrapf(err, "unable to get admin context")
    88  	}
    89  	defer cancel()
    90  
    91  	if len(password) == 0 {
    92  		var err error
    93  		password, err = x.AskUserPassword(userid, "New", 2)
    94  		if err != nil {
    95  			return err
    96  		}
    97  	}
    98  
    99  	ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second)
   100  	defer ctxCancel()
   101  	txn := dc.NewTxn()
   102  	defer func() {
   103  		if err := txn.Discard(ctx); err != nil {
   104  			glog.Errorf("Unable to discard transaction:%v", err)
   105  		}
   106  	}()
   107  
   108  	user, err := queryUser(ctx, txn, userid)
   109  	if err != nil {
   110  		return errors.Wrapf(err, "while querying user")
   111  	}
   112  	if user != nil {
   113  		return errors.Errorf("unable to create user because of conflict: %v", userid)
   114  	}
   115  
   116  	createUserNQuads := CreateUserNQuads(userid, password)
   117  
   118  	mu := &api.Mutation{
   119  		CommitNow: true,
   120  		Set:       createUserNQuads,
   121  	}
   122  
   123  	if _, err := txn.Mutate(ctx, mu); err != nil {
   124  		return errors.Wrapf(err, "unable to create user")
   125  	}
   126  
   127  	fmt.Printf("Created new user with id %v\n", userid)
   128  	return nil
   129  }
   130  
   131  func groupAdd(conf *viper.Viper, groupId string) error {
   132  	dc, cancel, err := getClientWithAdminCtx(conf)
   133  	if err != nil {
   134  		return errors.Wrapf(err, "unable to get admin context")
   135  	}
   136  	defer cancel()
   137  
   138  	ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second)
   139  	defer ctxCancel()
   140  	txn := dc.NewTxn()
   141  	defer func() {
   142  		if err := txn.Discard(ctx); err != nil {
   143  			fmt.Printf("Unable to discard transaction: %v\n", err)
   144  		}
   145  	}()
   146  
   147  	group, err := queryGroup(ctx, txn, groupId)
   148  	if err != nil {
   149  		return errors.Wrapf(err, "while querying group")
   150  	}
   151  	if group != nil {
   152  		return errors.Errorf("group %q already exists", groupId)
   153  	}
   154  
   155  	createGroupNQuads := []*api.NQuad{
   156  		{
   157  			Subject:     "_:newgroup",
   158  			Predicate:   "dgraph.xid",
   159  			ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: groupId}},
   160  		},
   161  		{
   162  			Subject:     "_:newgroup",
   163  			Predicate:   "dgraph.type",
   164  			ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: "Group"}},
   165  		},
   166  	}
   167  
   168  	mu := &api.Mutation{
   169  		CommitNow: true,
   170  		Set:       createGroupNQuads,
   171  	}
   172  	if _, err = txn.Mutate(ctx, mu); err != nil {
   173  		return errors.Wrapf(err, "unable to create group")
   174  	}
   175  
   176  	fmt.Printf("Created new group with id %v\n", groupId)
   177  	return nil
   178  }
   179  
   180  func del(conf *viper.Viper) error {
   181  	userId, groupId, err := getUserAndGroup(conf)
   182  	if err != nil {
   183  		return err
   184  	}
   185  	if len(userId) != 0 {
   186  		return userOrGroupDel(conf, userId,
   187  			func(ctx context.Context, txn *dgo.Txn, userId string) (AclEntity, error) {
   188  				user, err := queryUser(ctx, txn, userId)
   189  				return user, err
   190  			})
   191  	}
   192  	return userOrGroupDel(conf, groupId,
   193  		func(ctx context.Context, txn *dgo.Txn, groupId string) (AclEntity, error) {
   194  			group, err := queryGroup(ctx, txn, groupId)
   195  			return group, err
   196  		})
   197  }
   198  
   199  // AclEntity is an interface that must be met by all the types of entities (i.e users, groups)
   200  // in the ACL system.
   201  type AclEntity interface {
   202  	// GetUid returns the UID of the entity.
   203  	// The implementation of GetUid must check the case that the entity is nil
   204  	// and return an empty string accordingly.
   205  	GetUid() string
   206  }
   207  
   208  func userOrGroupDel(conf *viper.Viper, userOrGroupId string,
   209  	queryFn func(context.Context, *dgo.Txn, string) (AclEntity, error)) error {
   210  	dc, cancel, err := getClientWithAdminCtx(conf)
   211  	if err != nil {
   212  		return errors.Wrapf(err, "unable to get admin context")
   213  	}
   214  	defer cancel()
   215  
   216  	ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second)
   217  	defer ctxCancel()
   218  	txn := dc.NewTxn()
   219  	defer func() {
   220  		if err := txn.Discard(ctx); err != nil {
   221  			glog.Errorf("Unable to discard transaction:%v", err)
   222  		}
   223  	}()
   224  
   225  	entity, err := queryFn(ctx, txn, userOrGroupId)
   226  	if err != nil {
   227  		return err
   228  	}
   229  	if len(entity.GetUid()) == 0 {
   230  		return errors.Errorf("unable to delete %q since it does not exist",
   231  			userOrGroupId)
   232  	}
   233  
   234  	deleteNQuads := []*api.NQuad{
   235  		{
   236  			Subject:     entity.GetUid(),
   237  			Predicate:   x.Star,
   238  			ObjectValue: &api.Value{Val: &api.Value_DefaultVal{DefaultVal: x.Star}},
   239  		}}
   240  
   241  	mu := &api.Mutation{
   242  		CommitNow: true,
   243  		Del:       deleteNQuads,
   244  	}
   245  
   246  	if _, err = txn.Mutate(ctx, mu); err != nil {
   247  		return errors.Wrapf(err, "unable to delete %q", userOrGroupId)
   248  	}
   249  
   250  	fmt.Printf("Successfully deleted %q\n", userOrGroupId)
   251  	return nil
   252  }
   253  
   254  func mod(conf *viper.Viper) error {
   255  	userId, _, err := getUserAndGroup(conf)
   256  	if err != nil {
   257  		return err
   258  	}
   259  
   260  	if len(userId) != 0 {
   261  		// when modifying the user, some group options are forbidden
   262  		if err := checkForbiddenOpts(conf, []string{"pred", "pred_regex", "perm"}); err != nil {
   263  			return err
   264  		}
   265  
   266  		newPassword := conf.GetBool("new_password")
   267  		groupList := conf.GetString("group_list")
   268  		if (newPassword && groupList != defaultGroupList) ||
   269  			(!newPassword && groupList == defaultGroupList) {
   270  			return errors.Errorf(
   271  				"one of --new_password or --group_list must be provided, but not both")
   272  		}
   273  
   274  		if newPassword {
   275  			return changePassword(conf, userId)
   276  		}
   277  
   278  		return userMod(conf, userId, groupList)
   279  	}
   280  
   281  	// when modifying the group, some user options are forbidden
   282  	if err := checkForbiddenOpts(conf, []string{"group_list", "new_password"}); err != nil {
   283  		return err
   284  	}
   285  	return chMod(conf)
   286  }
   287  
   288  // changePassword changes a user's password
   289  func changePassword(conf *viper.Viper, userId string) error {
   290  	// 1. get the dgo client with appropriate access JWT
   291  	dc, cancel, err := getClientWithAdminCtx(conf)
   292  	if err != nil {
   293  		return errors.Wrapf(err, "unable to get dgo client")
   294  	}
   295  	defer cancel()
   296  
   297  	// 2. get the new password
   298  	newPassword, err := x.AskUserPassword(userId, "New", 2)
   299  	if err != nil {
   300  		return err
   301  	}
   302  
   303  	ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second)
   304  	defer ctxCancel()
   305  	txn := dc.NewTxn()
   306  	defer func() {
   307  		if err := txn.Discard(ctx); err != nil {
   308  			glog.Errorf("Unable to discard transaction:%v", err)
   309  		}
   310  	}()
   311  
   312  	// 3. query the user's current uid
   313  	user, err := queryUser(ctx, txn, userId)
   314  	if err != nil {
   315  		return errors.Wrapf(err, "while querying user")
   316  	}
   317  	if user == nil {
   318  		return errors.Errorf("user %q does not exist", userId)
   319  	}
   320  
   321  	// 4. mutate the user's password
   322  	chPdNQuads := []*api.NQuad{
   323  		{
   324  			Subject:     user.Uid,
   325  			Predicate:   "dgraph.password",
   326  			ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: newPassword}},
   327  		}}
   328  	mu := &api.Mutation{
   329  		CommitNow: true,
   330  		Set:       chPdNQuads,
   331  	}
   332  	if _, err := txn.Mutate(ctx, mu); err != nil {
   333  		return errors.Wrapf(err, "unable to change password for user %v", userId)
   334  	}
   335  	fmt.Printf("Successfully changed password for %v\n", userId)
   336  	return nil
   337  }
   338  
   339  func userMod(conf *viper.Viper, userId string, groups string) error {
   340  	dc, cancel, err := getClientWithAdminCtx(conf)
   341  	if err != nil {
   342  		return errors.Wrapf(err, "unable to get admin context")
   343  	}
   344  	defer cancel()
   345  
   346  	ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second)
   347  	defer ctxCancel()
   348  	txn := dc.NewTxn()
   349  	defer func() {
   350  		if err := txn.Discard(ctx); err != nil {
   351  			fmt.Printf("Unable to discard transaction: %v\n", err)
   352  		}
   353  	}()
   354  
   355  	user, err := queryUser(ctx, txn, userId)
   356  	if err != nil {
   357  		return errors.Wrapf(err, "while querying user")
   358  	}
   359  	if user == nil {
   360  		return errors.Errorf("user %q does not exist", userId)
   361  	}
   362  
   363  	targetGroupsMap := make(map[string]struct{})
   364  	if len(groups) > 0 {
   365  		for _, g := range strings.Split(groups, ",") {
   366  			targetGroupsMap[g] = struct{}{}
   367  		}
   368  	}
   369  
   370  	existingGroupsMap := make(map[string]struct{})
   371  	for _, g := range user.Groups {
   372  		existingGroupsMap[g.GroupID] = struct{}{}
   373  	}
   374  	newGroups, groupsToBeDeleted := x.Diff(targetGroupsMap, existingGroupsMap)
   375  
   376  	mu := &api.Mutation{
   377  		CommitNow: true,
   378  		Set:       []*api.NQuad{},
   379  		Del:       []*api.NQuad{},
   380  	}
   381  
   382  	for _, g := range newGroups {
   383  		fmt.Printf("Adding user %v to group %v\n", userId, g)
   384  		nquad, err := getUserModNQuad(ctx, txn, user.Uid, g)
   385  		if err != nil {
   386  			return err
   387  		}
   388  		mu.Set = append(mu.Set, nquad)
   389  	}
   390  
   391  	for _, g := range groupsToBeDeleted {
   392  		fmt.Printf("Deleting user %v from group %v\n", userId, g)
   393  		nquad, err := getUserModNQuad(ctx, txn, user.Uid, g)
   394  		if err != nil {
   395  			return err
   396  		}
   397  		mu.Del = append(mu.Del, nquad)
   398  	}
   399  	if len(mu.Del) == 0 && len(mu.Set) == 0 {
   400  		fmt.Printf("Nothing needs to be changed for the groups of user: %v\n", userId)
   401  		return nil
   402  	}
   403  
   404  	if _, err := txn.Mutate(ctx, mu); err != nil {
   405  		return errors.Wrapf(err, "while mutating the group")
   406  	}
   407  	fmt.Printf("Successfully modified groups for user %v.\n", userId)
   408  	fmt.Println("The latest info is:")
   409  	return queryAndPrintUser(ctx, dc.NewReadOnlyTxn(), userId)
   410  }
   411  
   412  func chMod(conf *viper.Viper) error {
   413  	groupId := conf.GetString("group")
   414  	predicate := conf.GetString("pred")
   415  	predRegex := conf.GetString("pred_regex")
   416  	perm := conf.GetInt("perm")
   417  	switch {
   418  	case len(groupId) == 0:
   419  		return errors.Errorf("the groupid must not be empty")
   420  	case len(predicate) > 0 && len(predRegex) > 0:
   421  		return errors.Errorf("one of --pred or --pred_regex must be specified, but not both")
   422  	case len(predicate) == 0 && len(predRegex) == 0:
   423  		return errors.Errorf("one of --pred or --pred_regex must be specified, but not both")
   424  	case perm > 7:
   425  		return errors.Errorf("the perm value must be less than or equal to 7, "+
   426  			"the provided value is %d", perm)
   427  	case len(predRegex) > 0:
   428  		// make sure the predRegex can be compiled as a regex
   429  		if _, err := regexp.Compile(predRegex); err != nil {
   430  			return errors.Wrapf(err, "unable to compile %v as a regular expression",
   431  				predRegex)
   432  		}
   433  	}
   434  
   435  	dc, cancel, err := getClientWithAdminCtx(conf)
   436  	if err != nil {
   437  		return errors.Wrapf(err, "unable to get admin context")
   438  	}
   439  	defer cancel()
   440  
   441  	ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second)
   442  	defer ctxCancel()
   443  	txn := dc.NewTxn()
   444  	defer func() {
   445  		if err := txn.Discard(ctx); err != nil {
   446  			fmt.Printf("Unable to discard transaction: %v\n", err)
   447  		}
   448  	}()
   449  
   450  	group, err := queryGroup(ctx, txn, groupId, "dgraph.group.acl")
   451  	if err != nil {
   452  		return errors.Wrapf(err, "while querying group")
   453  	}
   454  	if group == nil || len(group.Uid) == 0 {
   455  		return errors.Errorf("unable to change permission for group because it does not exist: %v",
   456  			groupId)
   457  	}
   458  
   459  	var currentAcls []Acl
   460  	if len(group.Acls) != 0 {
   461  		if err := json.Unmarshal([]byte(group.Acls), &currentAcls); err != nil {
   462  			return errors.Wrapf(err, "unable to unmarshal the acls associated with the group %v",
   463  				groupId)
   464  		}
   465  	}
   466  
   467  	var newAcl Acl
   468  	if len(predicate) > 0 {
   469  		newAcl = Acl{
   470  			Predicate: predicate,
   471  			Perm:      int32(perm),
   472  		}
   473  	} else {
   474  		newAcl = Acl{
   475  			Regex: predRegex,
   476  			Perm:  int32(perm),
   477  		}
   478  	}
   479  	newAcls, updated := updateAcl(currentAcls, newAcl)
   480  	if !updated {
   481  		fmt.Printf("Nothing needs to be changed for the permission of group: %v\n", groupId)
   482  		return nil
   483  	}
   484  
   485  	newAclBytes, err := json.Marshal(newAcls)
   486  	if err != nil {
   487  		return errors.Wrapf(err, "unable to marshal the updated acls")
   488  	}
   489  
   490  	chModNQuads := &api.NQuad{
   491  		Subject:     group.Uid,
   492  		Predicate:   "dgraph.group.acl",
   493  		ObjectValue: &api.Value{Val: &api.Value_BytesVal{BytesVal: newAclBytes}},
   494  	}
   495  	mu := &api.Mutation{
   496  		CommitNow: true,
   497  		Set:       []*api.NQuad{chModNQuads},
   498  	}
   499  
   500  	if _, err = txn.Mutate(ctx, mu); err != nil {
   501  		return errors.Wrapf(err, "unable to change mutations for the group %v on predicate %v",
   502  			groupId, predicate)
   503  	}
   504  	fmt.Printf("Successfully changed permission for group %v on predicate %v to %v\n",
   505  		groupId, predicate, perm)
   506  	fmt.Println("The latest info is:")
   507  	return queryAndPrintGroup(ctx, dc.NewReadOnlyTxn(), groupId)
   508  }
   509  
   510  func queryUser(ctx context.Context, txn *dgo.Txn, userid string) (user *User, err error) {
   511  	query := `
   512      query search($userid: string){
   513        user(func: eq(dgraph.xid, $userid)) @filter(type(User)) {
   514  	    uid
   515          dgraph.xid
   516          dgraph.user.group {
   517            uid
   518            dgraph.xid
   519          }
   520        }
   521      }`
   522  
   523  	queryVars := make(map[string]string)
   524  	queryVars["$userid"] = userid
   525  
   526  	queryResp, err := txn.QueryWithVars(ctx, query, queryVars)
   527  	if err != nil {
   528  		return nil, errors.Wrapf(err, "hile query user with id %s", userid)
   529  	}
   530  	user, err = UnmarshalUser(queryResp, "user")
   531  	if err != nil {
   532  		return nil, err
   533  	}
   534  	return user, nil
   535  }
   536  
   537  func getUserModNQuad(ctx context.Context, txn *dgo.Txn, userId string,
   538  	groupId string) (*api.NQuad, error) {
   539  	group, err := queryGroup(ctx, txn, groupId)
   540  	if err != nil {
   541  		return nil, err
   542  	}
   543  	if group == nil {
   544  		return nil, errors.Errorf("group %q does not exist", groupId)
   545  	}
   546  
   547  	createUserGroupNQuads := &api.NQuad{
   548  		Subject:   userId,
   549  		Predicate: "dgraph.user.group",
   550  		ObjectId:  group.Uid,
   551  	}
   552  
   553  	return createUserGroupNQuads, nil
   554  }
   555  
   556  func queryGroup(ctx context.Context, txn *dgo.Txn, groupid string,
   557  	fields ...string) (group *Group, err error) {
   558  
   559  	// write query header
   560  	query := fmt.Sprintf(`query search($groupid: string){
   561          group(func: eq(dgraph.xid, $groupid)) @filter(type(Group)) {
   562  			uid
   563  		    %s }}`, strings.Join(fields, ", "))
   564  
   565  	queryVars := map[string]string{
   566  		"$groupid": groupid,
   567  	}
   568  
   569  	queryResp, err := txn.QueryWithVars(ctx, query, queryVars)
   570  	if err != nil {
   571  		fmt.Printf("Error while querying group with id %s: %v\n", groupid, err)
   572  		return nil, err
   573  	}
   574  	group, err = UnmarshalGroup(queryResp.GetJson(), "group")
   575  	if err != nil {
   576  		return nil, err
   577  	}
   578  	return group, nil
   579  }
   580  
   581  func isSameAcl(acl1 *Acl, acl2 *Acl) bool {
   582  	return (len(acl1.Predicate) > 0 && len(acl2.Predicate) > 0 &&
   583  		acl1.Predicate == acl2.Predicate) ||
   584  		(len(acl1.Regex) > 0 && len(acl2.Regex) > 0 && acl1.Regex == acl2.Regex)
   585  }
   586  
   587  // returns whether the existing acls slice is changed
   588  func updateAcl(acls []Acl, newAcl Acl) ([]Acl, bool) {
   589  	for idx, aclEntry := range acls {
   590  		if isSameAcl(&aclEntry, &newAcl) {
   591  			if aclEntry.Perm == newAcl.Perm {
   592  				// new permission is the same as the current one, no update
   593  				return acls, false
   594  			}
   595  			if newAcl.Perm < 0 {
   596  				// remove the current aclEntry from the array
   597  				copy(acls[idx:], acls[idx+1:])
   598  				return acls[:len(acls)-1], true
   599  			}
   600  			acls[idx].Perm = newAcl.Perm
   601  			return acls, true
   602  		}
   603  	}
   604  
   605  	// we do not find any existing aclEntry matching the newAcl predicate
   606  	return append(acls, newAcl), true
   607  }
   608  
   609  func queryAndPrintUser(ctx context.Context, txn *dgo.Txn, userId string) error {
   610  	user, err := queryUser(ctx, txn, userId)
   611  	if err != nil {
   612  		return err
   613  	}
   614  	if user == nil {
   615  		return errors.Errorf("The user %q does not exist.\n", userId)
   616  	}
   617  
   618  	fmt.Printf("User  : %s\n", userId)
   619  	fmt.Printf("UID   : %s\n", user.Uid)
   620  	for _, group := range user.Groups {
   621  		fmt.Printf("Group : %-5s\n", group.GroupID)
   622  	}
   623  	return nil
   624  }
   625  
   626  func queryAndPrintGroup(ctx context.Context, txn *dgo.Txn, groupId string) error {
   627  	group, err := queryGroup(ctx, txn, groupId, "dgraph.xid", "~dgraph.user.group{dgraph.xid}",
   628  		"dgraph.group.acl")
   629  	if err != nil {
   630  		return err
   631  	}
   632  	if group == nil {
   633  		return errors.Errorf("The group %q does not exist.\n", groupId)
   634  	}
   635  	fmt.Printf("Group: %s\n", groupId)
   636  	fmt.Printf("UID  : %s\n", group.Uid)
   637  	fmt.Printf("ID   : %s\n", group.GroupID)
   638  
   639  	var userNames []string
   640  	for _, user := range group.Users {
   641  		userNames = append(userNames, user.UserID)
   642  	}
   643  	fmt.Printf("Users: %s\n", strings.Join(userNames, " "))
   644  
   645  	var acls []Acl
   646  	if len(group.Acls) != 0 {
   647  		if err := json.Unmarshal([]byte(group.Acls), &acls); err != nil {
   648  			return errors.Wrapf(err, "unable to unmarshal the acls associated with the group %v",
   649  				groupId)
   650  		}
   651  
   652  		for _, acl := range acls {
   653  			fmt.Printf("ACL  : %v\n", acl)
   654  		}
   655  	}
   656  	return nil
   657  }
   658  
   659  func info(conf *viper.Viper) error {
   660  	userId, groupId, err := getUserAndGroup(conf)
   661  	if err != nil {
   662  		return err
   663  	}
   664  
   665  	dc, cancel, err := getClientWithAdminCtx(conf)
   666  	defer cancel()
   667  	if err != nil {
   668  		return errors.Wrapf(err, "unable to get admin context")
   669  	}
   670  
   671  	ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second)
   672  	defer ctxCancel()
   673  	txn := dc.NewTxn()
   674  	defer func() {
   675  		if err := txn.Discard(ctx); err != nil {
   676  			fmt.Printf("Unable to discard transaction: %v\n", err)
   677  		}
   678  	}()
   679  
   680  	if len(userId) != 0 {
   681  		return queryAndPrintUser(ctx, txn, userId)
   682  	}
   683  
   684  	return queryAndPrintGroup(ctx, txn, groupId)
   685  }