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

     1  // Copyright 2016 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  	"time"
    16  
    17  	"github.com/cockroachdb/cockroach/pkg/security"
    18  	"github.com/cockroachdb/cockroach/pkg/settings"
    19  	"github.com/cockroachdb/cockroach/pkg/sql/catalog/resolver"
    20  	"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
    21  	"github.com/cockroachdb/cockroach/pkg/sql/sqlbase"
    22  	"github.com/cockroachdb/cockroach/pkg/util/contextutil"
    23  	"github.com/cockroachdb/cockroach/pkg/util/log"
    24  	"github.com/cockroachdb/cockroach/pkg/util/timeutil"
    25  	"github.com/cockroachdb/errors"
    26  )
    27  
    28  // GetUserHashedPassword determines if the given user exists and
    29  // also returns a password retrieval function.
    30  //
    31  // The function is tolerant of unavailable clusters (or unavailable
    32  // system.user) as follows:
    33  //
    34  // - if the user is root, the user is reported to exist immediately
    35  //   without querying system.users at all. The password retrieval
    36  //   is delayed until actually needed by the authentication method.
    37  //   This way, if the client presents a valid TLS certificate
    38  //   the password is not even needed at all. This is useful for e.g.
    39  //   `cockroach node status`.
    40  //
    41  //   If root is forced to use a password (e.g. logging in onto the UI)
    42  //   then a user login timeout greater than 5 seconds is also
    43  //   ignored. This ensures that root has a modicum of comfort
    44  //   logging into an unavailable cluster.
    45  //
    46  //   TODO(knz): this does not yet quite work becaus even if the pw
    47  //   auth on the UI succeeds writing to system.web_sessions will still
    48  //   stall on an unavailable cluster and prevent root from logging in.
    49  //
    50  // - if the user is another user than root, then the function fails
    51  //   after a timeout instead of blocking. The timeout is configurable
    52  //   via the cluster setting.
    53  //
    54  func GetUserHashedPassword(
    55  	ctx context.Context, ie *InternalExecutor, username string,
    56  ) (
    57  	exists bool,
    58  	canLogin bool,
    59  	pwRetrieveFn func(ctx context.Context) (hashedPassword []byte, err error),
    60  	validUntilFn func(ctx context.Context) (timestamp *tree.DTimestamp, err error),
    61  	err error,
    62  ) {
    63  	normalizedUsername := tree.Name(username).Normalize()
    64  	isRoot := normalizedUsername == security.RootUser
    65  
    66  	if isRoot {
    67  		// As explained above, for root we report that the user exists
    68  		// immediately, and delay retrieving the password until strictly
    69  		// necessary.
    70  		rootFn := func(ctx context.Context) ([]byte, error) {
    71  			_, _, hashedPassword, _, err := retrieveUserAndPassword(ctx, ie, isRoot, normalizedUsername)
    72  			return hashedPassword, err
    73  		}
    74  
    75  		// Root user cannot have password expiry and must have login.
    76  		validUntilFn := func(ctx context.Context) (*tree.DTimestamp, error) {
    77  			return nil, nil
    78  		}
    79  		return true, true, rootFn, validUntilFn, nil
    80  	}
    81  
    82  	// Other users must reach for system.users no matter what, because
    83  	// only that contains the truth about whether the user exists.
    84  	exists, canLogin, hashedPassword, validUntil, err := retrieveUserAndPassword(ctx, ie, isRoot, normalizedUsername)
    85  	return exists, canLogin,
    86  		func(ctx context.Context) ([]byte, error) { return hashedPassword, nil },
    87  		func(ctx context.Context) (*tree.DTimestamp, error) { return validUntil, nil },
    88  		err
    89  }
    90  
    91  func retrieveUserAndPassword(
    92  	ctx context.Context, ie *InternalExecutor, isRoot bool, normalizedUsername string,
    93  ) (exists bool, canLogin bool, hashedPassword []byte, validUntil *tree.DTimestamp, err error) {
    94  	// We may be operating with a timeout.
    95  	timeout := userLoginTimeout.Get(&ie.s.cfg.Settings.SV)
    96  	// We don't like long timeouts for root.
    97  	// (4.5 seconds to not exceed the default 5s timeout configured in many clients.)
    98  	const maxRootTimeout = 4*time.Second + 500*time.Millisecond
    99  	if isRoot && (timeout == 0 || timeout > maxRootTimeout) {
   100  		timeout = maxRootTimeout
   101  	}
   102  
   103  	runFn := func(fn func(ctx context.Context) error) error { return fn(ctx) }
   104  	if timeout != 0 {
   105  		runFn = func(fn func(ctx context.Context) error) error {
   106  			return contextutil.RunWithTimeout(ctx, "get-user-timeout", timeout, fn)
   107  		}
   108  	}
   109  
   110  	// Perform the lookup with a timeout.
   111  	err = runFn(func(ctx context.Context) error {
   112  		const getHashedPassword = `SELECT "hashedPassword" FROM system.users ` +
   113  			`WHERE username=$1`
   114  		values, err := ie.QueryRowEx(
   115  			ctx, "get-hashed-pwd", nil, /* txn */
   116  			sqlbase.InternalExecutorSessionDataOverride{User: security.RootUser},
   117  			getHashedPassword, normalizedUsername)
   118  		if err != nil {
   119  			return errors.Wrapf(err, "error looking up user %s", normalizedUsername)
   120  		}
   121  		if values != nil {
   122  			exists = true
   123  			if v := values[0]; v != tree.DNull {
   124  				hashedPassword = []byte(*(v.(*tree.DBytes)))
   125  			}
   126  		}
   127  
   128  		if !exists {
   129  			return nil
   130  		}
   131  
   132  		getLoginDependencies := `SELECT option, value FROM system.role_options ` +
   133  			`WHERE username=$1 AND option IN ('NOLOGIN', 'VALID UNTIL')`
   134  
   135  		loginDependencies, err := ie.QueryEx(
   136  			ctx, "get-login-dependencies", nil, /* txn */
   137  			sqlbase.InternalExecutorSessionDataOverride{User: security.RootUser},
   138  			getLoginDependencies,
   139  			normalizedUsername,
   140  		)
   141  		if err != nil {
   142  			return errors.Wrapf(err, "error looking up user %s", normalizedUsername)
   143  		}
   144  
   145  		// To support users created before 20.1, allow all USERS/ROLES to login
   146  		// if NOLOGIN is not found.
   147  		canLogin = true
   148  		for _, row := range loginDependencies {
   149  			option := string(tree.MustBeDString(row[0]))
   150  
   151  			if option == "NOLOGIN" {
   152  				canLogin = false
   153  			}
   154  
   155  			if option == "VALID UNTIL" {
   156  				if tree.DNull.Compare(nil, row[1]) != 0 {
   157  					ts := string(tree.MustBeDString(row[1]))
   158  					// This is okay because the VALID UNTIL is stored as a string
   159  					// representation of a TimestampTZ which has the same underlying
   160  					// representation in the table as a Timestamp (UTC time).
   161  					timeCtx := tree.NewParseTimeContext(timeutil.Now())
   162  					validUntil, err = tree.ParseDTimestamp(timeCtx, ts, time.Microsecond)
   163  					if err != nil {
   164  						return errors.Wrap(err,
   165  							"error trying to parse timestamp while retrieving password valid until value")
   166  					}
   167  				}
   168  			}
   169  		}
   170  
   171  		return nil
   172  	})
   173  
   174  	if err != nil {
   175  		// Failed to retrieve the user account. Report in logs for later investigation.
   176  		log.Warningf(ctx, "user lookup for %q failed: %v", normalizedUsername, err)
   177  		err = errors.HandledWithMessage(err, "internal error while retrieving user account")
   178  	}
   179  	return exists, canLogin, hashedPassword, validUntil, err
   180  }
   181  
   182  var userLoginTimeout = settings.RegisterPublicNonNegativeDurationSetting(
   183  	"server.user_login.timeout",
   184  	"timeout after which client authentication times out if some system range is unavailable (0 = no timeout)",
   185  	10*time.Second,
   186  )
   187  
   188  // GetAllRoles returns a "set" (map) of Roles -> true.
   189  func (p *planner) GetAllRoles(ctx context.Context) (map[string]bool, error) {
   190  	query := `SELECT username FROM system.users`
   191  	rows, err := p.ExtendedEvalContext().ExecCfg.InternalExecutor.QueryEx(
   192  		ctx, "read-users", p.txn,
   193  		sqlbase.InternalExecutorSessionDataOverride{User: security.RootUser},
   194  		query)
   195  	if err != nil {
   196  		return nil, err
   197  	}
   198  
   199  	users := make(map[string]bool)
   200  	for _, row := range rows {
   201  		username := tree.MustBeDString(row[0])
   202  		users[string(username)] = true
   203  	}
   204  	return users, nil
   205  }
   206  
   207  var roleMembersTableName = tree.MakeTableName("system", "role_members")
   208  
   209  // BumpRoleMembershipTableVersion increases the table version for the
   210  // role membership table.
   211  func (p *planner) BumpRoleMembershipTableVersion(ctx context.Context) error {
   212  	tableDesc, err := p.ResolveMutableTableDescriptor(ctx, &roleMembersTableName, true, resolver.ResolveAnyDescType)
   213  	if err != nil {
   214  		return err
   215  	}
   216  
   217  	return p.writeSchemaChange(
   218  		ctx, tableDesc, sqlbase.InvalidMutationID, "updating version for role membership table",
   219  	)
   220  }