-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add initial changes for access level feature * avoid copying config at each function pass * update TTL related info in readme * document max ttl default better * Make code default for maxItemTTL 1day * Update auth/bearerTokenFactory.go Co-authored-by: kristinaspring <kmspring57@gmail.com> * Improve resolver name * resolver name fix and breake comment into lines * update access level func reference * Update tests * Add access level values to spruce yaml * add issue comment * add release notes * Make default TTL less aggressive * fix typo in spruce config file Co-authored-by: kristinaspring <kmspring57@gmail.com>
- Loading branch information
1 parent
bffa41d
commit 1dbdab5
Showing
18 changed files
with
515 additions
and
139 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
package auth | ||
|
||
import ( | ||
"github.com/spf13/cast" | ||
"github.com/xmidt-org/bascule" | ||
) | ||
|
||
// Exported access level default values which application code may want to use. | ||
const ( | ||
DefaultAccessLevelAttributeKey = "access-level" | ||
DefaultAccessLevelAttributeValue = 0 | ||
) | ||
|
||
const ( | ||
ElevatedAccessLevelAttributeValue = 1 | ||
) | ||
|
||
// internal default values | ||
const ( | ||
defaultAccessLevelCapabilityName = "xmidt:svc:admin" | ||
) | ||
|
||
var defaultAccessLevelPath = []string{"capabilities"} | ||
|
||
type accessLevel struct { | ||
Resolve accessLevelResolver | ||
AttributeKey string | ||
} | ||
|
||
// accessLevelResolver lets users of accessLevelBearerTokenFactory determine what access level value is assigned to a | ||
// request based on its capabilities. | ||
type accessLevelResolver func(bascule.Attributes) int | ||
|
||
type accessLevelCapabilitySource struct { | ||
// Name is the capability we will search for inside the capability list pointed by path. | ||
// If this value is found in the list, the access level assigned to the request will be 1. Otherwise, it will be 0. | ||
// (Optional) defaults to 'xmidt:svc:admin' | ||
Name string | ||
|
||
// Path is the list of nested keys to get to the claim which contains the capabilities. | ||
// (Optional) default: ["capabilities"] | ||
Path []string | ||
} | ||
|
||
type accessLevelConfig struct { | ||
AttributeKey string | ||
CapabilitySource accessLevelCapabilitySource | ||
} | ||
|
||
func defaultAccessLevel() accessLevel { | ||
return accessLevel{ | ||
AttributeKey: DefaultAccessLevelAttributeKey, | ||
Resolve: func(_ bascule.Attributes) int { | ||
return DefaultAccessLevelAttributeValue | ||
}, | ||
} | ||
} | ||
|
||
func validateAccessLevelConfig(config *accessLevelConfig) { | ||
if len(config.AttributeKey) < 1 { | ||
config.AttributeKey = DefaultAccessLevelAttributeKey | ||
} | ||
|
||
if len(config.CapabilitySource.Name) < 1 { | ||
config.CapabilitySource.Name = defaultAccessLevelCapabilityName | ||
} | ||
|
||
if len(config.CapabilitySource.Path) < 1 { | ||
config.CapabilitySource.Path = defaultAccessLevelPath | ||
} | ||
} | ||
|
||
func newContainsAttributeAccessLevel(config *accessLevelConfig) accessLevel { | ||
validateAccessLevelConfig(config) | ||
|
||
resolve := func(attributes bascule.Attributes) int { | ||
capabilitiesClaim, ok := bascule.GetNestedAttribute(attributes, config.CapabilitySource.Path...) | ||
if !ok { | ||
return DefaultAccessLevelAttributeValue | ||
} | ||
capabilities := cast.ToStringSlice(capabilitiesClaim) | ||
|
||
for _, capability := range capabilities { | ||
if capability == config.CapabilitySource.Name { | ||
return ElevatedAccessLevelAttributeValue | ||
} | ||
} | ||
|
||
return DefaultAccessLevelAttributeValue | ||
} | ||
|
||
return accessLevel{ | ||
AttributeKey: config.AttributeKey, | ||
Resolve: resolve, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package auth | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"net/http" | ||
|
||
"emperror.dev/emperror" | ||
"github.com/dgrijalva/jwt-go" | ||
"github.com/xmidt-org/bascule" | ||
"github.com/xmidt-org/bascule/basculehttp" | ||
"github.com/xmidt-org/bascule/key" | ||
) | ||
|
||
const jwtPrincipalKey = "sub" | ||
|
||
// accessLevelBearerTokenFactory extends basculehttp.BearerTokenFactory by letting | ||
// the user of the factory inject an access level attribute to the jwt token. | ||
// Application code should handle case in which the value is not injected (i.e. basic auth tokens). | ||
type accessLevelBearerTokenFactory struct { | ||
DefaultKeyID string | ||
Resolver key.Resolver | ||
Parser bascule.JWTParser | ||
Leeway bascule.Leeway | ||
AccessLevel accessLevel | ||
} | ||
|
||
// ParseAndValidate expects the given value to be a JWT with a kid header. The | ||
// kid should be resolvable by the Resolver and the JWT should be Parseable and | ||
// pass any basic validation checks done by the Parser. If everything goes | ||
// well, a Token of type "jwt" is returned. | ||
func (a accessLevelBearerTokenFactory) ParseAndValidate(ctx context.Context, _ *http.Request, _ bascule.Authorization, value string) (bascule.Token, error) { | ||
if len(value) == 0 { | ||
return nil, errors.New("empty value") | ||
} | ||
|
||
leewayclaims := bascule.ClaimsWithLeeway{ | ||
MapClaims: make(jwt.MapClaims), | ||
Leeway: a.Leeway, | ||
} | ||
|
||
jwsToken, err := a.Parser.ParseJWT(value, &leewayclaims, defaultKeyfunc(ctx, a.DefaultKeyID, a.Resolver)) | ||
if err != nil { | ||
return nil, emperror.Wrap(err, "failed to parse JWS") | ||
} | ||
if !jwsToken.Valid { | ||
return nil, basculehttp.ErrorInvalidToken | ||
} | ||
|
||
claims, ok := jwsToken.Claims.(*bascule.ClaimsWithLeeway) | ||
|
||
if !ok { | ||
return nil, emperror.Wrap(basculehttp.ErrorUnexpectedClaims, "failed to parse JWS") | ||
} | ||
|
||
claimsMap, err := claims.GetMap() | ||
if err != nil { | ||
return nil, emperror.WrapWith(err, "failed to get map of claims", "claims struct", claims) | ||
} | ||
|
||
jwtClaims := bascule.NewAttributes(claimsMap) | ||
|
||
principalVal, ok := jwtClaims.Get(jwtPrincipalKey) | ||
if !ok { | ||
return nil, emperror.WrapWith(basculehttp.ErrorInvalidPrincipal, "principal value not found", "principal key", jwtPrincipalKey, "jwtClaims", claimsMap) | ||
} | ||
principal, ok := principalVal.(string) | ||
if !ok { | ||
return nil, emperror.WrapWith(basculehttp.ErrorInvalidPrincipal, "principal value not a string", "principal", principalVal) | ||
} | ||
|
||
if a.AccessLevel.Resolve != nil { | ||
claimsMap[a.AccessLevel.AttributeKey] = a.AccessLevel.Resolve(jwtClaims) | ||
jwtClaims = bascule.NewAttributes(claimsMap) | ||
} | ||
|
||
return bascule.NewToken("jwt", principal, jwtClaims), nil | ||
} | ||
|
||
func defaultKeyfunc(ctx context.Context, defaultKeyID string, keyResolver key.Resolver) jwt.Keyfunc { | ||
return func(token *jwt.Token) (interface{}, error) { | ||
keyID, ok := token.Header["kid"].(string) | ||
if !ok { | ||
keyID = defaultKeyID | ||
} | ||
|
||
pair, err := keyResolver.ResolveKey(ctx, keyID) | ||
if err != nil { | ||
return nil, emperror.Wrap(err, "failed to resolve key") | ||
} | ||
return pair.Public(), nil | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.