Rearrange some auth logic and add some opts useful for filtering/testing (#835)

This commit is contained in:
Jeff Mitchell authored and GitHub committed 2020-12-17 10:30:40 -05:00
1 parent ea878e49fa
commit 8b39faef1b
2 files changed
+310 -282

No files matched your search

+288 -271
View File
@@ -195,277 +195,6 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) {
return
}
// AdditionalVerification is used to perform checks of additional resources for
// actions that need to touch more than one.
func (r *VerifyResults) AdditionalVerification(ctx context.Context, opt ...Option) (ret VerifyResults) {
v := r.v
ret.Error = handlers.ForbiddenError()
// Set other parameters the same to start with
ret.Scope = r.Scope
ret.UserId = r.UserId
ret.AuthTokenId = r.AuthTokenId
ret.v = r.v
opts := getOpts(opt...)
act := opts.withAction
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 {
res.ScopeId = scope.Global.String()
}
// Only perform lookup if it's actually different, otherwise use cached info
if res.ScopeId != r.Scope.Id {
iamRepo, err := v.iamRepoFn()
if err != nil {
v.logger.Error("additional verification: failed to get iam repo", "error", err)
return
}
// Look up scope details to return. We can skip a lookup when using the
// global scope
switch res.ScopeId {
case "global":
ret.Scope = &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 {
v.logger.Error("additional verification: failed to get look up scope", "error", err)
return
}
if scp == nil {
v.logger.Error("additional verification: non-existent scope", "error", err)
return
}
ret.Scope = &scopes.ScopeInfo{
Id: scp.GetPublicId(),
Type: scp.GetType(),
Name: scp.GetName(),
Description: scp.GetDescription(),
ParentScopeId: scp.GetParentId(),
}
}
}
// Always allowed
if v.requestInfo.TokenFormat == AuthTokenTypeRecoveryKms {
ret.Error = nil
return
}
aclResults := v.acl.Allowed(res, act)
if !aclResults.Allowed {
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 == "u_anon" {
ret.Error = handlers.UnauthenticatedError()
}
return
}
}
ret.Error = nil
return
}
func (v verifier) performAuthCheck() (aclResults perms.ACLResults, userId string, scopeInfo *scopes.ScopeInfo, retAcl perms.ACL, retErr error) {
// Ensure we return an error by default if we forget to set this somewhere
retErr = errors.New("unknown")
// Make the linter happy
_ = retErr
scopeInfo = new(scopes.ScopeInfo)
userId = "u_anon"
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 = fmt.Errorf("perform auth check: failed to get authtoken repo: %w", err)
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 = "u_anon"
accountId = ""
}
}
}
iamRepo, err := v.iamRepoFn()
if err != nil {
retErr = fmt.Errorf("perform auth check: failed to get iam repo: %w", err)
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 = fmt.Errorf("perform auth check: failed to lookup scope: %w", err)
return
}
if scp == nil {
retErr = fmt.Errorf("perform auth check: 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.Allowed = true
retErr = nil
return
}
var parsedGrants []perms.Grant
var grantPairs []perms.GrantPair
// Fetch and parse grants for this user ID (which may include grants for
// u_anon and u_auth)
grantPairs, err = iamRepo.GrantsForUser(v.ctx, userId)
if err != nil {
retErr = fmt.Errorf("perform auth check: failed to query for user grants: %w", err)
return
}
parsedGrants = make([]perms.Grant, 0, len(grantPairs))
for _, pair := range grantPairs {
parsed, err := perms.Parse(
pair.ScopeId,
pair.Grant,
perms.WithUserId(userId),
perms.WithAccountId(accountId),
perms.WithSkipFinalValidation(true))
if err != nil {
retErr = fmt.Errorf("perform auth check: failed to parse grant %#v: %w", pair.Grant, err)
return
}
parsedGrants = append(parsedGrants, parsed)
}
retAcl = perms.NewACL(parsedGrants...)
aclResults = retAcl.Allowed(*v.res, v.act)
retErr = nil
return
}
// 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
}
func (v *verifier) decryptToken() {
switch v.requestInfo.TokenFormat {
case AuthTokenTypeUnknown:
@@ -590,3 +319,291 @@ func (v *verifier) decryptToken() {
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) {
// Ensure we return an error by default if we forget to set this somewhere
retErr = errors.New("unknown")
// Make the linter happy
_ = retErr
scopeInfo = new(scopes.ScopeInfo)
userId = "u_anon"
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 = fmt.Errorf("perform auth check: failed to get authtoken repo: %w", err)
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 = "u_anon"
accountId = ""
}
}
}
iamRepo, err := v.iamRepoFn()
if err != nil {
retErr = fmt.Errorf("perform auth check: failed to get iam repo: %w", err)
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 = fmt.Errorf("perform auth check: failed to lookup scope: %w", err)
return
}
if scp == nil {
retErr = fmt.Errorf("perform auth check: 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.Allowed = true
retErr = nil
return
}
var parsedGrants []perms.Grant
var grantPairs []perms.GrantPair
// Fetch and parse grants for this user ID (which may include grants for
// u_anon and u_auth)
grantPairs, err = iamRepo.GrantsForUser(v.ctx, userId)
if err != nil {
retErr = fmt.Errorf("perform auth check: failed to query for user grants: %w", err)
return
}
parsedGrants = make([]perms.Grant, 0, len(grantPairs))
for _, pair := range grantPairs {
parsed, err := perms.Parse(
pair.ScopeId,
pair.Grant,
perms.WithUserId(userId),
perms.WithAccountId(accountId),
perms.WithSkipFinalValidation(true))
if err != nil {
retErr = fmt.Errorf("perform auth check: failed to parse grant %#v: %w", pair.Grant, err)
return
}
parsedGrants = append(parsedGrants, parsed)
}
retAcl = perms.NewACL(parsedGrants...)
aclResults = retAcl.Allowed(*v.res, v.act)
retErr = nil
return
}
// AdditionalVerification is used to perform checks of additional resources for
// actions that need to touch more than one.
func (r *VerifyResults) AdditionalVerification(ctx context.Context, opt ...Option) (ret VerifyResults) {
v := r.v
ret.Error = handlers.ForbiddenError()
// Set other parameters the same to start with
ret.Scope = r.Scope
ret.UserId = r.UserId
ret.AuthTokenId = r.AuthTokenId
ret.v = r.v
opts := getOpts(opt...)
act := opts.withAction
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 {
res.ScopeId = scope.Global.String()
}
// Only perform lookup if it's actually different, otherwise use cached info
if res.ScopeId != r.Scope.Id {
iamRepo, err := v.iamRepoFn()
if err != nil {
v.logger.Error("additional verification: failed to get iam repo", "error", err)
return
}
// Look up scope details to return. We can skip a lookup when using the
// global scope
switch res.ScopeId {
case "global":
ret.Scope = &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 {
v.logger.Error("additional verification: failed to get look up scope", "error", err)
return
}
if scp == nil {
v.logger.Error("additional verification: non-existent scope", "error", err)
return
}
ret.Scope = &scopes.ScopeInfo{
Id: scp.GetPublicId(),
Type: scp.GetType(),
Name: scp.GetName(),
Description: scp.GetDescription(),
ParentScopeId: scp.GetParentId(),
}
}
}
// NOTE: the exclusions for recovery/anonymous user below are best effort.
// It's up to the caller to ensure that an invalid user ID for the operation
// is not actually allowed. The options here are really for
// listing/filtering purposes.
// Always allowed
if v.requestInfo.TokenFormat == AuthTokenTypeRecoveryKms {
if opts.withRecoveryTokenNotAllowed {
// Don't allow it. This is used in some cases like target listing
// where the command will succeed for most things but it's not
// allowed for an authorize-session action.
return
}
ret.Error = nil
return
}
if opts.withAnonymousUserNotAllowed {
// Similar to above, in some cases we never allow the anonymous user,
// such as with targets and authorize-session
return
}
aclResults := v.acl.Allowed(res, act)
if !aclResults.Allowed {
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 == "u_anon" {
ret.Error = handlers.UnauthenticatedError()
}
return
}
}
ret.Error = nil
return
}
// 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
}
+22 -11
View File
@@ -19,20 +19,19 @@ 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
withScopeId string
withPin string
withId string
withAction action.Type
withType resource.Type
withUserId string
withKms *kms.Kms
withRecoveryTokenNotAllowed bool
withAnonymousUserNotAllowed bool
}
func getDefaultOptions() options {
return options{
withScopeId: "",
withUserId: "",
}
return options{}
}
func WithScopeId(id string) Option {
@@ -76,3 +75,15 @@ func WithKms(kms *kms.Kms) Option {
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
}
}