blob: e7e6412efb796c1766c6e02b29b9a76b1fb97e67 [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"
"net"
"net/http"
"golang.org/x/oauth2"
"google.golang.org/grpc/codes"
"go.chromium.org/luci/auth/identity"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/retry/transient"
"go.chromium.org/luci/grpc/grpcutil"
"go.chromium.org/luci/server/auth/authdb"
"go.chromium.org/luci/server/auth/delegation"
"go.chromium.org/luci/server/auth/internal/tracing"
"go.chromium.org/luci/server/auth/signing"
"go.chromium.org/luci/server/router"
)
var (
// Authenticate errors (must be grpc-tagged).
// ErrNotConfigured is returned by Authenticate and other functions if the
// context wasn't previously initialized via 'Initialize'.
ErrNotConfigured = errors.New("auth: the library is not properly configured", grpcutil.InternalTag)
// ErrBadClientID is returned by Authenticate if caller is using an OAuth2
// client ID not in the list of allowed IDs. More info is in the log.
ErrBadClientID = errors.New("auth: OAuth client_id is not in the allowlist", grpcutil.PermissionDeniedTag)
// ErrBadAudience is returned by Authenticate if token's audience is unknown.
ErrBadAudience = errors.New("auth: bad token audience", grpcutil.PermissionDeniedTag)
// ErrBadRemoteAddr is returned by Authenticate if request's remote_addr can't
// be parsed.
ErrBadRemoteAddr = errors.New("auth: bad remote addr", grpcutil.InternalTag)
// ErrForbiddenIP is returned when an account is restricted by an IP allowlist
// and request's remote_addr is not in it.
ErrForbiddenIP = errors.New("auth: IP is not in the allowlist", grpcutil.PermissionDeniedTag)
// ErrProjectHeaderForbidden is returned by Authenticate if an unknown caller
// tries to use X-Luci-Project header. Only a preapproved set of callers are
// allowed to use this header, see InternalServicesGroup.
ErrProjectHeaderForbidden = errors.New("auth: the caller is not allowed to use X-Luci-Project", grpcutil.PermissionDeniedTag)
// Other errors.
// ErrNoUsersAPI is returned by LoginURL and LogoutURL if none of
// the authentication methods support UsersAPI.
ErrNoUsersAPI = errors.New("auth: methods do not support login or logout URL")
// ErrNoForwardableCreds is returned by GetRPCTransport when attempting to
// forward credentials (via AsCredentialsForwarder) that are not forwardable.
ErrNoForwardableCreds = errors.New("auth: no forwardable credentials in the context")
// ErrNoStateEndpoint is returned by StateEndpointURL if the state endpoint is
// not exposed.
ErrNoStateEndpoint = errors.New("auth: the state endpoint is not available")
)
const (
// InternalServicesGroup is a name of a group with service accounts of LUCI
// microservices of the current LUCI deployment (and only them!).
//
// Accounts in this group are allowed to use X-Luci-Project header to specify
// that RPCs are done in a context of some particular project. For such
// requests CurrentIdentity() == 'project:<X-Luci-Project value>'.
//
// This group should contain only **fully trusted** services, deployed and
// managed by the LUCI deployment administrators. Adding "random" services
// here is a security risk, since they will be able to impersonate any LUCI
// project.
InternalServicesGroup = "auth-luci-services"
)
// RequestMetadata is metadata used when authenticating a request.
//
// Can be constructed by:
// - RequestMetadataForHTTP based on http.Request.
// - authtest.NewFakeRequestMetadata based on fakes for unit tests.
type RequestMetadata interface {
// Header returns a value of a given header or an empty string.
//
// Headers are also known as simply "metadata" in gRPC world.
//
// The key is case-insensitive. If the request has multiple headers matching
// the key, returns only the first one.
Header(key string) string
// Cookie returns a cookie or an error if there's no such cookie.
//
// Transports that do not support cookies (e.g. gRPC) can always return
// an error. They will just not work with authentication schemes based on
// cookies.
Cookie(key string) (*http.Cookie, error)
// RemoteAddr returns the IP address the request came from or "" if unknown.
//
// It is used by default for IP allowlist checks if there's no EndUserIP
// callback set in the auth library configuration. The EndUserIP callback is
// usually set in environments where the server runs behind a proxy, when
// the real end user IP is passed via some trusted header or other form of
// metadata.
//
// If "", IP allowlist check will be skipped and the request will be assumed
// to come from "0.0.0.0" aka "unspecified IPv4".
RemoteAddr() string
// Host returns the hostname the request was sent to or "" if unknown.
//
// Also known as HTTP2 `:authority` pseudo-header.
Host() string
}
// Method implements a particular low-level authentication mechanism.
//
// It may also optionally implement a bunch of other interfaces:
//
// UsersAPI: if the method supports login and logout URLs.
// Warmable: if the method supports warm up.
// HasHandlers: if the method needs to install HTTP handlers.
//
// Methods are not usually used directly, but passed to Authenticator{...} that
// knows how to apply them.
type Method interface {
// Authenticate extracts user information from the incoming request.
//
// It returns:
// * (*User, Session or nil, nil) on success.
// * (nil, nil, nil) if the method is not applicable.
// * (nil, nil, error) if the method is applicable, but credentials are bad.
//
// The returned error may be tagged with an grpcutil error tag. Its code will
// be used to derive the response status code. Internal error messages (e.g.
// ones tagged with grpcutil.InternalTag or similar) are logged, but not sent
// to clients. All other errors are sent to clients as is.
Authenticate(context.Context, RequestMetadata) (*User, Session, error)
}
// UsersAPI may be additionally implemented by Method if it supports login and
// logout URLs.
type UsersAPI interface {
// LoginURL returns a URL that, when visited, prompts the user to sign in,
// then redirects the user to the URL specified by dest.
LoginURL(ctx context.Context, dest string) (string, error)
// LogoutURL returns a URL that, when visited, signs the user out,
// then redirects the user to the URL specified by dest.
LogoutURL(ctx context.Context, dest string) (string, error)
}
// Warmable may be additionally implemented by Method if it supports warm up.
type Warmable interface {
// Warmup may be called to precache the data needed by the method.
//
// There's no guarantee when it will be called or if it will be called at all.
// Should always do best-effort initialization. Errors are logged and ignored.
Warmup(ctx context.Context) error
}
// HasHandlers may be additionally implemented by Method if it needs to
// install HTTP handlers.
type HasHandlers interface {
// InstallHandlers installs necessary HTTP handlers into the router.
InstallHandlers(r *router.Router, base router.MiddlewareChain)
}
// HasStateEndpoint may be additionally implemented by Method if it exposes
// an HTTP endpoints that returns the authentication state, OAuth and ID tokens
// for frontend applications.
type HasStateEndpoint interface {
// StateEndpointURL returns an URL that serves StateEndpointResponse JSON.
//
// See StateEndpointResponse for the format and meaning of the response.
//
// Returns ErrNoStateEndpoint if the endpoint is not actually exposed. This
// can happen if the method generally supports the state endpoint, but it is
// turned off in the method's configuration.
StateEndpointURL(ctx context.Context) (string, error)
}
// UserCredentialsGetter may be additionally implemented by Method if it knows
// how to extract end-user credentials from the incoming request. Currently
// understands only OAuth2 tokens.
type UserCredentialsGetter interface {
// GetUserCredentials extracts an OAuth access token from the incoming request
// or returns an error if it isn't possible.
//
// May omit token's expiration time if it isn't known.
//
// Guaranteed to be called only after the successful authentication, so it
// doesn't have to recheck the validity of the token.
GetUserCredentials(context.Context, RequestMetadata) (*oauth2.Token, error)
}
// Session holds some extra information pertaining to the request.
//
// It is stored in the context as part of State. Used by AsSessionUser RPC
// authority kind.
type Session interface {
// AccessToken returns an OAuth access token identifying the session user.
AccessToken(ctx context.Context) (*oauth2.Token, error)
// IDToken returns an ID token identifying the session user.
IDToken(ctx context.Context) (*oauth2.Token, error)
}
// User represents identity and profile of a user.
type User struct {
// Identity is identity string of the user (may be AnonymousIdentity).
// If User is returned by Authenticate(...), Identity string is always present
// and valid.
Identity identity.Identity `json:"identity,omitempty"`
// Superuser is true if the user is site-level administrator. For example, on
// GAE this bit is set for GAE-level administrators. Optional, default false.
Superuser bool `json:"superuser,omitempty"`
// Email is email of the user. Optional, default "". Don't use it as a key
// in various structures. Prefer to use Identity() instead (it is always
// available).
Email string `json:"email,omitempty"`
// Name is full name of the user. Optional, default "".
Name string `json:"name,omitempty"`
// Picture is URL of the user avatar. Optional, default "".
Picture string `json:"picture,omitempty"`
// ClientID is the ID of the pre-registered OAuth2 client so its identity can
// be verified. Used only by authentication methods based on OAuth2.
// See https://developers.google.com/console/help/#generatingoauth2 for more.
ClientID string `json:"client_id,omitempty"`
// Extra is any additional information the authentication method produces.
//
// Its exact type depends on the authentication method used. Usually the
// authentication method will have an accompanying getter function that knows
// how to interpret this field.
Extra any `json:"-"`
}
// StateEndpointResponse defines a JSON structure of a state endpoint response.
//
// It represents the state of the authentication session based on the session
// cookie (or other ambient credential) in the request metadata.
//
// It is intended to be called via a same origin URL fetch request by the
// frontend code that needs an OAuth access token or an ID token representing
// the signed in user.
//
// If there's a valid authentication credential, the state endpoint replies with
// HTTP 200 status code and the JSON-serialized StateEndpointResponse struct
// with state details. The handler refreshes access and ID tokens if they expire
// soon.
//
// If there is no authentication credential or it has expired or was revoked,
// the state endpoint still replies with HTTP 200 code and the JSON-serialized
// StateEndpointResponse struct, except its `identity` field is
// `anonymous:anonymous` and no other fields are populated.
//
// On errors the state endpoint replies with a non-200 HTTP status code with a
// `plain/text` body containing the error message. This is an exceptional
// situation (usually internal transient errors caused by the session store
// unavailability or some misconfiguration in code). Replies with HTTP code
// equal or larger than 500 indicate transient errors and can be retried.
//
// The state endpoint is exposed only by auth methods that implement
// HasStateEndpoint interface (e.g. `encryptedcookies`), and only if they are
// configured to expose it.
type StateEndpointResponse struct {
// Identity is a LUCI identity string of the user or `anonymous:anonymous` if
// the user is not logged in.
Identity string `json:"identity"`
// Email is the email of the user account if the user is logged in.
Email string `json:"email,omitempty"`
// Picture is the https URL of the user profile picture if available.
Picture string `json:"picture,omitempty"`
// AccessToken is an OAuth access token of the logged in user.
//
// See RequiredScopes and OptionalScopes in AuthMethod for what scopes this
// token can have.
AccessToken string `json:"accessToken,omitempty"`
// AccessTokenExpiry is an absolute expiration time (as a unix timestamp) of
// the access token.
//
// It is at least 10 min in the future.
AccessTokenExpiry int64 `json:"accessTokenExpiry,omitempty"`
// AccessTokenExpiresIn is approximately how long the access token will be
// valid since when the response was generated, in seconds.
//
// It is at least 600 sec.
AccessTokenExpiresIn int32 `json:"accessTokenExpiresIn,omitempty"`
// IDToken is an identity token of the logged in user.
//
// Its `aud` claim is equal to ClientID in OpenIDConfig passed to AuthMethod.
IDToken string `json:"idToken,omitempty"`
// IDTokenExpiry is an absolute expiration time (as a unix timestamp) of
// the identity token.
//
// It is at least 10 min in the future.
IDTokenExpiry int64 `json:"idTokenExpiry,omitempty"`
// IDTokenExpiresIn is approximately how long the identity token will be
// valid since when the response was generated, in seconds.
//
// It is at least 600 sec.
IDTokenExpiresIn int32 `json:"idTokenExpiresIn,omitempty"`
}
// Authenticator performs authentication of incoming requests.
//
// It is a stateless object configured with a list of methods to try when
// authenticating incoming requests. It implements Authenticate method that
// performs high-level authentication logic using the provided list of low-level
// auth methods.
//
// Note that most likely you don't need to instantiate this object directly.
// Use Authenticate middleware instead. Authenticator is exposed publicly only
// to be used in advanced cases, when you need to fine-tune authentication
// behavior.
type Authenticator struct {
Methods []Method // a list of authentication methods to try
}
// GetMiddleware returns a middleware that uses this Authenticator for
// authentication.
//
// It uses a.Authenticate internally and handles errors appropriately.
//
// TODO(vadimsh): Refactor to be a function instead of a method and move to
// http.go.
func (a *Authenticator) GetMiddleware() router.Middleware {
return func(c *router.Context, next router.Handler) {
ctx, err := a.AuthenticateHTTP(c.Request.Context(), c.Request)
if err != nil {
code, ok := grpcutil.Tag.In(err)
if !ok {
if transient.Tag.In(err) {
code = codes.Internal
} else {
code = codes.Unauthenticated
}
}
replyError(c.Request.Context(), c.Writer, grpcutil.CodeStatus(code), err)
} else {
c.Request = c.Request.WithContext(ctx)
next(c)
}
}
}
// AuthenticateHTTP authenticates an HTTP request.
//
// See Authenticate for all details.
//
// This method is likely temporary until pRPC server switches to use gRPC
// interceptors for authentication.
func (a *Authenticator) AuthenticateHTTP(ctx context.Context, r *http.Request) (context.Context, error) {
return a.Authenticate(ctx, RequestMetadataForHTTP(r))
}
// Authenticate authenticates the request and adds State into the context.
//
// Returns an error if credentials are provided, but invalid. If no credentials
// are provided (i.e. the request is anonymous), finishes successfully, but in
// that case CurrentIdentity() returns AnonymousIdentity.
//
// The returned error may be tagged with an grpcutil error tag. Its code should
// be used to derive the response status code. Internal error messages (e.g.
// ones tagged with grpcutil.InternalTag or similar) should be logged, but not
// sent to clients. All other errors should be sent to clients as is.
func (a *Authenticator) Authenticate(ctx context.Context, r RequestMetadata) (_ context.Context, err error) {
tracedCtx, span := tracing.Start(ctx, "go.chromium.org/luci/server/auth.Authenticate")
report := durationReporter(tracedCtx, authenticateDuration)
// This variable is changed throughout the function's execution. It it used
// in the defer to figure out at what stage the call failed.
stage := ""
// This defer reports the outcome of the authentication to the monitoring.
defer func() {
switch {
case err == nil:
report(nil, "SUCCESS")
case err == ErrNotConfigured:
report(err, "ERROR_NOT_CONFIGURED")
case err == ErrBadClientID:
report(err, "ERROR_FORBIDDEN_OAUTH_CLIENT")
case err == ErrBadAudience:
report(err, "ERROR_FORBIDDEN_AUDIENCE")
case err == ErrBadRemoteAddr:
report(err, "ERROR_BAD_REMOTE_ADDR")
case err == ErrForbiddenIP:
report(err, "ERROR_FORBIDDEN_IP")
case err == ErrProjectHeaderForbidden:
report(err, "ERROR_PROJECT_HEADER_FORBIDDEN")
case transient.Tag.In(err):
report(err, "ERROR_TRANSIENT_IN_"+stage)
default:
report(err, "ERROR_IN_"+stage)
}
tracing.End(span, err)
}()
// We will need working DB factory below to check IP allowlist.
cfg := getConfig(tracedCtx)
if cfg == nil || cfg.DBProvider == nil || len(a.Methods) == 0 {
return nil, ErrNotConfigured
}
// The future state that will be placed into the context.
s := state{authenticator: a, endUserErr: ErrNoForwardableCreds}
// Pick the first authentication method that applies.
stage = "AUTH"
for _, m := range a.Methods {
var err error
if s.user, s.session, err = m.Authenticate(tracedCtx, r); err != nil {
return nil, err
}
if s.user != nil {
if err = s.user.Identity.Validate(); err != nil {
stage = "ID_REGEXP_CHECK"
return nil, err
}
s.method = m
break
}
}
// If no authentication method is applicable, default to anonymous identity.
if s.method == nil {
s.user = &User{Identity: identity.AnonymousIdentity}
s.session = nil
}
// peerIdent always matches the identity of a remote peer. It may end up being
// different from s.user.Identity if the delegation tokens or project
// identities are used (see below). They affect s.user.Identity but don't
// touch s.peerIdent.
s.peerIdent = s.user.Identity
// Grab a snapshot of auth DB to use it consistently for the duration of this
// request.
stage = "AUTHDB_FETCH"
s.db, err = cfg.DBProvider(tracedCtx)
if err != nil {
return nil, err
}
// If using OAuth2, make sure the ClientID is allowlisted.
if s.user.ClientID != "" {
stage = "OAUTH_CLIENT_ID_CHECK"
if err := checkClientID(tracedCtx, cfg, s.db, s.user.Email, s.user.ClientID); err != nil {
return nil, err
}
}
// Extract peer's IP address and, if necessary, check it against an allowlist.
stage = "IP_CHECK"
if s.peerIP, err = checkEndUserIP(tracedCtx, cfg, s.db, r, s.peerIdent); err != nil {
return nil, err
}
// Check X-Delegation-Token-V1 and X-Luci-Project headers. They are used in
// LUCI-specific protocols to allow LUCI micro-services to act on behalf of
// end-users or projects.
var delegationToken string
var projectHeader string
if delegationToken = r.Header(delegation.HTTPHeaderName); delegationToken != "" {
stage = "DELEGATION_TOKEN_CHECK"
if s.user, err = checkDelegationToken(tracedCtx, cfg, s.db, delegationToken, s.peerIdent); err != nil {
return nil, err
}
} else if projectHeader = r.Header(XLUCIProjectHeader); projectHeader != "" {
stage = "PROJECT_HEADER_CHECK"
if s.user, err = checkProjectHeader(tracedCtx, s.db, projectHeader, s.peerIdent); err != nil {
return nil, err
}
}
// If the main authentication mechanism is based on forwardable OAuth tokens,
// grab all forwardable headers for GetRPCTransport(AsCredentialsForwarder).
if credsGetter, _ := s.method.(UserCredentialsGetter); credsGetter != nil {
s.endUserTok, s.endUserErr = credsGetter.GetUserCredentials(tracedCtx, r)
if s.endUserErr == nil && (delegationToken != "" || projectHeader != "") {
s.endUserExtraHeaders = make(map[string]string, 2)
if delegationToken != "" {
s.endUserExtraHeaders[delegation.HTTPHeaderName] = delegationToken
}
if projectHeader != "" {
s.endUserExtraHeaders[XLUCIProjectHeader] = projectHeader
}
}
}
// Inject the auth state into the original context (not the traced one).
return WithState(ctx, &s), nil
}
// usersAPI returns implementation of UsersAPI by examining Methods.
//
// Returns nil if none of Methods implement UsersAPI.
func (a *Authenticator) usersAPI() UsersAPI {
for _, m := range a.Methods {
if api, ok := m.(UsersAPI); ok {
return api
}
}
return nil
}
// LoginURL returns a URL that, when visited, prompts the user to sign in,
// then redirects the user to the URL specified by dest.
//
// Returns ErrNoUsersAPI if none of the authentication methods support login
// URLs.
func (a *Authenticator) LoginURL(ctx context.Context, dest string) (string, error) {
if api := a.usersAPI(); api != nil {
return api.LoginURL(ctx, dest)
}
return "", ErrNoUsersAPI
}
// LogoutURL returns a URL that, when visited, signs the user out, then
// redirects the user to the URL specified by dest.
//
// Returns ErrNoUsersAPI if none of the authentication methods support login
// URLs.
func (a *Authenticator) LogoutURL(ctx context.Context, dest string) (string, error) {
if api := a.usersAPI(); api != nil {
return api.LogoutURL(ctx, dest)
}
return "", ErrNoUsersAPI
}
////
// replyError logs the error and writes a response to ResponseWriter.
//
// For codes < 500, the error is logged at Warning level and written to the
// response as is. For codes >= 500 the error is logged at Error level and
// the generic error message is written instead.
func replyError(ctx context.Context, rw http.ResponseWriter, code int, err error) {
if code < 500 {
logging.Warningf(ctx, "HTTP %d: %s", code, err)
http.Error(rw, err.Error(), code)
} else {
logging.Errorf(ctx, "HTTP %d: %s", code, err)
http.Error(rw, http.StatusText(code), code)
}
}
// checkClientID returns nil if the clientID is allowed, ErrBadClientID if not,
// and a transient errors if the check itself failed.
func checkClientID(ctx context.Context, cfg *Config, db authdb.DB, email, clientID string) error {
// Check the global allowlist in the AuthDB.
switch valid, err := db.IsAllowedOAuthClientID(ctx, email, clientID); {
case err != nil:
return errors.Annotate(err, "failed to check client ID allowlist").Tag(transient.Tag).Err()
case valid:
return nil
}
// It may be an app-specific client ID supplied via cfg.FrontendClientID.
if cfg.FrontendClientID != nil {
switch frontendClientID, err := cfg.FrontendClientID(ctx); {
case err != nil:
return errors.Annotate(err, "failed to grab frontend client ID").Tag(transient.Tag).Err()
case clientID == frontendClientID:
return nil
}
}
logging.Errorf(ctx, "auth: %q is using client_id %q not in the allowlist", email, clientID)
return ErrBadClientID
}
// checkEndUserIP parses the caller IP address and checks it against an
// allowlist (if necessary). Returns ErrBadRemoteAddr if the IP is malformed,
// ErrForbiddenIP if the IP is not allowlisted or a transient error if the check
// itself failed.
func checkEndUserIP(ctx context.Context, cfg *Config, db authdb.DB, r RequestMetadata, peerID identity.Identity) (net.IP, error) {
var ipAddr string
if cfg.EndUserIP != nil {
ipAddr = cfg.EndUserIP(r)
} else {
ipAddr = r.RemoteAddr()
}
peerIP, err := parseRemoteIP(ipAddr)
if err != nil {
logging.Errorf(ctx, "auth: bad remote_addr %q in a call from %q - %s", ipAddr, peerID, err)
return nil, ErrBadRemoteAddr
}
if peerIP.IsUnspecified() {
return peerIP, nil
}
// Some callers may be constrained by an IP allowlist.
switch ipAllowlist, err := db.GetAllowlistForIdentity(ctx, peerID); {
case err != nil:
return nil, errors.Annotate(err, "failed to get IP allowlist for identity %q", peerID).Tag(transient.Tag).Err()
case ipAllowlist != "":
switch allowed, err := db.IsAllowedIP(ctx, peerIP, ipAllowlist); {
case err != nil:
return nil, errors.Annotate(err, "failed to check IP %s is in the allowlist %q", peerIP, ipAllowlist).Tag(transient.Tag).Err()
case !allowed:
return nil, ErrForbiddenIP
}
}
return peerIP, nil
}
// checkDelegationToken checks correctness of a delegation token and returns
// a delegated *User.
func checkDelegationToken(ctx context.Context, cfg *Config, db authdb.DB, token string, peerID identity.Identity) (*User, error) {
// Log the token fingerprint (even before parsing the token), it can be used
// to grab the info about the token from the token server logs.
logging.Fields{
"fingerprint": tokenFingerprint(token),
}.Debugf(ctx, "auth: Received delegation token")
// Need to grab our own identity to verify that the delegation token is
// minted for consumption by us and not some other service.
ownServiceIdentity, err := getOwnServiceIdentity(ctx, cfg.Signer)
if err != nil {
return nil, err
}
delegatedIdentity, err := delegation.CheckToken(ctx, delegation.CheckTokenParams{
Token: token,
PeerID: peerID,
CertificatesProvider: db,
GroupsChecker: db,
OwnServiceIdentity: ownServiceIdentity,
})
if err != nil {
return nil, err
}
// Log that peerID is pretending to be delegatedIdentity.
logging.Fields{
"peerID": peerID,
"delegatedID": delegatedIdentity,
}.Debugf(ctx, "auth: Using delegation")
return &User{Identity: delegatedIdentity}, nil
}
// checkProjectHeader verifies the caller is allowed to use X-Luci-Project
// mechanism and returns a *User (with project-scoped identity) to use for
// the request.
func checkProjectHeader(ctx context.Context, db authdb.DB, project string, peerID identity.Identity) (*User, error) {
// See comment for InternalServicesGroup.
switch yes, err := db.IsMember(ctx, peerID, []string{InternalServicesGroup}); {
case err != nil:
return nil, errors.Annotate(err, "error when checking if %q is in %q", peerID, InternalServicesGroup).Tag(transient.Tag).Err()
case !yes:
return nil, ErrProjectHeaderForbidden
}
// Verify the actual value passes the regexp check.
projIdent, err := identity.MakeIdentity("project:" + project)
if err != nil {
return nil, errors.Annotate(err, "bad %s", XLUCIProjectHeader).Err()
}
// Log that peerID is using project-scoped identity.
logging.Fields{
"peerID": peerID,
"projectID": projIdent,
}.Debugf(ctx, "auth: Using project identity")
return &User{Identity: projIdent}, nil
}
// getOwnServiceIdentity returns 'service:<appID>' identity of the current
// service.
func getOwnServiceIdentity(ctx context.Context, signer signing.Signer) (identity.Identity, error) {
if signer == nil {
return "", ErrNotConfigured
}
switch serviceInfo, err := signer.ServiceInfo(ctx); {
case err != nil:
return "", err
case serviceInfo.AppID == "":
return "", errors.Reason("auth: don't known our own app ID to check the delegation token is for us").Err()
default:
return identity.MakeIdentity("service:" + serviceInfo.AppID)
}
}