go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/state.go (about) 1 // Copyright 2015 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package auth 16 17 import ( 18 "context" 19 "fmt" 20 "net" 21 22 "golang.org/x/oauth2" 23 24 "go.chromium.org/luci/auth/identity" 25 "go.chromium.org/luci/common/errors" 26 "go.chromium.org/luci/common/logging" 27 28 "go.chromium.org/luci/server/auth/authdb" 29 "go.chromium.org/luci/server/auth/realms" 30 ) 31 32 // State is stored in the context when handling an incoming request. It 33 // contains authentication related state of the current request. 34 type State interface { 35 // Authenticator is an Authenticator used to authenticate the request. 36 Authenticator() *Authenticator 37 38 // DB is authdb.DB snapshot with authorization information to use when 39 // processing this request. 40 // 41 // Use directly only when you know what your are doing. Prefer to use wrapping 42 // functions (e.g. IsMember) instead. 43 DB() authdb.DB 44 45 // Method returns an authentication method used for the current request or nil 46 // if the request is anonymous. 47 // 48 // If non-nil, its one of the methods in Authenticator.Methods. 49 Method() Method 50 51 // User holds the identity and profile of the current caller. 52 // 53 // User.Identity usually matches PeerIdentity(), but can be different if 54 // the delegation is used. 55 // 56 // This field is never nil. For anonymous call it contains User with identity 57 // AnonymousIdentity. 58 // 59 // Do not modify it. 60 User() *User 61 62 // Session is the session object produced by the authentication method. 63 // 64 // It may hold some extra information pertaining to the request. It may be nil 65 // if there's no extra information. The session can be used to transfer 66 // information from the authentication method to other parts of the auth 67 // stack that execute later. 68 Session() Session 69 70 // PeerIdentity identifies whoever is making the request. 71 // 72 // It's an identity directly extracted from user credentials (ignoring 73 // delegation tokens). 74 PeerIdentity() identity.Identity 75 76 // PeerIP is IP address (IPv4 or IPv6) of whoever is making the request or 77 // nil if not available. 78 PeerIP() net.IP 79 80 // UserCredentials is an end-user credentials as they were received if they 81 // are allowed to be forwarded. 82 // 83 // Includes the primary OAuth token and any extra LUCI-specific headers. 84 UserCredentials() (*oauth2.Token, map[string]string, error) 85 } 86 87 type stateContextKey int 88 89 // WithState injects State into the context. 90 // 91 // Mostly useful from tests. Must not be normally used from production code, 92 // 'Authenticate' sets the state itself. 93 func WithState(ctx context.Context, s State) context.Context { 94 return context.WithValue(ctx, stateContextKey(0), s) 95 } 96 97 // GetState return State stored in the context by 'Authenticate' call, the 98 // background state if 'Authenticate' wasn't used or nil if the auth library 99 // wasn't configured. 100 // 101 // The background state roughly is similar to the state of anonymous call. 102 // Various background non user-facing handlers (crons, task queues) that do not 103 // use 'Authenticate' see this state by default. Its most important role is to 104 // provide access to authdb.DB (and all functionality that depends on it) to 105 // background handlers. 106 func GetState(ctx context.Context) State { 107 if s, ok := ctx.Value(stateContextKey(0)).(State); ok && s != nil { 108 return s 109 } 110 if getConfig(ctx) != nil { 111 return backgroundState{ctx} 112 } 113 return nil 114 } 115 116 // CurrentUser represents the current caller. 117 // 118 // Shortcut for GetState(ctx).User(). Returns user with AnonymousIdentity if 119 // the context doesn't have State. 120 func CurrentUser(ctx context.Context) *User { 121 if s := GetState(ctx); s != nil { 122 return s.User() 123 } 124 return &User{Identity: identity.AnonymousIdentity} 125 } 126 127 // CurrentIdentity return identity of the current caller. 128 // 129 // Shortcut for GetState(ctx).User().Identity(). Returns AnonymousIdentity if 130 // the context doesn't have State. 131 func CurrentIdentity(ctx context.Context) identity.Identity { 132 if s := GetState(ctx); s != nil { 133 return s.User().Identity 134 } 135 return identity.AnonymousIdentity 136 } 137 138 // IsMember returns true if the current caller is in any of the given groups. 139 // 140 // Unknown groups are considered empty (the function returns false) but are 141 // logged as warnings. 142 // 143 // May return errors if the check can not be performed (e.g. on datastore 144 // issues). 145 func IsMember(ctx context.Context, groups ...string) (bool, error) { 146 if s := GetState(ctx); s != nil { 147 return s.DB().IsMember(ctx, s.User().Identity, groups) 148 } 149 return false, ErrNotConfigured 150 } 151 152 // HasPermission returns true if the current caller has the given permission 153 // in the realm. 154 // 155 // A non-existing realm is replaced with the corresponding root realm (e.g. if 156 // "projectA:some/realm" doesn't exist, "projectA:@root" will be used in its 157 // place). If the project doesn't exist or is not using realms yet, all its 158 // realms (including the root realm) are considered empty. HasPermission returns 159 // false in this case. 160 // 161 // Attributes are the context of this particular permission check and are used 162 // as inputs to `conditions` predicates in conditional bindings. If a service 163 // supports conditional bindings, it must document what attributes it passes 164 // with each permission it checks. 165 // 166 // Returns an error only if the check itself failed due to a misconfiguration 167 // or transient issues. This should usually result in an Internal error. 168 func HasPermission(ctx context.Context, perm realms.Permission, realm string, attrs realms.Attrs) (bool, error) { 169 if s := GetState(ctx); s != nil { 170 return s.DB().HasPermission(ctx, s.User().Identity, perm, realm, attrs) 171 } 172 return false, ErrNotConfigured 173 } 174 175 // HasPermissionDryRun compares result of HasPermission to 'expected'. 176 // 177 // Intended to be used during the migration between the old and new ACL models. 178 type HasPermissionDryRun struct { 179 ExpectedResult bool // the expected result of this dry run 180 TrackingBug string // identifier of a particular migration, for logs 181 AdminGroup string // if given, implicitly grant all permissions to its members 182 } 183 184 // Execute calls HasPermission and compares the result to the expectations. 185 // 186 // Logs information about the call and any errors or discrepancies found. 187 // 188 // Accepts same arguments as HasPermission. Intentionally returns nothing. 189 func (dr HasPermissionDryRun) Execute(ctx context.Context, perm realms.Permission, realm string, attrs realms.Attrs) { 190 s := GetState(ctx) 191 if s == nil { // this should not really be happening at all 192 logging.Errorf(ctx, "HasPermissionDryRun: no state in the context") 193 return 194 } 195 196 db := s.DB() 197 ident := s.User().Identity 198 199 // We use python naming convention in the log to make Go and Python dry run 200 // logs look identical in case we want to parse them. 201 logPfx := fmt.Sprintf("has_permission_dryrun(%q, %q, %q), authdb=%d", perm, realm, ident, authdb.Revision(db)) 202 if dr.TrackingBug != "" { 203 logPfx = dr.TrackingBug + ": " + logPfx 204 } 205 206 allowDeny := func(b bool) string { 207 if b { 208 return "ALLOW" 209 } 210 return "DENY" 211 } 212 213 switch result, err := db.HasPermission(ctx, ident, perm, realm, attrs); { 214 case err != nil: 215 logging.Errorf(ctx, "%s: error - want %s, got: %s", logPfx, allowDeny(dr.ExpectedResult), err) 216 case result == dr.ExpectedResult: 217 logging.Infof(ctx, "%s: match - %s", logPfx, allowDeny(result)) 218 case dr.AdminGroup == "" || !dr.ExpectedResult: 219 logging.Warningf(ctx, "%s: mismatch - got %s, want %s", logPfx, allowDeny(result), allowDeny(dr.ExpectedResult)) 220 default: 221 // We expected ALLOW, but got DENY. Maybe the legacy ACL check relied on 222 // the admin group. Check this separately. 223 switch admin, err := db.IsMember(ctx, ident, []string{dr.AdminGroup}); { 224 case err != nil: 225 logging.Errorf(ctx, "%s: error - want ALLOW, got: %s", logPfx, err) 226 case admin: 227 logging.Infof(ctx, "%s: match - ADMIN_ALLOW", logPfx) 228 default: 229 logging.Warningf(ctx, "%s: mismatch - got DENY, want ALLOW", logPfx) 230 } 231 } 232 } 233 234 // QueryRealms returns a list of realms where the current caller has the given 235 // permission. 236 // 237 // If `project` is not empty, restricts the check only to the realms in this 238 // project, otherwise checks all realms across all projects. Either way, the 239 // returned realm names have form `<some-project>:<some-realm>`. The list is 240 // returned in some arbitrary order. 241 // 242 // Semantically it is equivalent to visiting all explicitly defined realms 243 // (plus "<project>:@root" and "<project>:@legacy") in the requested project or 244 // all projects, and calling HasPermission(perm, realm, attr) for each of them. 245 // 246 // The permission `perm` should be flagged in the process with UsedInQueryRealms 247 // flag, which lets the runtime know it must prepare indexes for the 248 // corresponding QueryRealms call. 249 // 250 // Returns an error only if the check itself failed due to a misconfiguration 251 // or transient issues. This should usually result in an Internal error. 252 func QueryRealms(ctx context.Context, perm realms.Permission, project string, attrs realms.Attrs) ([]string, error) { 253 if s := GetState(ctx); s != nil { 254 return s.DB().QueryRealms(ctx, s.User().Identity, perm, project, attrs) 255 } 256 return nil, ErrNotConfigured 257 } 258 259 // ShouldEnforceRealmACL is true if the service should enforce the realm's ACLs. 260 // 261 // Based on `enforce_in_service` realm data. Exists temporarily during the 262 // realms migration. 263 // 264 // TODO(crbug.com/1051724): Remove when no longer used. 265 func ShouldEnforceRealmACL(ctx context.Context, realm string) (bool, error) { 266 s := GetState(ctx) 267 if s == nil { 268 return false, ErrNotConfigured 269 } 270 271 data, err := s.DB().GetRealmData(ctx, realm) 272 switch { 273 case err != nil: 274 return false, errors.Annotate(err, "failed to load realm data").Err() 275 case data == nil: 276 return false, nil // no realms.cfg in the project at all 277 case len(data.EnforceInService) == 0: 278 return false, nil // enforced nowhere 279 } 280 281 info, err := GetSigner(ctx).ServiceInfo(ctx) 282 if err != nil { 283 return false, errors.Annotate(err, "failed to get our own service info").Err() 284 } 285 286 for _, id := range data.EnforceInService { 287 if id == info.AppID { 288 return true, nil 289 } 290 } 291 return false, nil 292 } 293 294 // IsAllowedIP returns true if the current caller is in the given IP allowlist. 295 // 296 // Unknown allowlists are considered empty (the function returns false). 297 // 298 // May return errors if the check can not be performed (e.g. on datastore 299 // issues). 300 func IsAllowedIP(ctx context.Context, allowlist string) (bool, error) { 301 if s := GetState(ctx); s != nil { 302 return s.DB().IsAllowedIP(ctx, s.PeerIP(), allowlist) 303 } 304 return false, ErrNotConfigured 305 } 306 307 // LoginURL returns a URL that, when visited, prompts the user to sign in, 308 // then redirects the user to the URL specified by dest. 309 // 310 // Shortcut for GetState(ctx).Authenticator().LoginURL(...). 311 func LoginURL(ctx context.Context, dest string) (string, error) { 312 if s := GetState(ctx); s != nil { 313 return s.Authenticator().LoginURL(ctx, dest) 314 } 315 return "", ErrNotConfigured 316 } 317 318 // LogoutURL returns a URL that, when visited, signs the user out, then 319 // redirects the user to the URL specified by dest. 320 // 321 // Shortcut for GetState(ctx).Authenticator().LogoutURL(...). 322 func LogoutURL(ctx context.Context, dest string) (string, error) { 323 if s := GetState(ctx); s != nil { 324 return s.Authenticator().LogoutURL(ctx, dest) 325 } 326 return "", ErrNotConfigured 327 } 328 329 /// 330 331 // state implements State. Immutable. 332 type state struct { 333 authenticator *Authenticator 334 db authdb.DB 335 method Method 336 user *User 337 session Session 338 peerIdent identity.Identity 339 peerIP net.IP 340 341 // For AsCredentialsForwarder. 'endUserErr' (if not nil) would be returned by 342 // GetRPCTransport when attempting to forward the credentials. 343 endUserTok *oauth2.Token 344 endUserExtraHeaders map[string]string 345 endUserErr error 346 } 347 348 func (s *state) Authenticator() *Authenticator { return s.authenticator } 349 func (s *state) DB() authdb.DB { return s.db } 350 func (s *state) Method() Method { return s.method } 351 func (s *state) User() *User { return s.user } 352 func (s *state) Session() Session { return s.session } 353 func (s *state) PeerIdentity() identity.Identity { return s.peerIdent } 354 func (s *state) PeerIP() net.IP { return s.peerIP } 355 func (s *state) UserCredentials() (*oauth2.Token, map[string]string, error) { 356 return s.endUserTok, s.endUserExtraHeaders, s.endUserErr 357 } 358 359 /// 360 361 // backgroundState corresponds to the state of auth library before any 362 // authentication is performed. 363 type backgroundState struct { 364 ctx context.Context 365 } 366 367 func isBackgroundState(s State) bool { 368 _, yes := s.(backgroundState) 369 return yes 370 } 371 372 func (s backgroundState) DB() authdb.DB { 373 db, err := GetDB(s.ctx) 374 if err != nil { 375 return authdb.ErroringDB{Error: err} 376 } 377 return db 378 } 379 380 func (s backgroundState) Authenticator() *Authenticator { return nil } 381 func (s backgroundState) Method() Method { return nil } 382 func (s backgroundState) User() *User { return &User{Identity: identity.AnonymousIdentity} } 383 func (s backgroundState) Session() Session { return nil } 384 func (s backgroundState) PeerIdentity() identity.Identity { return identity.AnonymousIdentity } 385 func (s backgroundState) PeerIP() net.IP { return nil } 386 func (s backgroundState) UserCredentials() (*oauth2.Token, map[string]string, error) { 387 return nil, nil, ErrNoForwardableCreds 388 }