You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/internal/daemon/controller/auth/auth.go

965 lines
34 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package auth
import (
"context"
stderrors "errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/hashicorp/boundary/api/recovery"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/auth/ldap"
"github.com/hashicorp/boundary/internal/auth/oidc"
"github.com/hashicorp/boundary/internal/auth/password"
"github.com/hashicorp/boundary/internal/daemon/controller/common"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/event"
authpb "github.com/hashicorp/boundary/internal/gen/controller/auth"
"github.com/hashicorp/boundary/internal/gen/controller/tokens"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/perms"
"github.com/hashicorp/boundary/internal/requests"
"github.com/hashicorp/boundary/internal/server"
"github.com/hashicorp/boundary/internal/types/action"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/boundary/internal/util"
"github.com/hashicorp/boundary/internal/util/template"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/mr-tron/base58"
"google.golang.org/protobuf/proto"
)
type TokenFormat uint32
const (
// We weren't given one or couldn't parse it
AuthTokenTypeUnknown TokenFormat = iota
// Came in via the Authentication: Bearer header
AuthTokenTypeBearer
// Came in via split cookies
AuthTokenTypeSplitCookie
// It's of recovery type
AuthTokenTypeRecoveryKms
)
// CallbackAction represents the action type for
// callback operations in a request's URL path.
// This is currently only used during auth method
// authentication.
const CallbackAction = "callback"
type key int
var verifierKey key
type VerifyResults struct {
UserData template.Data
// This is copied out from UserData above but used to avoid nil checks in
// lots of places that embed this value
UserId string
AuthTokenId string
Error error
Scope *scopes.ScopeInfo
// AuthenticatedFinished means that the request has passed through the
// authentication system successfully. This does _not_ indicate whether a
// token was provided on the request. Requests for `u_anon` will still have
// this set true! This is because if a request has a token that is invalid,
// we fall back to `u_anon` because the request may still be allowed for any
// anonymous user; it simply fails to validate for and look up grants for an
// actual known user.
//
// A good example is when running dev mode twice. The first time you can
// authenticate and get a token which is saved by the token helper. The
// second time, you run a command and it reads the token from the helper.
// That token is no longer valid, but if the action is granted to `u_anon`
// the action should still succeed. What happens internally is that the
// token fails to look up a non-anonymous user, so we fallback to the
// anonymous user, which is the default.
//
// If you want to know if the request had a valid token provided, use a
// switch on UserId. Anything that isn't `u_anon` will have to have had a
// valid token provided. And a valid token will never fall back to `u_anon`.
AuthenticationFinished bool
// RoundTripValue can be set to allow the function performing authentication
// (often accompanied by lookup(s)) to return a result of that lookup to the
// calling function. It is opaque to this package.
RoundTripValue any
// Used for additional verification
v *verifier
// Used to generate a hash of all grants
grants perms.GrantTuples
}
type verifier struct {
iamRepoFn common.IamRepoFactory
authTokenRepoFn common.AuthTokenRepoFactory
serversRepoFn common.ServersRepoFactory
passwordAuthRepoFn common.PasswordAuthRepoFactory
oidcAuthRepoFn common.OidcAuthRepoFactory
ldapAuthRepoFn common.LdapAuthRepoFactory
kms *kms.Kms
requestInfo *authpb.RequestInfo
res *perms.Resource
act action.Type
ctx context.Context
acl perms.ACL
}
// TODO (jefferai 10/2022): NewVerifierContextWithAccounts performs the function
// of NewVerifierContext (see the docs for that function) but with extra
// parameters that can be used to look up account information. This is not
// intended to be a long-lived function; see
// https://hashicorp.atlassian.net/browse/ICU-6571 and
// https://hashicorp.atlassian.net/browse/ICU-6572
//
// This is being added for a quick turnaround purpose and to avoid making large
// numbers of changes to tests when we may do a much bigger refactor; when those
// items are addressed this can be removed.
func NewVerifierContextWithAccounts(ctx context.Context,
iamRepoFn common.IamRepoFactory,
authTokenRepoFn common.AuthTokenRepoFactory,
serversRepoFn common.ServersRepoFactory,
passwordAuthRepoFn common.PasswordAuthRepoFactory,
oidcAuthRepoFn common.OidcAuthRepoFactory,
ldapAuthRepoFn common.LdapAuthRepoFactory,
kms *kms.Kms,
requestInfo *authpb.RequestInfo,
) context.Context {
return context.WithValue(ctx, verifierKey, &verifier{
iamRepoFn: iamRepoFn,
authTokenRepoFn: authTokenRepoFn,
serversRepoFn: serversRepoFn,
passwordAuthRepoFn: passwordAuthRepoFn,
oidcAuthRepoFn: oidcAuthRepoFn,
ldapAuthRepoFn: ldapAuthRepoFn,
kms: kms,
requestInfo: requestInfo,
})
}
// NewVerifierContext creates a context that carries a verifier object from the
// HTTP handlers to the gRPC service handlers. It should only be created in the
// HTTP handler and should exist for every request that reaches the service
// handlers.
func NewVerifierContext(ctx context.Context,
iamRepoFn common.IamRepoFactory,
authTokenRepoFn common.AuthTokenRepoFactory,
serversRepoFn common.ServersRepoFactory,
kms *kms.Kms,
requestInfo *authpb.RequestInfo,
) context.Context {
return NewVerifierContextWithAccounts(ctx, iamRepoFn, authTokenRepoFn, serversRepoFn, nil, nil, nil, kms, requestInfo)
}
// Verify takes in a context that has expected parameters as values and runs an
// authn/authz check. It returns a user ID, the scope ID for the request (which
// may come from the URL and may come from the token) and whether or not to
// proceed, e.g. whether the authn/authz check resulted in failure. If an error
// occurs it's logged to the system log.
func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) {
const op = "auth.Verify"
ret.Error = handlers.ForbiddenError()
v, ok := ctx.Value(verifierKey).(*verifier)
if !ok {
// We don't have a logger yet and this should never happen in any
// context we won't catch in tests
panic("no verifier information found in context")
}
ret.v = v
v.ctx = ctx
ea := &event.Auth{}
defer func() {
if err := event.WriteAudit(ctx, op, event.WithAuth(ea)); err != nil {
event.WriteError(ctx, op, err)
}
}()
opts := getOpts(opt...)
ret.Scope = new(scopes.ScopeInfo)
// Note: we don't call RequestContextFromCtx here because that performs a
// SelfOrDefault. That's useful in the handlers, but here we don't want to
// do anything if it's nil. That's mainly useful in tests where
// DisableAuthEntirely is set. Later, if we don't have the value set at all,
// we have some safeguards to ensure we fail safe (e.g. no output fields at
// all). So it provides a good indication as well whether we have set this
// where and when needed.
var reqInfo *requests.RequestContext
reqInfoRaw := ctx.Value(requests.ContextRequestInformationKey)
if reqInfoRaw != nil {
reqInfo = reqInfoRaw.(*requests.RequestContext)
}
// In tests we often simply disable auth so we can test the service handlers
// without fuss
if v.requestInfo.DisableAuthEntirely {
yes := true
ea.DisabledAuthEntirely = &yes
const op = "auth.(disabled).lookupScope"
ret.Scope.Id = v.requestInfo.ScopeIdOverride
if ret.Scope.Id == "" {
ret.Scope.Id = opts.withScopeId
}
// Look up scope details to return. We can skip a lookup when using the
// global scope
switch ret.Scope.Id {
case scope.Global.String():
ret.Scope = &scopes.ScopeInfo{
Id: scope.Global.String(),
Type: scope.Global.String(),
Name: scope.Global.String(),
Description: "Global Scope",
ParentScopeId: "",
}
default:
iamRepo, err := v.iamRepoFn()
if err != nil {
ret.Error = errors.Wrap(ctx, err, op, errors.WithMsg("failed to get iam repo"))
return
}
scp, err := iamRepo.LookupScope(v.ctx, ret.Scope.Id)
if err != nil {
ret.Error = errors.Wrap(ctx, err, op)
return
}
if scp == nil {
ret.Error = errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("non-existent scope %q", ret.Scope.Id))
return
}
ret.Scope = &scopes.ScopeInfo{
Id: scp.GetPublicId(),
Type: scp.GetType(),
Name: scp.GetName(),
Description: scp.GetDescription(),
ParentScopeId: scp.GetParentId(),
}
}
ret.UserId = v.requestInfo.UserIdOverride
ret.UserData.User.Id = util.Pointer(ret.UserId)
if reqInfo != nil {
reqInfo.UserId = ret.UserId
}
ea.UserInfo = &event.UserInfo{UserId: ret.UserId}
ret.Error = nil
return
}
v.act = opts.withAction
v.res = &perms.Resource{
ScopeId: opts.withScopeId,
Id: opts.withId,
Pin: opts.withPin,
Type: opts.withType,
// Parent Scope ID will be filled in via performAuthCheck
}
// Global scope has no parent ID; account for this
if opts.withId == scope.Global.String() && opts.withType == resource.Scope {
v.res.ScopeId = scope.Global.String()
}
if v.requestInfo.EncryptedToken != "" {
v.decryptToken(ctx)
}
var authResults perms.ACLResults
var userData template.Data
var err error
authResults, ret.UserData, ret.Scope, v.acl, ret.grants, err = v.performAuthCheck(ctx)
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error performing authn/authz check"))
return
}
if ret.UserData.User.Id != nil {
ret.UserId = *ret.UserData.User.Id
}
ret.AuthTokenId = v.requestInfo.PublicId
ret.AuthenticationFinished = authResults.AuthenticationFinished
if !authResults.Authorized {
if v.requestInfo.DisableAuthzFailures {
ret.Error = nil
// TODO: Decide whether to remove this
err := event.WriteObservation(ctx, op,
event.WithHeader(
"auth-results", struct {
Msg string `json:"msg"`
Resource *perms.Resource `json:"resource"`
UserId string `json:"user_id"`
Action string `json:"action"`
}{
Msg: "failed authz info for request",
Resource: v.res,
UserId: ret.UserId,
Action: v.act.String(),
}))
if err != nil {
event.WriteError(ctx, op, err)
}
} else {
// If the anon user was used (either no token, or invalid (perhaps
// expired) token), return a 401. That way if it's an authn'd user
// that is not authz'd we'll return 403 to be explicit.
if ret.UserId == globals.AnonymousUserId {
ret.Error = handlers.UnauthenticatedError()
}
ea.UserInfo = &event.UserInfo{
UserId: ret.UserId,
}
return
}
}
grants := make([]event.Grant, 0, len(ret.grants))
for _, g := range ret.grants {
grants = append(grants, event.Grant{
Grant: g.Grant,
RoleId: g.RoleId,
ScopeId: g.GrantScopeId,
})
}
ea.UserInfo = &event.UserInfo{
UserId: ret.UserId,
}
if userData.Account.Id != nil {
ea.UserInfo.AuthAccountId = *userData.Account.Id
}
ea.GrantsInfo = &event.GrantsInfo{
Grants: grants,
}
if userData.User.FullName != nil {
ea.UserName = *userData.User.FullName
}
if userData.User.Email != nil {
ea.UserEmail = *userData.User.Email
}
if reqInfo != nil {
reqInfo.UserId = ret.UserId
reqInfo.OutputFields = authResults.OutputFields
}
ret.Error = nil
return
}
func (v *verifier) decryptToken(ctx context.Context) {
const op = "auth.(verifier).decryptToken"
switch v.requestInfo.TokenFormat {
case uint32(AuthTokenTypeUnknown):
// Nothing to decrypt
return
case uint32(AuthTokenTypeBearer), uint32(AuthTokenTypeSplitCookie):
if v.kms == nil {
event.WriteError(ctx, op, stderrors.New("no KMS object available to authz system"))
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
tokenRepo, err := v.authTokenRepoFn()
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("failed to get authtoken repo"))
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
at, err := tokenRepo.LookupAuthToken(v.ctx, v.requestInfo.PublicId)
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("failed to look up auth token by public ID"))
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
if at == nil {
event.WriteError(ctx, op, stderrors.New("nil result from looking up auth token by public ID"))
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
tokenWrapper, err := v.kms.GetWrapper(v.ctx, at.GetScopeId(), kms.KeyPurposeTokens)
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("unable to get wrapper for tokens; continuing as anonymous user"))
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
version := v.requestInfo.EncryptedToken[0:len(globals.ServiceTokenV1)]
switch version {
case globals.ServiceTokenV1:
default:
event.WriteError(ctx, op, stderrors.New("unknown token encryption version; continuing as anonymous user"), event.WithInfo("version", version))
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
marshaledToken, err := base58.FastBase58Decoding(v.requestInfo.EncryptedToken[len(globals.ServiceTokenV1):])
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error unmarshaling base58 token; continuing as anonymous user"))
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
blobInfo := new(wrapping.BlobInfo)
if err := proto.Unmarshal(marshaledToken, blobInfo); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error decoding encrypted token; continuing as anonymous user"))
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
s1Bytes, err := tokenWrapper.Decrypt(v.ctx, blobInfo, wrapping.WithAad([]byte(v.requestInfo.PublicId)))
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error decrypting encrypted token; continuing as anonymous user"))
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
var s1Info tokens.S1TokenInfo
if err := proto.Unmarshal(s1Bytes, &s1Info); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error unmarshaling token info; continuing as anonymous user"))
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
if v.requestInfo.TokenFormat == uint32(AuthTokenTypeUnknown) || s1Info.Token == "" || v.requestInfo.PublicId == "" {
event.WriteError(ctx, op, stderrors.New("after parsing, could not find valid token; continuing as anonymous user"))
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
v.requestInfo.Token = s1Info.Token
return
case uint32(AuthTokenTypeRecoveryKms):
if v.kms == nil {
event.WriteError(ctx, op, stderrors.New("no KMS object available to authz system"))
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
wrapper := v.kms.GetExternalWrappers(ctx).Recovery()
if wrapper == nil {
event.WriteError(ctx, op, stderrors.New("no recovery KMS is available"))
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
info, err := recovery.ParseRecoveryToken(v.ctx, wrapper, v.requestInfo.EncryptedToken)
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("decrypt recovery token: error parsing and validating recovery token"))
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
// If we add the validity period to the creation time (which we've
// verified is before the current time, with a minute of fudging), and
// it's before now, it's expired and might be a replay.
if info.CreationTime.Add(globals.RecoveryTokenValidityPeriod).Before(time.Now()) {
event.WriteError(ctx, op, stderrors.New("recovery token has expired (possible replay attack)"))
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
repo, err := v.serversRepoFn()
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("decrypt recovery token: error fetching server repo"))
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
if err := repo.AddNonce(v.ctx, info.Nonce, server.NoncePurposeRecovery); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("decrypt recovery token: error adding nonce to database (possible replay attack)"))
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
event.WriteError(ctx, op, stderrors.New("recovery KMS was used to authorize a call"), event.WithInfo("url", v.requestInfo.Path, "method", v.requestInfo.Method))
}
}
func (v verifier) performAuthCheck(ctx context.Context) (
aclResults perms.ACLResults,
userData template.Data,
scopeInfo *scopes.ScopeInfo,
retAcl perms.ACL,
grantTuples []perms.GrantTuple,
retErr error,
) {
const op = "auth.(verifier).performAuthCheck"
// Ensure we return an error by default if we forget to set this somewhere
retErr = errors.New(ctx, errors.Unknown, op, "default auth error", errors.WithoutEvent())
// Make the linter happy
_ = retErr
scopeInfo = new(scopes.ScopeInfo)
// This will always be set, so further down below we can switch on whether
// it's empty
userData.User.Id = util.Pointer(globals.AnonymousUserId)
// Validate the token and fetch the corresponding user ID
switch v.requestInfo.TokenFormat {
case uint32(AuthTokenTypeUnknown):
// Nothing; remain as the anonymous user
case uint32(AuthTokenTypeRecoveryKms):
// We validated the encrypted token in decryptToken and handled the
// nonces there, so just set the user
userData.User.Id = util.Pointer(globals.RecoveryUserId)
case uint32(AuthTokenTypeBearer), uint32(AuthTokenTypeSplitCookie):
if v.requestInfo.Token == "" {
// This will end up staying as the anonymous user
break
}
tokenRepo, err := v.authTokenRepoFn()
if err != nil {
retErr = errors.Wrap(ctx, err, op)
return
}
at, err := tokenRepo.ValidateToken(v.ctx, v.requestInfo.PublicId, v.requestInfo.Token)
if err != nil {
// Continue as the anonymous user as maybe this token is expired but
// we can still perform the action
event.WriteError(ctx, op, err, event.WithInfoMsg("error validating token; continuing as anonymous user"))
break
}
if at != nil {
userData.Account.Id = util.Pointer(at.GetAuthAccountId())
userData.User.Id = util.Pointer(at.GetIamUserId())
if *userData.User.Id == "" {
event.WriteError(ctx, op, stderrors.New("perform auth check: valid token did not map to a user, likely because no account is associated with the user any longer; continuing as u_anon"), event.WithInfo("token_id", at.GetPublicId()))
userData.User.Id = util.Pointer(globals.AnonymousUserId)
userData.Account.Id = nil
}
}
}
iamRepo, err := v.iamRepoFn()
if err != nil {
retErr = errors.Wrap(ctx, err, op, errors.WithMsg("failed to get iam repo"))
return
}
u, _, err := iamRepo.LookupUser(ctx, *userData.User.Id)
if err != nil {
retErr = errors.Wrap(ctx, err, op, errors.WithMsg("failed to lookup user"))
return
}
userData.User.Name = util.Pointer(u.Name)
userData.User.Email = util.Pointer(u.Email)
userData.User.FullName = util.Pointer(u.FullName)
if userData.Account.Id != nil && *userData.Account.Id != "" && v.passwordAuthRepoFn != nil && v.oidcAuthRepoFn != nil && v.ldapAuthRepoFn != nil {
const domain = "auth"
var acct auth.Account
var err error
switch globals.ResourceInfoFromPrefix(*userData.Account.Id).Subtype {
case password.Subtype:
repo, repoErr := v.passwordAuthRepoFn()
if repoErr != nil {
retErr = errors.Wrap(ctx, repoErr, op, errors.WithMsg("failed to get password auth repo"))
return
}
acct, err = repo.LookupAccount(ctx, *userData.Account.Id)
case oidc.Subtype:
repo, repoErr := v.oidcAuthRepoFn()
if repoErr != nil {
retErr = errors.Wrap(ctx, repoErr, op, errors.WithMsg("failed to get oidc auth repo"))
return
}
acct, err = repo.LookupAccount(ctx, *userData.Account.Id)
case ldap.Subtype:
repo, repoErr := v.ldapAuthRepoFn()
if repoErr != nil {
retErr = errors.Wrap(ctx, repoErr, op, errors.WithMsg("failed to get ldap auth repo"))
return
}
acct, err = repo.LookupAccount(ctx, *userData.Account.Id)
default:
retErr = errors.Wrap(ctx, err, op, errors.WithMsg("unrecognized account id type"))
return
}
if err != nil {
if errors.IsNotFoundError(err) {
retErr = errors.Wrap(ctx, err, op, errors.WithMsg("account doesn't exist"))
return
}
retErr = errors.Wrap(ctx, err, op, errors.WithMsg("error looking up account"))
return
}
userData.Account.Name = util.Pointer(acct.GetName())
userData.Account.Email = util.Pointer(acct.GetEmail())
userData.Account.LoginName = util.Pointer(acct.GetLoginName())
userData.Account.Subject = util.Pointer(acct.GetSubject())
}
// Look up scope details to return. We can skip a lookup when using the
// global scope
switch v.res.ScopeId {
case scope.Global.String():
scopeInfo = &scopes.ScopeInfo{
Id: scope.Global.String(),
Type: scope.Global.String(),
Name: scope.Global.String(),
Description: "Global Scope",
ParentScopeId: "",
}
default:
scp, err := iamRepo.LookupScope(v.ctx, v.res.ScopeId)
if err != nil {
retErr = errors.Wrap(ctx, err, op)
return
}
if scp == nil {
retErr = errors.New(ctx, errors.InvalidParameter, op, fmt.Sprint("non-existent scope $q", v.res.ScopeId))
return
}
scopeInfo = &scopes.ScopeInfo{
Id: scp.GetPublicId(),
Type: scp.GetType(),
Name: scp.GetName(),
Description: scp.GetDescription(),
ParentScopeId: scp.GetParentId(),
}
}
v.res.ParentScopeId = scopeInfo.ParentScopeId
// At this point we don't need to look up grants since it's automatically allowed
if v.requestInfo.TokenFormat == uint32(AuthTokenTypeRecoveryKms) {
aclResults.AuthenticationFinished = true
aclResults.Authorized = true
retErr = nil
return
}
var parsedGrants []perms.Grant
// Fetch and parse grants for this user ID (which may include grants for
// u_anon and u_auth)
grantTuples, err = iamRepo.GrantsForUser(v.ctx, *userData.User.Id)
if err != nil {
retErr = errors.Wrap(ctx, err, op)
return
}
parsedGrants = make([]perms.Grant, 0, len(grantTuples))
// Note: Below, we always skip validation so that we don't error on formats
// that we've since restricted, e.g. "ids=foo;actions=create,read". These
// will simply not have an effect.
for _, tuple := range grantTuples {
permsOpts := []perms.Option{
perms.WithUserId(*userData.User.Id),
perms.WithSkipFinalValidation(true),
}
if userData.Account.Id != nil {
permsOpts = append(permsOpts, perms.WithAccountId(*userData.Account.Id))
}
parsed, err := perms.Parse(
ctx,
tuple,
permsOpts...)
if err != nil {
retErr = errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed to parse grant %#v", tuple.Grant)))
return
}
parsedGrants = append(parsedGrants, parsed)
}
retAcl = perms.NewACL(parsedGrants...)
aclResults = retAcl.Allowed(*v.res, v.act, *userData.User.Id)
// We don't set authenticated above because setting this but not authorized
// is used for further permissions checks, such as during recursive listing.
// So we want to make sure any code relying on that has the full set of
// grants successfully loaded.
aclResults.AuthenticationFinished = true
retErr = nil
return
}
// FetchActionSetForId returns the allowed actions for a given ID using the
// current set of ACLs and all other parameters the same (user, etc.)
func (r *VerifyResults) FetchActionSetForId(ctx context.Context, id string, availableActions action.ActionSet, opt ...Option) action.ActionSet {
return r.fetchActions(id, resource.Unknown, availableActions, opt...)
}
// FetchActionSetForType returns the allowed actions for a given collection type
// using the current set of ACLs and all other parameters the same (user, etc.)
func (r *VerifyResults) FetchActionSetForType(ctx context.Context, typ resource.Type, availableActions action.ActionSet, opt ...Option) action.ActionSet {
return r.fetchActions("", typ, availableActions, opt...)
}
func (r *VerifyResults) fetchActions(id string, typ resource.Type, availableActions action.ActionSet, opt ...Option) action.ActionSet {
switch {
case r.v.requestInfo.DisableAuthEntirely,
r.v.requestInfo.TokenFormat == uint32(AuthTokenTypeRecoveryKms):
// TODO: See if we can be better about what we return with the anonymous
// user and recovery KMS, perhaps given exclusionary options for each
return availableActions
}
// If there is no user ID set by definition there are no actions to fetch.
// This shouldn't happen because we should always fall back to at least the
// anonymous user so it's defense in depth.
if r.UserData.User.Id == nil {
return nil
}
opts := getOpts(opt...)
res := opts.withResource
// If not passed in, use what's already been populated through verification
if res == nil {
res = r.v.res
}
// If this is being called directly we may not have a resource yet
if res == nil {
res = new(perms.Resource)
}
if id != "" {
res.Id = id
}
if typ != resource.Unknown {
res.Type = typ
}
ret := make(action.ActionSet, len(availableActions))
for act := range availableActions {
if r.v.acl.Allowed(*res, act, *r.UserData.User.Id).Authorized {
ret.Add(act)
}
}
if len(ret) == 0 {
return nil
}
return ret
}
func (r *VerifyResults) FetchOutputFields(res perms.Resource, act action.Type) *perms.OutputFields {
var ret *perms.OutputFields
switch {
case r.v.requestInfo.TokenFormat == uint32(AuthTokenTypeRecoveryKms):
return ret.AddFields([]string{"*"})
case r.v.requestInfo.DisableAuthEntirely:
return ret
case r.UserData.User.Id == nil:
// If there is no user ID set by definition there are no actions to fetch.
// This shouldn't happen because we should always fall back to at least the
// anonymous user so it's defense in depth.
return ret
}
return r.v.acl.Allowed(res, act, *r.UserData.User.Id).OutputFields
}
// ACL returns the perms.ACL of the verifier.
func (r *VerifyResults) ACL() perms.ACL {
if r.v == nil {
return perms.ACL{}
}
return r.v.acl
}
// GetTokenFromRequest pulls the token from either the Authorization header or
// split cookies and parses it. If it cannot be parsed successfully, the issue
// is logged and we return blank, so logic will continue as the anonymous user.
// The public ID and _encrypted_ token are returned along with the token format.
func GetTokenFromRequest(ctx context.Context, kmsCache *kms.Kms, req *http.Request) (string, string, uint32) {
const op = "auth.GetTokenFromRequest"
// First, get the token, either from the authorization header or from split
// cookies
var receivedTokenType TokenFormat
var fullToken string
if authHeader := req.Header.Get("Authorization"); authHeader != "" {
headerSplit := strings.SplitN(strings.TrimSpace(authHeader), " ", 2)
if len(headerSplit) == 2 && strings.EqualFold(strings.TrimSpace(headerSplit[0]), "bearer") {
receivedTokenType = AuthTokenTypeBearer
fullToken = strings.TrimSpace(headerSplit[1])
}
}
if receivedTokenType != AuthTokenTypeBearer {
var httpCookiePayload string
var jsCookiePayload string
if hc, err := req.Cookie(handlers.HttpOnlyCookieName); err == nil {
httpCookiePayload = hc.Value
}
if jc, err := req.Cookie(handlers.JsVisibleCookieName); err == nil {
jsCookiePayload = jc.Value
}
if httpCookiePayload != "" && jsCookiePayload != "" {
receivedTokenType = AuthTokenTypeSplitCookie
fullToken = jsCookiePayload + httpCookiePayload
}
}
if receivedTokenType == AuthTokenTypeUnknown || fullToken == "" {
// We didn't find auth info or a client screwed up and put in a blank
// header instead of nothing at all, so return blank which will indicate
// the anonymous user
return "", "", uint32(AuthTokenTypeUnknown)
}
if strings.HasPrefix(fullToken, "r_") {
return "", fullToken, uint32(AuthTokenTypeRecoveryKms)
}
splitFullToken := strings.Split(fullToken, "_")
if len(splitFullToken) != 3 {
event.WriteError(ctx, op, stderrors.New("unexpected number of segments in token; continuing as anonymous user"), event.WithInfo("expected", 3, "found", len(splitFullToken)))
return "", "", uint32(AuthTokenTypeUnknown)
}
publicId := strings.Join(splitFullToken[0:2], "_")
encryptedToken := splitFullToken[2]
return publicId, encryptedToken, uint32(receivedTokenType)
}
// ScopesAuthorizedForList retrieves and returns all scopes where a user is authorized
// to perform a *list* action on. It looks recursively from `rootScopeId`.
func (r *VerifyResults) ScopesAuthorizedForList(ctx context.Context, rootScopeId string, resourceType resource.Type) (map[string]*scopes.ScopeInfo, error) {
const op = "auth.(VerifyResults).ScopesAuthorizedForList"
// Validation
switch {
case resourceType == resource.Unknown:
return nil, errors.New(ctx, errors.InvalidParameter, op, "unknown resource")
case r.v.iamRepoFn == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil iam repo")
case rootScopeId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing root scope id")
case r.Scope == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil scope in auth results")
}
repo, err := r.v.iamRepoFn()
if err != nil {
return nil, err
}
// Get all scopes recursively. Start at global because we need to take into
// account permissions in parent scopes even if they want to scale back the
// returned information to a child scope and its children.
scps, err := repo.ListScopesRecursively(ctx, scope.Global.String())
if err != nil {
return nil, err
}
var deferredScopes []*iam.Scope
var globalHasList bool // Store whether global has list permission
scopeResourceMap := make(map[string]*scopes.ScopeInfo) // Scope data per scope id
// For each scope, see if we have permission to list that resource in that scope
for _, scp := range scps {
scpId := scp.GetPublicId()
aSet := r.FetchActionSetForType(ctx,
resource.Unknown, // This is overridden by `WithResource` option.
action.NewActionSet(action.List),
WithResource(&perms.Resource{Type: resourceType, ScopeId: scpId, ParentScopeId: scp.GetParentId()}),
)
// We only expect the action set to be nothing, or list. In case
// this is not the case, we bail out.
switch {
case len(aSet) == 0:
// Defer until we've read all scopes. We do this because if the
// ordering coming back isn't in parent-first ordering our map
// lookup might fail.
deferredScopes = append(deferredScopes, scp)
case len(aSet) == 1 || r.UserId == globals.RecoveryUserId:
if !aSet.HasAction(action.List) {
return nil, errors.New(ctx, errors.Internal, op, "unexpected action in set")
}
if scopeResourceMap[scpId] == nil {
scopeResourceMap[scpId] = &scopes.ScopeInfo{
Id: scp.GetPublicId(),
Type: scp.GetType(),
Name: scp.GetName(),
Description: scp.GetDescription(),
ParentScopeId: scp.GetParentId(),
}
}
if scpId == scope.Global.String() {
globalHasList = true
}
default:
return nil, errors.New(ctx, errors.Internal, op, "unexpected number of actions back in set")
}
}
// Now go through the deferred scopes and see if a parent matches
for _, scp := range deferredScopes {
// If they had list on global scope anything else is automatically
// included; otherwise if they had list on the parent scope, this
// scope is included in the map and is sufficient here.
if globalHasList || scopeResourceMap[scp.GetParentId()] != nil {
scpId := scp.GetPublicId()
if scopeResourceMap[scpId] == nil {
scopeResourceMap[scpId] = &scopes.ScopeInfo{
Id: scp.GetPublicId(),
Type: scp.GetType(),
Name: scp.GetName(),
Description: scp.GetDescription(),
ParentScopeId: scp.GetParentId(),
}
}
}
}
// Elide out any that aren't under the root scope id
elideScopes := make([]string, 0, len(scopeResourceMap))
for scpId, scp := range scopeResourceMap {
switch rootScopeId {
// If the root is global, it matches
case scope.Global.String():
// If the current scope matches the root, it matches
case scpId:
// Or if the parent of this scope is the root (for orgs that would mean
// a root scope ID which is covered in the case above, so this is really
// projects matching an org used as the root)
case scp.ParentScopeId:
default:
elideScopes = append(elideScopes, scpId)
}
}
for _, scpId := range elideScopes {
delete(scopeResourceMap, scpId)
}
// If we have nothing in scopeInfoMap at this point, we aren't authorized
// anywhere so return 403.
if len(scopeResourceMap) == 0 {
return nil, handlers.ForbiddenError()
}
return scopeResourceMap, nil
}
// GrantsHash returns a stable hash of all the grants in the verify results.
func (r *VerifyResults) GrantsHash(ctx context.Context) ([]byte, error) {
return r.grants.GrantHash(ctx)
}
// GetRequestInfo extracts the request info stored in the context, if it exists.
// This returns nil, false if the request info could not be found.
func GetRequestInfo(ctx context.Context) (*authpb.RequestInfo, bool) {
v, ok := ctx.Value(verifierKey).(*verifier)
if !ok {
return nil, false
}
return v.requestInfo, true
}