github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/authorization.go (about)

     1  // Copyright 2017 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package sql
    12  
    13  import (
    14  	"context"
    15  	"fmt"
    16  
    17  	"github.com/cockroachdb/cockroach/pkg/security"
    18  	"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
    19  	"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
    20  	"github.com/cockroachdb/cockroach/pkg/sql/privilege"
    21  	"github.com/cockroachdb/cockroach/pkg/sql/roleoption"
    22  	"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
    23  	"github.com/cockroachdb/cockroach/pkg/sql/sqlbase"
    24  	"github.com/cockroachdb/cockroach/pkg/sql/types"
    25  	"github.com/cockroachdb/cockroach/pkg/util/syncutil"
    26  	"github.com/cockroachdb/errors"
    27  )
    28  
    29  // MembershipCache is a shared cache for role membership information.
    30  type MembershipCache struct {
    31  	syncutil.Mutex
    32  	tableVersion sqlbase.DescriptorVersion
    33  	// userCache is a mapping from username to userRoleMembership.
    34  	userCache map[string]userRoleMembership
    35  }
    36  
    37  // userRoleMembership is a mapping of "rolename" -> "with admin option".
    38  type userRoleMembership map[string]bool
    39  
    40  // AuthorizationAccessor for checking authorization (e.g. desc privileges).
    41  type AuthorizationAccessor interface {
    42  	// CheckPrivilege verifies that the user has `privilege` on `descriptor`.
    43  	CheckPrivilege(
    44  		ctx context.Context, descriptor sqlbase.DescriptorProto, privilege privilege.Kind,
    45  	) error
    46  
    47  	// CheckAnyPrivilege returns nil if user has any privileges at all.
    48  	CheckAnyPrivilege(ctx context.Context, descriptor sqlbase.DescriptorProto) error
    49  
    50  	// HasAdminRole returns tuple of bool and error:
    51  	// (true, nil) means that the user has an admin role (i.e. root or node)
    52  	// (false, nil) means that the user has NO admin role
    53  	// (false, err) means that there was an error running the query on
    54  	// the `system.users` table
    55  	HasAdminRole(ctx context.Context) (bool, error)
    56  
    57  	// RequireAdminRole is a wrapper on top of HasAdminRole.
    58  	// It errors if HasAdminRole errors or if the user isn't a super-user.
    59  	// Includes the named action in the error message.
    60  	RequireAdminRole(ctx context.Context, action string) error
    61  
    62  	// MemberOfWithAdminOption looks up all the roles (direct and indirect) that 'member' is a member
    63  	// of and returns a map of role -> isAdmin.
    64  	MemberOfWithAdminOption(ctx context.Context, member string) (map[string]bool, error)
    65  }
    66  
    67  var _ AuthorizationAccessor = &planner{}
    68  
    69  // CheckPrivilege implements the AuthorizationAccessor interface.
    70  // Requires a valid transaction to be open.
    71  func (p *planner) CheckPrivilege(
    72  	ctx context.Context, descriptor sqlbase.DescriptorProto, privilege privilege.Kind,
    73  ) error {
    74  	// Verify that the txn is valid in any case, so that
    75  	// we don't get the risk to say "OK" to root requests
    76  	// with an invalid API usage.
    77  	if p.txn == nil || !p.txn.IsOpen() {
    78  		return errors.AssertionFailedf("cannot use CheckPrivilege without a txn")
    79  	}
    80  
    81  	// Test whether the object is being audited, and if so, record an
    82  	// audit event. We place this check here to increase the likelihood
    83  	// it will not be forgotten if features are added that access
    84  	// descriptors (since every use of descriptors presumably need a
    85  	// permission check).
    86  	p.maybeAudit(descriptor, privilege)
    87  
    88  	user := p.SessionData().User
    89  	privs := descriptor.GetPrivileges()
    90  
    91  	// Check if 'user' itself has privileges.
    92  	if privs.CheckPrivilege(user, privilege) {
    93  		return nil
    94  	}
    95  
    96  	// Check if the 'public' pseudo-role has privileges.
    97  	if privs.CheckPrivilege(sqlbase.PublicRole, privilege) {
    98  		return nil
    99  	}
   100  
   101  	// Expand role memberships.
   102  	memberOf, err := p.MemberOfWithAdminOption(ctx, user)
   103  	if err != nil {
   104  		return err
   105  	}
   106  
   107  	// Iterate over the roles that 'user' is a member of. We don't care about the admin option.
   108  	for role := range memberOf {
   109  		if privs.CheckPrivilege(role, privilege) {
   110  			return nil
   111  		}
   112  	}
   113  
   114  	return pgerror.Newf(pgcode.InsufficientPrivilege,
   115  		"user %s does not have %s privilege on %s %s",
   116  		user, privilege, descriptor.TypeName(), descriptor.GetName())
   117  }
   118  
   119  // CheckAnyPrivilege implements the AuthorizationAccessor interface.
   120  // Requires a valid transaction to be open.
   121  func (p *planner) CheckAnyPrivilege(ctx context.Context, descriptor sqlbase.DescriptorProto) error {
   122  	// Verify that the txn is valid in any case, so that
   123  	// we don't get the risk to say "OK" to root requests
   124  	// with an invalid API usage.
   125  	if p.txn == nil || !p.txn.IsOpen() {
   126  		return errors.AssertionFailedf("cannot use CheckAnyPrivilege without a txn")
   127  	}
   128  
   129  	user := p.SessionData().User
   130  	privs := descriptor.GetPrivileges()
   131  
   132  	// Check if 'user' itself has privileges.
   133  	if privs.AnyPrivilege(user) {
   134  		return nil
   135  	}
   136  
   137  	// Check if 'public' has privileges.
   138  	if privs.AnyPrivilege(sqlbase.PublicRole) {
   139  		return nil
   140  	}
   141  
   142  	// Expand role memberships.
   143  	memberOf, err := p.MemberOfWithAdminOption(ctx, user)
   144  	if err != nil {
   145  		return err
   146  	}
   147  
   148  	// Iterate over the roles that 'user' is a member of. We don't care about the admin option.
   149  	for role := range memberOf {
   150  		if privs.AnyPrivilege(role) {
   151  			return nil
   152  		}
   153  	}
   154  
   155  	return pgerror.Newf(pgcode.InsufficientPrivilege,
   156  		"user %s has no privileges on %s %s",
   157  		p.SessionData().User, descriptor.TypeName(), descriptor.GetName())
   158  }
   159  
   160  // HasAdminRole implements the AuthorizationAccessor interface.
   161  // Requires a valid transaction to be open.
   162  func (p *planner) HasAdminRole(ctx context.Context) (bool, error) {
   163  	user := p.SessionData().User
   164  	if user == "" {
   165  		return false, errors.AssertionFailedf("empty user")
   166  	}
   167  	// Verify that the txn is valid in any case, so that
   168  	// we don't get the risk to say "OK" to root requests
   169  	// with an invalid API usage.
   170  	if p.txn == nil || !p.txn.IsOpen() {
   171  		return false, errors.AssertionFailedf("cannot use HasAdminRole without a txn")
   172  	}
   173  
   174  	// Check if user is 'root' or 'node'.
   175  	// TODO(knz): planner HasAdminRole has no business authorizing
   176  	// the "node" principal - node should not be issuing SQL queries.
   177  	if user == security.RootUser || user == security.NodeUser {
   178  		return true, nil
   179  	}
   180  
   181  	// Expand role memberships.
   182  	memberOf, err := p.MemberOfWithAdminOption(ctx, user)
   183  	if err != nil {
   184  		return false, err
   185  	}
   186  
   187  	// Check is 'user' is a member of role 'admin'.
   188  	if _, ok := memberOf[sqlbase.AdminRole]; ok {
   189  		return true, nil
   190  	}
   191  
   192  	return false, nil
   193  }
   194  
   195  // RequireAdminRole implements the AuthorizationAccessor interface.
   196  // Requires a valid transaction to be open.
   197  func (p *planner) RequireAdminRole(ctx context.Context, action string) error {
   198  	ok, err := p.HasAdminRole(ctx)
   199  
   200  	if err != nil {
   201  		return err
   202  	}
   203  	if !ok {
   204  		//raise error if user is not a super-user
   205  		return pgerror.Newf(pgcode.InsufficientPrivilege,
   206  			"only users with the admin role are allowed to %s", action)
   207  	}
   208  	return nil
   209  }
   210  
   211  // MemberOfWithAdminOption looks up all the roles 'member' belongs to (direct and indirect) and
   212  // returns a map of "role" -> "isAdmin".
   213  // The "isAdmin" flag applies to both direct and indirect members.
   214  // Requires a valid transaction to be open.
   215  func (p *planner) MemberOfWithAdminOption(
   216  	ctx context.Context, member string,
   217  ) (map[string]bool, error) {
   218  	if p.txn == nil || !p.txn.IsOpen() {
   219  		return nil, errors.AssertionFailedf("cannot use MemberOfWithAdminoption without a txn")
   220  	}
   221  
   222  	roleMembersCache := p.execCfg.RoleMemberCache
   223  
   224  	// Lookup table version.
   225  	objDesc, err := p.PhysicalSchemaAccessor().GetObjectDesc(
   226  		ctx,
   227  		p.txn,
   228  		p.ExecCfg().Settings,
   229  		p.ExecCfg().Codec,
   230  		roleMembersTableName.Catalog(),
   231  		roleMembersTableName.Schema(),
   232  		roleMembersTableName.Table(),
   233  		p.ObjectLookupFlags(true /*required*/, false /*requireMutable*/),
   234  	)
   235  	if err != nil {
   236  		return nil, err
   237  	}
   238  	tableDesc := objDesc.TableDesc()
   239  	tableVersion := tableDesc.Version
   240  
   241  	// We loop in case the table version changes while we're looking up memberships.
   242  	for {
   243  		// Check version and maybe clear cache while holding the mutex.
   244  		// We release the lock here instead of using defer as we need to keep
   245  		// going and re-lock if adding the looked-up entry.
   246  		roleMembersCache.Lock()
   247  		if roleMembersCache.tableVersion != tableVersion {
   248  			// Update version and drop the map.
   249  			roleMembersCache.tableVersion = tableVersion
   250  			roleMembersCache.userCache = make(map[string]userRoleMembership)
   251  		}
   252  
   253  		userMapping, ok := roleMembersCache.userCache[member]
   254  		roleMembersCache.Unlock()
   255  
   256  		if ok {
   257  			// Found: return.
   258  			return userMapping, nil
   259  		}
   260  
   261  		// Lookup memberships outside the lock.
   262  		memberships, err := p.resolveMemberOfWithAdminOption(ctx, member)
   263  		if err != nil {
   264  			return nil, err
   265  		}
   266  
   267  		// Update membership.
   268  		roleMembersCache.Lock()
   269  		if roleMembersCache.tableVersion != tableVersion {
   270  			// Table version has changed while we were looking, unlock and start over.
   271  			tableVersion = roleMembersCache.tableVersion
   272  			roleMembersCache.Unlock()
   273  			continue
   274  		}
   275  
   276  		// Table version remains the same: update map, unlock, return.
   277  		roleMembersCache.userCache[member] = memberships
   278  		roleMembersCache.Unlock()
   279  		return memberships, nil
   280  	}
   281  }
   282  
   283  // resolveMemberOfWithAdminOption performs the actual recursive role membership lookup.
   284  // TODO(mberhault): this is the naive way and performs a full lookup for each user,
   285  // we could save detailed memberships (as opposed to fully expanded) and reuse them
   286  // across users. We may then want to lookup more than just this user.
   287  func (p *planner) resolveMemberOfWithAdminOption(
   288  	ctx context.Context, member string,
   289  ) (map[string]bool, error) {
   290  	ret := map[string]bool{}
   291  
   292  	// Keep track of members we looked up.
   293  	visited := map[string]struct{}{}
   294  	toVisit := []string{member}
   295  	lookupRolesStmt := `SELECT "role", "isAdmin" FROM system.role_members WHERE "member" = $1`
   296  
   297  	for len(toVisit) > 0 {
   298  		// Pop first element.
   299  		m := toVisit[0]
   300  		toVisit = toVisit[1:]
   301  		if _, ok := visited[m]; ok {
   302  			continue
   303  		}
   304  		visited[m] = struct{}{}
   305  
   306  		rows, err := p.ExecCfg().InternalExecutor.Query(
   307  			ctx, "expand-roles", nil /* txn */, lookupRolesStmt, m,
   308  		)
   309  		if err != nil {
   310  			return nil, err
   311  		}
   312  
   313  		for _, row := range rows {
   314  			roleName := tree.MustBeDString(row[0])
   315  			isAdmin := row[1].(*tree.DBool)
   316  
   317  			ret[string(roleName)] = bool(*isAdmin)
   318  
   319  			// We need to expand this role. Let the "pop" worry about already-visited elements.
   320  			toVisit = append(toVisit, string(roleName))
   321  		}
   322  	}
   323  
   324  	return ret, nil
   325  }
   326  
   327  // HasRoleOption converts the roleoption to it's SQL column name and
   328  // checks if the user belongs to a role where the roleprivilege has value true.
   329  // Only works on checking the "positive version" of the privilege.
   330  // Requires a valid transaction to be open.
   331  // Example: CREATEROLE instead of NOCREATEROLE.
   332  func (p *planner) HasRoleOption(ctx context.Context, roleOption roleoption.Option) error {
   333  	// Verify that the txn is valid in any case, so that
   334  	// we don't get the risk to say "OK" to root requests
   335  	// with an invalid API usage.
   336  	if p.txn == nil || !p.txn.IsOpen() {
   337  		return errors.AssertionFailedf("cannot use HasRoleOption without a txn")
   338  	}
   339  
   340  	user := p.SessionData().User
   341  	if user == security.RootUser || user == security.NodeUser {
   342  		return nil
   343  	}
   344  
   345  	normalizedName, err := NormalizeAndValidateUsername(user)
   346  	if err != nil {
   347  		return err
   348  	}
   349  
   350  	// Create list of roles for sql WHERE IN clause.
   351  	memberOf, err := p.MemberOfWithAdminOption(ctx, normalizedName)
   352  	if err != nil {
   353  		return err
   354  	}
   355  
   356  	var roles = tree.NewDArray(types.String)
   357  	err = roles.Append(tree.NewDString(normalizedName))
   358  	if err != nil {
   359  		return err
   360  	}
   361  	for role := range memberOf {
   362  		err := roles.Append(tree.NewDString(role))
   363  		if err != nil {
   364  			return err
   365  		}
   366  	}
   367  
   368  	hasRolePrivilege, err := p.ExecCfg().InternalExecutor.QueryEx(
   369  		ctx, "has-role-option", p.Txn(),
   370  		sqlbase.InternalExecutorSessionDataOverride{User: security.RootUser},
   371  		fmt.Sprintf(
   372  			`SELECT 1 from %s WHERE option = '%s' AND username = ANY($1) LIMIT 1`,
   373  			RoleOptionsTableName,
   374  			roleOption.String()),
   375  		roles)
   376  
   377  	if err != nil {
   378  		return err
   379  	}
   380  
   381  	if len(hasRolePrivilege) != 0 {
   382  		return nil
   383  	}
   384  
   385  	// User is not a member of a role that has CREATEROLE privilege.
   386  	return pgerror.Newf(pgcode.InsufficientPrivilege,
   387  		"user %s does not have %s privilege", user, roleOption.String())
   388  }
   389  
   390  // ConnAuditingClusterSettingName is the name of the cluster setting
   391  // for the cluster setting that enables pgwire-level connection audit
   392  // logs.
   393  //
   394  // This name is defined here because it is needed in the telemetry
   395  // counts in SetClusterSetting() and importing pgwire here would
   396  // create a circular dependency.
   397  const ConnAuditingClusterSettingName = "server.auth_log.sql_connections.enabled"
   398  
   399  // AuthAuditingClusterSettingName is the name of the cluster setting
   400  // for the cluster setting that enables pgwire-level authentication audit
   401  // logs.
   402  //
   403  // This name is defined here because it is needed in the telemetry
   404  // counts in SetClusterSetting() and importing pgwire here would
   405  // create a circular dependency.
   406  const AuthAuditingClusterSettingName = "server.auth_log.sql_sessions.enabled"