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 }