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  }