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:
Todd Knight authored and GitHub committed 2021-07-27 09:59:52 -07:00
1 parent ab6f3eaeb4
commit 10e8aeae4b
62 files changed
+603 -597

No files matched your search

+648
View File
@@ -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)
}