Subtype Id registration moved to its own package (#1416)
* Allow repo packages to register subtypes and id prefixes. * Refactor out request authentication logic to a new package as a sibling to handler.
This commit is contained in:
1 parent
ab6f3eaeb4
commit
10e8aeae4b
62 files changed
+603
-597
No files matched your search
@@ -0,0 +1,648 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/globals"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/gen/controller/api/resources/scopes"
|
||||
"github.com/hashicorp/boundary/internal/gen/controller/tokens"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/perms"
|
||||
"github.com/hashicorp/boundary/internal/requests"
|
||||
"github.com/hashicorp/boundary/internal/servers/controller/common"
|
||||
"github.com/hashicorp/boundary/internal/servers/controller/handlers"
|
||||
"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/sdk/recovery"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping"
|
||||
"github.com/kr/pretty"
|
||||
"github.com/mr-tron/base58"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type TokenFormat int
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
const (
|
||||
AnonymousUserId = "u_anon"
|
||||
)
|
||||
|
||||
type key int
|
||||
|
||||
var verifierKey key
|
||||
|
||||
// RequestInfo contains request parameters necessary for checking authn/authz
|
||||
type RequestInfo struct {
|
||||
Path string
|
||||
Method string
|
||||
PublicId string
|
||||
EncryptedToken string
|
||||
Token string
|
||||
TokenFormat TokenFormat
|
||||
|
||||
// The following are useful for tests
|
||||
scopeIdOverride string
|
||||
userIdOverride string
|
||||
DisableAuthzFailures bool
|
||||
DisableAuthEntirely bool
|
||||
}
|
||||
|
||||
type VerifyResults struct {
|
||||
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 interface{}
|
||||
|
||||
// Used for additional verification
|
||||
v *verifier
|
||||
}
|
||||
|
||||
type verifier struct {
|
||||
logger hclog.Logger
|
||||
iamRepoFn common.IamRepoFactory
|
||||
authTokenRepoFn common.AuthTokenRepoFactory
|
||||
serversRepoFn common.ServersRepoFactory
|
||||
kms *kms.Kms
|
||||
requestInfo RequestInfo
|
||||
res *perms.Resource
|
||||
act action.Type
|
||||
ctx context.Context
|
||||
acl perms.ACL
|
||||
}
|
||||
|
||||
// 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,
|
||||
logger hclog.Logger,
|
||||
iamRepoFn common.IamRepoFactory,
|
||||
authTokenRepoFn common.AuthTokenRepoFactory,
|
||||
serversRepoFn common.ServersRepoFactory,
|
||||
kms *kms.Kms,
|
||||
requestInfo RequestInfo) context.Context {
|
||||
return context.WithValue(ctx, verifierKey, &verifier{
|
||||
logger: logger,
|
||||
iamRepoFn: iamRepoFn,
|
||||
authTokenRepoFn: authTokenRepoFn,
|
||||
serversRepoFn: serversRepoFn,
|
||||
kms: kms,
|
||||
requestInfo: 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) {
|
||||
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
|
||||
|
||||
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 {
|
||||
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 "global":
|
||||
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(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(err, op)
|
||||
return
|
||||
}
|
||||
if scp == nil {
|
||||
ret.Error = errors.New(errors.InvalidParameter, op, fmt.Sprint("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
|
||||
if reqInfo != nil {
|
||||
reqInfo.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,
|
||||
}
|
||||
// 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()
|
||||
}
|
||||
|
||||
var authResults perms.ACLResults
|
||||
var err error
|
||||
authResults, ret.UserId, ret.Scope, v.acl, err = v.performAuthCheck()
|
||||
if err != nil {
|
||||
v.logger.Error("error performing authn/authz check", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
v.logger.Info("failed authz info for request", "resource", pretty.Sprint(v.res), "user_id", ret.UserId, "action", v.act.String())
|
||||
} 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 == AnonymousUserId {
|
||||
ret.Error = handlers.UnauthenticatedError()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if reqInfo != nil {
|
||||
reqInfo.UserId = ret.UserId
|
||||
reqInfo.OutputFields = authResults.OutputFields
|
||||
}
|
||||
|
||||
ret.Error = nil
|
||||
return
|
||||
}
|
||||
|
||||
func (v *verifier) decryptToken() {
|
||||
switch v.requestInfo.TokenFormat {
|
||||
case AuthTokenTypeUnknown:
|
||||
// Nothing to decrypt
|
||||
return
|
||||
|
||||
case AuthTokenTypeBearer, AuthTokenTypeSplitCookie:
|
||||
if v.kms == nil {
|
||||
v.logger.Trace("decrypt token: no KMS object available to authz system")
|
||||
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
|
||||
return
|
||||
}
|
||||
|
||||
tokenRepo, err := v.authTokenRepoFn()
|
||||
if err != nil {
|
||||
v.logger.Warn("decrypt bearer token: failed to get authtoken repo", "error", err)
|
||||
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
|
||||
return
|
||||
}
|
||||
|
||||
at, err := tokenRepo.LookupAuthToken(v.ctx, v.requestInfo.PublicId)
|
||||
if err != nil {
|
||||
v.logger.Trace("decrypt bearer token: failed to look up auth token by public ID", "error", err)
|
||||
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
|
||||
return
|
||||
}
|
||||
if at == nil {
|
||||
v.logger.Trace("decrypt bearer token: nil result from looking up auth token by public ID")
|
||||
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
|
||||
return
|
||||
}
|
||||
|
||||
tokenWrapper, err := v.kms.GetWrapper(v.ctx, at.GetScopeId(), kms.KeyPurposeTokens)
|
||||
if err != nil {
|
||||
v.logger.Warn("decrypt bearer token: unable to get wrapper for tokens; continuing as anonymous user", "error", err)
|
||||
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
|
||||
return
|
||||
}
|
||||
|
||||
version := v.requestInfo.EncryptedToken[0:len(globals.ServiceTokenV1)]
|
||||
switch version {
|
||||
case globals.ServiceTokenV1:
|
||||
default:
|
||||
v.logger.Trace("decrypt bearer token: unknown token encryption version; continuing as anonymous user", "version", version)
|
||||
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
|
||||
return
|
||||
}
|
||||
marshaledToken, err := base58.FastBase58Decoding(v.requestInfo.EncryptedToken[len(globals.ServiceTokenV1):])
|
||||
if err != nil {
|
||||
v.logger.Trace("decrypt bearer token: error unmarshaling base58 token; continuing as anonymous user", "error", err)
|
||||
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
|
||||
return
|
||||
}
|
||||
|
||||
blobInfo := new(wrapping.EncryptedBlobInfo)
|
||||
if err := proto.Unmarshal(marshaledToken, blobInfo); err != nil {
|
||||
v.logger.Trace("decrypt bearer token: error decoding encrypted token; continuing as anonymous user", "error", err)
|
||||
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
|
||||
return
|
||||
}
|
||||
|
||||
s1Bytes, err := tokenWrapper.Decrypt(v.ctx, blobInfo, []byte(v.requestInfo.PublicId))
|
||||
if err != nil {
|
||||
v.logger.Trace("decrypt bearer token: error decrypting encrypted token; continuing as anonymous user", "error", err)
|
||||
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
|
||||
return
|
||||
}
|
||||
|
||||
var s1Info tokens.S1TokenInfo
|
||||
if err := proto.Unmarshal(s1Bytes, &s1Info); err != nil {
|
||||
v.logger.Trace("decrypt bearer token: error unmarshaling token info; continuing as anonymous user", "error", err)
|
||||
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
|
||||
return
|
||||
}
|
||||
|
||||
if v.requestInfo.TokenFormat == AuthTokenTypeUnknown || s1Info.Token == "" || v.requestInfo.PublicId == "" {
|
||||
v.logger.Trace("decrypt bearer token: after parsing, could not find valid token; continuing as anonymous user")
|
||||
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
|
||||
return
|
||||
}
|
||||
|
||||
v.requestInfo.Token = s1Info.Token
|
||||
return
|
||||
|
||||
case AuthTokenTypeRecoveryKms:
|
||||
if v.kms == nil {
|
||||
v.logger.Trace("decrypt recovery token: no KMS object available to authz system")
|
||||
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
|
||||
return
|
||||
}
|
||||
wrapper := v.kms.GetExternalWrappers().Recovery()
|
||||
if wrapper == nil {
|
||||
v.logger.Trace("decrypt recovery token: no recovery KMS is available")
|
||||
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
|
||||
return
|
||||
}
|
||||
info, err := recovery.ParseRecoveryToken(v.ctx, wrapper, v.requestInfo.EncryptedToken)
|
||||
if err != nil {
|
||||
v.logger.Trace("decrypt recovery token: error parsing and validating recovery token", "error", err)
|
||||
v.requestInfo.TokenFormat = 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()) {
|
||||
v.logger.Warn("decrypt recovery token: recovery token has expired (possible replay attack)")
|
||||
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
|
||||
return
|
||||
}
|
||||
repo, err := v.serversRepoFn()
|
||||
if err != nil {
|
||||
v.logger.Trace("decrypt recovery token: error fetching servers repo", "error", err)
|
||||
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
|
||||
return
|
||||
}
|
||||
if err := repo.AddRecoveryNonce(v.ctx, info.Nonce); err != nil {
|
||||
v.logger.Warn("decrypt recovery token: error adding nonce to database (possible replay attack)", "error", err)
|
||||
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
|
||||
return
|
||||
}
|
||||
v.logger.Warn("recovery KMS was used to authorize a call", "url", v.requestInfo.Path, "method", v.requestInfo.Method)
|
||||
}
|
||||
}
|
||||
|
||||
func (v verifier) performAuthCheck() (aclResults perms.ACLResults, userId string, scopeInfo *scopes.ScopeInfo, retAcl perms.ACL, retErr error) {
|
||||
const op = "auth.(verifier).performAuthCheck"
|
||||
// Ensure we return an error by default if we forget to set this somewhere
|
||||
retErr = errors.New(errors.Unknown, op, "default auth error")
|
||||
// Make the linter happy
|
||||
_ = retErr
|
||||
scopeInfo = new(scopes.ScopeInfo)
|
||||
userId = AnonymousUserId
|
||||
var accountId string
|
||||
|
||||
// Validate the token and fetch the corresponding user ID
|
||||
switch v.requestInfo.TokenFormat {
|
||||
case AuthTokenTypeUnknown:
|
||||
// Nothing; remain as the anonymous user
|
||||
|
||||
case AuthTokenTypeRecoveryKms:
|
||||
// We validated the encrypted token in decryptToken and handled the
|
||||
// nonces there, so just set the user
|
||||
userId = "u_recovery"
|
||||
|
||||
case AuthTokenTypeBearer, 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(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
|
||||
v.logger.Error("perform auth check: error validating token; continuing as anonymous user", "error", err)
|
||||
break
|
||||
}
|
||||
if at != nil {
|
||||
accountId = at.GetAuthAccountId()
|
||||
userId = at.GetIamUserId()
|
||||
if userId == "" {
|
||||
v.logger.Warn("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", "token_id", at.GetPublicId())
|
||||
userId = AnonymousUserId
|
||||
accountId = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
iamRepo, err := v.iamRepoFn()
|
||||
if err != nil {
|
||||
retErr = errors.Wrap(err, op, errors.WithMsg("failed to get iam repo"))
|
||||
return
|
||||
}
|
||||
|
||||
// Look up scope details to return. We can skip a lookup when using the
|
||||
// global scope
|
||||
switch v.res.ScopeId {
|
||||
case "global":
|
||||
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(err, op)
|
||||
return
|
||||
}
|
||||
if scp == nil {
|
||||
retErr = errors.New(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(),
|
||||
}
|
||||
}
|
||||
|
||||
// At this point we don't need to look up grants since it's automatically allowed
|
||||
if v.requestInfo.TokenFormat == AuthTokenTypeRecoveryKms {
|
||||
aclResults.AuthenticationFinished = true
|
||||
aclResults.Authorized = true
|
||||
retErr = nil
|
||||
return
|
||||
}
|
||||
|
||||
var parsedGrants []perms.Grant
|
||||
var grantTuples []perms.GrantTuple
|
||||
|
||||
// Fetch and parse grants for this user ID (which may include grants for
|
||||
// u_anon and u_auth)
|
||||
grantTuples, err = iamRepo.GrantsForUser(v.ctx, userId)
|
||||
if err != nil {
|
||||
retErr = errors.Wrap(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. "id=foo;actions=create,read". These
|
||||
// will simply not have an effect.
|
||||
for _, pair := range grantTuples {
|
||||
parsed, err := perms.Parse(
|
||||
pair.ScopeId,
|
||||
pair.Grant,
|
||||
perms.WithUserId(userId),
|
||||
perms.WithAccountId(accountId),
|
||||
perms.WithSkipFinalValidation(true))
|
||||
if err != nil {
|
||||
retErr = errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("failed to parse grant %#v", pair.Grant)))
|
||||
return
|
||||
}
|
||||
parsedGrants = append(parsedGrants, parsed)
|
||||
}
|
||||
|
||||
retAcl = perms.NewACL(parsedGrants...)
|
||||
aclResults = retAcl.Allowed(*v.res, v.act)
|
||||
// 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 == 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
|
||||
}
|
||||
|
||||
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, 0, len(availableActions))
|
||||
for _, act := range availableActions {
|
||||
if r.v.acl.Allowed(*res, act).Authorized {
|
||||
ret = append(ret, act)
|
||||
}
|
||||
}
|
||||
if len(ret) == 0 {
|
||||
return nil
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (r *VerifyResults) FetchOutputFields(res perms.Resource, act action.Type) perms.OutputFieldsMap {
|
||||
switch {
|
||||
case r.v.requestInfo.TokenFormat == AuthTokenTypeRecoveryKms:
|
||||
return perms.OutputFieldsMap{"*": true}
|
||||
case r.v.requestInfo.DisableAuthEntirely:
|
||||
return nil
|
||||
}
|
||||
|
||||
return r.v.acl.Allowed(res, act).OutputFields
|
||||
}
|
||||
|
||||
// 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(logger hclog.Logger, kmsCache *kms.Kms, req *http.Request) (string, string, TokenFormat) {
|
||||
// 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 anonymouse user
|
||||
return "", "", AuthTokenTypeUnknown
|
||||
}
|
||||
|
||||
if strings.HasPrefix(fullToken, "r_") {
|
||||
return "", fullToken, AuthTokenTypeRecoveryKms
|
||||
}
|
||||
|
||||
splitFullToken := strings.Split(fullToken, "_")
|
||||
if len(splitFullToken) != 3 {
|
||||
logger.Trace("get token from request: unexpected number of segments in token; continuing as anonymous user", "expected", 3, "found", len(splitFullToken))
|
||||
return "", "", AuthTokenTypeUnknown
|
||||
}
|
||||
|
||||
publicId := strings.Join(splitFullToken[0:2], "_")
|
||||
encryptedToken := splitFullToken[2]
|
||||
|
||||
return publicId, encryptedToken, receivedTokenType
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/authtoken"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/servers"
|
||||
"github.com/hashicorp/boundary/internal/servers/controller/handlers"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAuthTokenAuthenticator(t *testing.T) {
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
logger := hclog.New(nil)
|
||||
wrapper := db.TestWrapper(t)
|
||||
kms := kms.TestKms(t, conn, wrapper)
|
||||
tokenRepo, err := authtoken.NewRepository(rw, rw, kms)
|
||||
require.NoError(t, err)
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
tokenRepoFn := func() (*authtoken.Repository, error) {
|
||||
return tokenRepo, nil
|
||||
}
|
||||
iamRepoFn := func() (*iam.Repository, error) {
|
||||
return iamRepo, nil
|
||||
}
|
||||
serversRepoFn := func() (*servers.Repository, error) {
|
||||
return servers.NewRepository(rw, rw, kms)
|
||||
}
|
||||
|
||||
o, _ := iam.TestScopes(t, iamRepo)
|
||||
at := authtoken.TestAuthToken(t, conn, kms, o.GetPublicId())
|
||||
encToken, err := authtoken.EncryptToken(context.Background(), kms, o.GetPublicId(), at.GetPublicId(), at.GetToken())
|
||||
require.NoError(t, err)
|
||||
|
||||
tokValue := at.GetPublicId() + "_" + encToken
|
||||
jsCookieVal, httpCookieVal := tokValue[:len(tokValue)/2], tokValue[len(tokValue)/2:]
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
headers map[string]string
|
||||
cookies []http.Cookie
|
||||
userId string
|
||||
tokenFormat TokenFormat
|
||||
}{
|
||||
{
|
||||
name: "Empty headers",
|
||||
headers: map[string]string{},
|
||||
tokenFormat: AuthTokenTypeUnknown,
|
||||
},
|
||||
{
|
||||
name: "Bearer token",
|
||||
headers: map[string]string{"Authorization": fmt.Sprintf("Bearer %s", tokValue)},
|
||||
userId: at.GetIamUserId(),
|
||||
tokenFormat: AuthTokenTypeBearer,
|
||||
},
|
||||
{
|
||||
name: "Split cookie token",
|
||||
cookies: []http.Cookie{
|
||||
{Name: handlers.HttpOnlyCookieName, Value: httpCookieVal},
|
||||
{Name: handlers.JsVisibleCookieName, Value: jsCookieVal},
|
||||
},
|
||||
userId: at.GetIamUserId(),
|
||||
tokenFormat: AuthTokenTypeSplitCookie,
|
||||
},
|
||||
{
|
||||
name: "Split cookie token only http cookie",
|
||||
cookies: []http.Cookie{
|
||||
{Name: handlers.HttpOnlyCookieName, Value: httpCookieVal},
|
||||
},
|
||||
tokenFormat: AuthTokenTypeUnknown,
|
||||
},
|
||||
{
|
||||
name: "Split cookie token only js cookie",
|
||||
cookies: []http.Cookie{
|
||||
{Name: handlers.JsVisibleCookieName, Value: jsCookieVal},
|
||||
},
|
||||
tokenFormat: AuthTokenTypeUnknown,
|
||||
},
|
||||
{
|
||||
name: "Cookie and auth header",
|
||||
headers: map[string]string{"Authorization": fmt.Sprintf("Bearer %s", tokValue)},
|
||||
cookies: []http.Cookie{
|
||||
{Name: handlers.HttpOnlyCookieName, Value: httpCookieVal},
|
||||
{Name: handlers.JsVisibleCookieName, Value: jsCookieVal},
|
||||
},
|
||||
userId: at.GetIamUserId(),
|
||||
tokenFormat: AuthTokenTypeBearer,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "http://127.0.0.1/v1/scopes/o_1", nil)
|
||||
for k, v := range tc.headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
for _, c := range tc.cookies {
|
||||
req.AddCookie(&c)
|
||||
}
|
||||
|
||||
// Add values for authn/authz checking
|
||||
requestInfo := RequestInfo{
|
||||
Path: req.URL.Path,
|
||||
Method: req.Method,
|
||||
}
|
||||
requestInfo.PublicId, requestInfo.EncryptedToken, requestInfo.TokenFormat = GetTokenFromRequest(logger, kms, req)
|
||||
assert.Equal(t, tc.tokenFormat, requestInfo.TokenFormat)
|
||||
|
||||
if tc.userId == "" {
|
||||
return
|
||||
}
|
||||
ctx := NewVerifierContext(context.Background(), logger, iamRepoFn, tokenRepoFn, serversRepoFn, kms, requestInfo)
|
||||
|
||||
v, ok := ctx.Value(verifierKey).(*verifier)
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, v)
|
||||
|
||||
v.decryptToken()
|
||||
|
||||
at, err := tokenRepo.ValidateToken(ctx, v.requestInfo.PublicId, v.requestInfo.Token)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.userId, at.GetIamUserId())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/perms"
|
||||
"github.com/hashicorp/boundary/internal/types/action"
|
||||
"github.com/hashicorp/boundary/internal/types/resource"
|
||||
"github.com/hashicorp/go-secure-stdlib/strutil"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
// CalculateAuthorizedCollectionActions returns authorized actions for the given
|
||||
// inputs.
|
||||
//
|
||||
// NOTE: Eventually we should unit test this, but for now every service handler
|
||||
// is validating the results of this (as it was pulled out of the service
|
||||
// handlers).
|
||||
func CalculateAuthorizedCollectionActions(ctx context.Context,
|
||||
authResults VerifyResults,
|
||||
mapToRange map[resource.Type]action.ActionSet,
|
||||
scopeId, pin string) (map[string]*structpb.ListValue, error) {
|
||||
res := &perms.Resource{
|
||||
ScopeId: scopeId,
|
||||
Pin: pin,
|
||||
}
|
||||
// Range over the defined collections and check permissions against those
|
||||
// collections.
|
||||
var ret map[string]*structpb.ListValue
|
||||
for k, v := range mapToRange {
|
||||
res.Type = k
|
||||
acts := authResults.FetchActionSetForType(ctx, k, v, WithResource(res)).Strings()
|
||||
if len(acts) > 0 {
|
||||
if ret == nil {
|
||||
ret = make(map[string]*structpb.ListValue)
|
||||
}
|
||||
lv, err := structpb.NewList(strutil.StringListToInterfaceList(acts))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret[k.String()+"s"] = lv
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/perms"
|
||||
"github.com/hashicorp/boundary/internal/types/action"
|
||||
"github.com/hashicorp/boundary/internal/types/resource"
|
||||
)
|
||||
|
||||
func getOpts(opt ...Option) options {
|
||||
opts := getDefaultOptions()
|
||||
for _, o := range opt {
|
||||
o(&opts)
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
// Option - how Options are passed as arguments
|
||||
type Option func(*options)
|
||||
|
||||
// options = how options are represented
|
||||
type options struct {
|
||||
withScopeId string
|
||||
withPin string
|
||||
withId string
|
||||
withAction action.Type
|
||||
withType resource.Type
|
||||
withUserId string
|
||||
withKms *kms.Kms
|
||||
withRecoveryTokenNotAllowed bool
|
||||
withAnonymousUserNotAllowed bool
|
||||
withResource *perms.Resource
|
||||
}
|
||||
|
||||
func getDefaultOptions() options {
|
||||
return options{}
|
||||
}
|
||||
|
||||
func WithScopeId(id string) Option {
|
||||
return func(o *options) {
|
||||
o.withScopeId = id
|
||||
}
|
||||
}
|
||||
|
||||
func WithPin(pin string) Option {
|
||||
return func(o *options) {
|
||||
o.withPin = pin
|
||||
}
|
||||
}
|
||||
|
||||
func WithId(id string) Option {
|
||||
return func(o *options) {
|
||||
o.withId = id
|
||||
}
|
||||
}
|
||||
|
||||
func WithAction(action action.Type) Option {
|
||||
return func(o *options) {
|
||||
o.withAction = action
|
||||
}
|
||||
}
|
||||
|
||||
func WithType(rt resource.Type) Option {
|
||||
return func(o *options) {
|
||||
o.withType = rt
|
||||
}
|
||||
}
|
||||
|
||||
func WithUserId(id string) Option {
|
||||
return func(o *options) {
|
||||
o.withUserId = id
|
||||
}
|
||||
}
|
||||
|
||||
func WithKms(kms *kms.Kms) Option {
|
||||
return func(o *options) {
|
||||
o.withKms = kms
|
||||
}
|
||||
}
|
||||
|
||||
func WithRecoveryTokenNotAllowed(notAllowed bool) Option {
|
||||
return func(o *options) {
|
||||
o.withRecoveryTokenNotAllowed = notAllowed
|
||||
}
|
||||
}
|
||||
|
||||
func WithAnonymousUserNotAllowed(notAllowed bool) Option {
|
||||
return func(o *options) {
|
||||
o.withAnonymousUserNotAllowed = notAllowed
|
||||
}
|
||||
}
|
||||
|
||||
// WithResource specifies a resouce to use
|
||||
func WithResource(resource *perms.Resource) Option {
|
||||
return func(o *options) {
|
||||
o.withResource = resource
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/perms"
|
||||
"github.com/hashicorp/boundary/internal/types/action"
|
||||
"github.com/hashicorp/boundary/internal/types/resource"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test_GetOpts provides unit tests for GetOpts and all the options
|
||||
func Test_GetOpts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
withKms := new(kms.Kms)
|
||||
res := new(perms.Resource)
|
||||
|
||||
opts := getOpts(
|
||||
WithScopeId("foo"),
|
||||
WithPin("bar"),
|
||||
WithId("zip"),
|
||||
WithAction(action.AddHosts),
|
||||
WithType(resource.Group),
|
||||
WithUserId("user"),
|
||||
WithKms(withKms),
|
||||
WithRecoveryTokenNotAllowed(true),
|
||||
WithAnonymousUserNotAllowed(true),
|
||||
WithResource(res),
|
||||
)
|
||||
exp := options{
|
||||
withScopeId: "foo",
|
||||
withPin: "bar",
|
||||
withId: "zip",
|
||||
withAction: action.AddHosts,
|
||||
withType: resource.Group,
|
||||
withUserId: "user",
|
||||
withKms: withKms,
|
||||
withRecoveryTokenNotAllowed: true,
|
||||
withAnonymousUserNotAllowed: true,
|
||||
withResource: res,
|
||||
}
|
||||
assert.Equal(t, exp, opts)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/requests"
|
||||
"github.com/hashicorp/boundary/internal/servers/controller/common"
|
||||
)
|
||||
|
||||
// DisabledAuthTestContext is meant for testing, and uses a context that has
|
||||
// auth checking entirely disabled. Supported options: WithScopeId an WithUserId
|
||||
// are used directly; WithKms is passed through into the verifier context.
|
||||
func DisabledAuthTestContext(iamRepoFn common.IamRepoFactory, scopeId string, opt ...Option) context.Context {
|
||||
reqInfo := RequestInfo{DisableAuthEntirely: true}
|
||||
opts := getOpts(opt...)
|
||||
reqInfo.scopeIdOverride = opts.withScopeId
|
||||
if reqInfo.scopeIdOverride == "" {
|
||||
reqInfo.scopeIdOverride = scopeId
|
||||
}
|
||||
reqInfo.userIdOverride = opts.withUserId
|
||||
if reqInfo.userIdOverride == "" {
|
||||
reqInfo.userIdOverride = "u_auth"
|
||||
}
|
||||
requestContext := context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{})
|
||||
return NewVerifierContext(requestContext, nil, iamRepoFn, nil, nil, opts.withKms, reqInfo)
|
||||
}
|
||||
Reference in new issue
Block a user