blob: 702f5335ac087948cc61e24a3287d9da6da6f856 [file] [log] [blame]
// Copyright 2015 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"context"
"fmt"
"net"
"golang.org/x/oauth2"
"go.chromium.org/luci/auth/identity"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/server/auth/authdb"
"go.chromium.org/luci/server/auth/realms"
)
// State is stored in the context when handling an incoming request. It
// contains authentication related state of the current request.
type State interface {
// Authenticator is an Authenticator used to authenticate the request.
Authenticator() *Authenticator
// DB is authdb.DB snapshot with authorization information to use when
// processing this request.
//
// Use directly only when you know what your are doing. Prefer to use wrapping
// functions (e.g. IsMember) instead.
DB() authdb.DB
// Method returns an authentication method used for the current request or nil
// if the request is anonymous.
//
// If non-nil, its one of the methods in Authenticator.Methods.
Method() Method
// User holds the identity and profile of the current caller.
//
// User.Identity usually matches PeerIdentity(), but can be different if
// the delegation is used.
//
// This field is never nil. For anonymous call it contains User with identity
// AnonymousIdentity.
//
// Do not modify it.
User() *User
// Session is the session object produced by the authentication method.
//
// It may hold some extra information pertaining to the request. It may be nil
// if there's no extra information. The session can be used to transfer
// information from the authentication method to other parts of the auth
// stack that execute later.
Session() Session
// PeerIdentity identifies whoever is making the request.
//
// It's an identity directly extracted from user credentials (ignoring
// delegation tokens).
PeerIdentity() identity.Identity
// PeerIP is IP address (IPv4 or IPv6) of whoever is making the request or
// nil if not available.
PeerIP() net.IP
// UserCredentials is an end-user credentials as they were received if they
// are allowed to be forwarded.
//
// Includes the primary OAuth token and any extra LUCI-specific headers.
UserCredentials() (*oauth2.Token, map[string]string, error)
}
type stateContextKey int
// WithState injects State into the context.
//
// Mostly useful from tests. Must not be normally used from production code,
// 'Authenticate' sets the state itself.
func WithState(c context.Context, s State) context.Context {
return context.WithValue(c, stateContextKey(0), s)
}
// GetState return State stored in the context by 'Authenticate' call, the
// background state if 'Authenticate' wasn't used or nil if the auth library
// wasn't configured.
//
// The background state roughly is similar to the state of anonymous call.
// Various background non user-facing handlers (crons, task queues) that do not
// use 'Authenticate' see this state by default. Its most important role is to
// provide access to authdb.DB (and all functionality that depends on it) to
// background handlers.
func GetState(c context.Context) State {
if s, ok := c.Value(stateContextKey(0)).(State); ok && s != nil {
return s
}
if getConfig(c) != nil {
return backgroundState{c}
}
return nil
}
// CurrentUser represents the current caller.
//
// Shortcut for GetState(c).User(). Returns user with AnonymousIdentity if
// the context doesn't have State.
func CurrentUser(c context.Context) *User {
if s := GetState(c); s != nil {
return s.User()
}
return &User{Identity: identity.AnonymousIdentity}
}
// CurrentIdentity return identity of the current caller.
//
// Shortcut for GetState(c).User().Identity(). Returns AnonymousIdentity if
// the context doesn't have State.
func CurrentIdentity(c context.Context) identity.Identity {
if s := GetState(c); s != nil {
return s.User().Identity
}
return identity.AnonymousIdentity
}
// IsMember returns true if the current caller is in any of the given groups.
//
// Unknown groups are considered empty (the function returns false).
//
// May return errors if the check can not be performed (e.g. on datastore
// issues).
func IsMember(c context.Context, groups ...string) (bool, error) {
if s := GetState(c); s != nil {
return s.DB().IsMember(c, s.User().Identity, groups)
}
return false, ErrNotConfigured
}
// HasPermission returns true if the current caller has the given permission
// in the realm.
//
// A non-existing realm is replaced with the corresponding root realm (e.g. if
// "projectA:some/realm" doesn't exist, "projectA:@root" will be used in its
// place). If the project doesn't exist or is not using realms yet, all its
// realms (including the root realm) are considered empty. HasPermission returns
// false in this case.
//
// Returns an error only if the check itself failed due to a misconfiguration
// or transient issues. This should usually result in an Internal error.
func HasPermission(c context.Context, perm realms.Permission, realm string) (bool, error) {
if s := GetState(c); s != nil {
return s.DB().HasPermission(c, s.User().Identity, perm, realm)
}
return false, ErrNotConfigured
}
// HasPermissionDryRun compares result of HasPermission to 'expected'.
//
// Intended to be used during the migration between the old and new ACL models.
type HasPermissionDryRun struct {
ExpectedResult bool // the expected result of this dry run
TrackingBug string // identifier of a particular migration, for logs
AdminGroup string // if given, implicitly grant all permissions to its members
}
// Execute calls HasPermission and compares the result to the expectations.
//
// Logs information about the call and any errors or discrepancies found.
//
// Accepts same arguments as HasPermission. Intentionally returns nothing.
func (dr HasPermissionDryRun) Execute(c context.Context, perm realms.Permission, realm string) {
s := GetState(c)
if s == nil { // this should not really be happening at all
logging.Errorf(c, "HasPermissionDryRun: no state in the context")
return
}
db := s.DB()
ident := s.User().Identity
// We use python naming convention in the log to make Go and Python dry run
// logs look identical in case we want to parse them.
logPfx := fmt.Sprintf("has_permission_dryrun(%q, %q, %q), authdb=%d", perm, realm, ident, authdb.Revision(db))
if dr.TrackingBug != "" {
logPfx = dr.TrackingBug + ": " + logPfx
}
allowDeny := func(b bool) string {
if b {
return "ALLOW"
}
return "DENY"
}
switch result, err := db.HasPermission(c, ident, perm, realm); {
case err != nil:
logging.Errorf(c, "%s: error - want %s, got: %s", logPfx, allowDeny(dr.ExpectedResult), err)
case result == dr.ExpectedResult:
logging.Infof(c, "%s: match - %s", logPfx, allowDeny(result))
case dr.AdminGroup == "" || !dr.ExpectedResult:
logging.Warningf(c, "%s: mismatch - got %s, want %s", logPfx, allowDeny(result), allowDeny(dr.ExpectedResult))
default:
// We expected ALLOW, but got DENY. Maybe the legacy ACL check relied on
// the admin group. Check this separately.
switch admin, err := db.IsMember(c, ident, []string{dr.AdminGroup}); {
case err != nil:
logging.Errorf(c, "%s: error - want ALLOW, got: %s", logPfx, err)
case admin:
logging.Infof(c, "%s: match - ADMIN_ALLOW", logPfx)
default:
logging.Warningf(c, "%s: mismatch - got DENY, want ALLOW", logPfx)
}
}
}
// ShouldEnforceRealmACL is true if the service should enforce the realm's ACLs.
//
// Based on `enforce_in_service` realm data. Exists temporarily during the
// realms migration.
//
// TODO(crbug.com/1051724): Remove when no longer used.
func ShouldEnforceRealmACL(c context.Context, realm string) (bool, error) {
s := GetState(c)
if s == nil {
return false, ErrNotConfigured
}
data, err := s.DB().GetRealmData(c, realm)
switch {
case err != nil:
return false, errors.Annotate(err, "failed to load realm data").Err()
case data == nil:
return false, nil // no realms.cfg in the project at all
case len(data.EnforceInService) == 0:
return false, nil // enforced nowhere
}
info, err := GetSigner(c).ServiceInfo(c)
if err != nil {
return false, errors.Annotate(err, "failed to get our own service info").Err()
}
for _, id := range data.EnforceInService {
if id == info.AppID {
return true, nil
}
}
return false, nil
}
// IsInWhitelist returns true if the current caller is in the given IP
// whitelist.
//
// Unknown whitelists are considered empty (the function returns false).
//
// May return errors if the check can not be performed (e.g. on datastore
// issues).
func IsInWhitelist(c context.Context, whitelist string) (bool, error) {
if s := GetState(c); s != nil {
return s.DB().IsInWhitelist(c, s.PeerIP(), whitelist)
}
return false, ErrNotConfigured
}
// LoginURL returns a URL that, when visited, prompts the user to sign in,
// then redirects the user to the URL specified by dest.
//
// Shortcut for GetState(c).Authenticator().LoginURL(...).
func LoginURL(c context.Context, dest string) (string, error) {
if s := GetState(c); s != nil {
return s.Authenticator().LoginURL(c, dest)
}
return "", ErrNotConfigured
}
// LogoutURL returns a URL that, when visited, signs the user out, then
// redirects the user to the URL specified by dest.
//
// Shortcut for GetState(c).Authenticator().LogoutURL(...).
func LogoutURL(c context.Context, dest string) (string, error) {
if s := GetState(c); s != nil {
return s.Authenticator().LogoutURL(c, dest)
}
return "", ErrNotConfigured
}
///
// state implements State. Immutable.
type state struct {
authenticator *Authenticator
db authdb.DB
method Method
user *User
session Session
peerIdent identity.Identity
peerIP net.IP
// For AsCredentialsForwarder. 'endUserErr' (if not nil) would be returned by
// GetRPCTransport when attempting to forward the credentials.
endUserTok *oauth2.Token
endUserExtraHeaders map[string]string
endUserErr error
}
func (s *state) Authenticator() *Authenticator { return s.authenticator }
func (s *state) DB() authdb.DB { return s.db }
func (s *state) Method() Method { return s.method }
func (s *state) User() *User { return s.user }
func (s *state) Session() Session { return s.session }
func (s *state) PeerIdentity() identity.Identity { return s.peerIdent }
func (s *state) PeerIP() net.IP { return s.peerIP }
func (s *state) UserCredentials() (*oauth2.Token, map[string]string, error) {
return s.endUserTok, s.endUserExtraHeaders, s.endUserErr
}
///
// backgroundState corresponds to the state of auth library before any
// authentication is performed.
type backgroundState struct {
ctx context.Context
}
func isBackgroundState(s State) bool {
_, yes := s.(backgroundState)
return yes
}
func (s backgroundState) DB() authdb.DB {
db, err := GetDB(s.ctx)
if err != nil {
return authdb.ErroringDB{Error: err}
}
return db
}
func (s backgroundState) Authenticator() *Authenticator { return nil }
func (s backgroundState) Method() Method { return nil }
func (s backgroundState) User() *User { return &User{Identity: identity.AnonymousIdentity} }
func (s backgroundState) Session() Session { return nil }
func (s backgroundState) PeerIdentity() identity.Identity { return identity.AnonymousIdentity }
func (s backgroundState) PeerIP() net.IP { return nil }
func (s backgroundState) UserCredentials() (*oauth2.Token, map[string]string, error) {
return nil, nil, ErrNoForwardableCreds
}