Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: process nested tag claims #6

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cmd/handler-builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ func authorizeS3Action(ctx context.Context, sessionToken string, action S3ApiAct
return
}

policyStr, err := pm.GetPolicy(sessionClaims.RoleARN, PolicyTemplateDataFromClaims(sessionClaims))
policySessionData := GetPolicySessionDataFromClaims(sessionClaims)
policyStr, err := pm.GetPolicy(sessionClaims.RoleARN, policySessionData)
if err != nil {
slog.Error("Could not get policy for temporary credentials", "error", err, xRequestIDStr, getRequestID(ctx), "role_arn", sessionClaims.RoleARN)
writeS3ErrorResponse(ctx, w, ErrS3InternalError, nil)
Expand All @@ -125,7 +126,7 @@ func authorizeS3Action(ctx context.Context, sessionToken string, action S3ApiAct
writeS3ErrorResponse(ctx, w, ErrS3InternalError, nil)
return
}
iamActions, err := NewIamActionsFromS3Request(action, r)
iamActions, err := newIamActionsFromS3Request(action, r, policySessionData)
if err != nil {
slog.Error("Could not get IAM actions from request", "error", err, xRequestIDStr, getRequestID(ctx), "policy", sessionClaims.RoleARN)
writeS3ErrorResponse(ctx, w, ErrS3InternalError, nil)
Expand Down
45 changes: 37 additions & 8 deletions cmd/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,20 @@ import (
"github.com/google/uuid"
)

type SessionClaims struct {
RoleARN string `json:"role_arn"`
//The issuer of the initial OIDC refresh token
IIssuer string `json:"initial_issuer"`
type AWSSessionTags struct {
PrincipalTags map[string][]string `json:"principal_tags"`
TransitiveTagKeys []string `json:"transitive_tag_keys,omitempty"`
}

type IDPClaims struct {
//The optional session tags
Tags AWSSessionTags `json:"https://aws.amazon.com/tags,omitempty"`
jwt.RegisteredClaims
}

func createRS256PolicyToken(issuer, iIssuer, subject, roleARN string, expiry time.Duration) (*jwt.Token) {
claims := &SessionClaims{
roleARN,
iIssuer,
func newIDPClaims(issuer, subject string, expiry time.Duration, tags AWSSessionTags) (*IDPClaims) {
return &IDPClaims{
tags,
jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().UTC().Add(expiry)),
IssuedAt: jwt.NewNumericDate(time.Now().UTC()),
Expand All @@ -30,6 +33,32 @@ func createRS256PolicyToken(issuer, iIssuer, subject, roleARN string, expiry tim
},
}

}

type SessionClaims struct {
RoleARN string `json:"role_arn"`
//The issuer of the initial OIDC refresh token
IIssuer string `json:"initial_issuer"`
IDPClaims
}

func createRS256PolicyToken(issuer, iIssuer, subject, roleARN string, expiry time.Duration) (*jwt.Token) {
claims := &SessionClaims{
roleARN,
iIssuer,
IDPClaims{
AWSSessionTags{},
jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().UTC().Add(expiry)),
IssuedAt: jwt.NewNumericDate(time.Now().UTC()),
NotBefore: jwt.NewNumericDate(time.Now().UTC()),
Issuer: issuer,
Subject: subject,
ID: uuid.New().String(),
},
},
}

token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
return token
}
Expand Down
22 changes: 8 additions & 14 deletions cmd/policy-api-action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@ package cmd
import (
"errors"
"net/http"
"strings"
"testing"

sg "github.com/aws/smithy-go"
)


type StubJustReturnApiAction struct{
t *testing.T
}

var globalLastApiActionStubJustReturnApiAction S3ApiAction = ""

func (p *StubJustReturnApiAction) Build(action S3ApiAction, presigned bool) http.HandlerFunc{
return func (w http.ResponseWriter, r *http.Request) {
//AWS CLI expects certain structure for ok responses
//For error we could use the message field to pass a message regardless
//of the api action
globalLastApiActionStubJustReturnApiAction = action
writeS3ErrorResponse(
buildContextWithRequestID(r),
w,
Expand All @@ -41,18 +41,12 @@ func TestExpectedAPIActionIdentified(t *testing.T) {

for _, tc := range getApiAndIAMActionTestCases() { //see policy_iam_action_test
err := tc.ApiCall(t)
smityError, ok := err.(*sg.OperationError)
if !ok {
t.Errorf("err was not smithy error %s", err)
if err == nil {
t.Errorf("%s: an error should have been returned", tc.ApiAction)
}
accessDeniedParts := strings.Split(smityError.Error(), "AccessDenied: ")
if len(accessDeniedParts) < 2 {
t.Errorf("Encountered unexpected error (not Access Denied) %s", smityError)
continue
}
msg := accessDeniedParts[1]
if msg != tc.ApiAction {
t.Errorf("Expected %s, got %s, bug in router code", tc.ApiAction, msg)

if tc.ApiAction != string(globalLastApiActionStubJustReturnApiAction) {
t.Errorf("wrong APIAction identified; expected %s, got %s", tc.ApiAction, globalLastApiActionStubJustReturnApiAction)
}
}
}
48 changes: 35 additions & 13 deletions cmd/policy-evaluation.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ func NewPolicyEvaluatorFromStr(policyContent string) (*PolicyEvaluator, error)
type evalReason string
const reasonActionIsAllowed evalReason = "Action is allowed"
const reasonNoStatementAllowingAction evalReason = "No statement allows the action"
const reasonExplicitDeny evalReason = "Explicit deny"
const reasonErrorEncountered evalReason = "Error was encountered"

//Allow wildcards like * and ? but escape other special characters
//https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
Expand Down Expand Up @@ -75,28 +77,42 @@ func areAllConditionValuesSingular(context map[string]*policy.ConditionValue) (b
return true
}

//Evaluate what a StringLike operation does
func evalStringLike(conditionDetails map[string]*policy.ConditionValue, context map[string]*policy.ConditionValue) (bool, error) {
if !areAllConditionValuesSingular(context) {
return false, fmt.Errorf("non-singular value got %v", context)
}
for sConditionKey, sConditionValue := range conditionDetails {
contextValue, exists := context[sConditionKey]
if !exists {
return false, fmt.Errorf("condition key '%s' was not set in request context", sConditionKey)
}
if !isConditionMetForStringLike(sConditionValue, contextValue) {
return false, nil
}
}
return true, nil
}

// See whether the condition defined by the conditionOperator and conditionDetails is met
// for the given context
func isConditionMetForOperator(conditionOperator string, conditionDetails map[string]*policy.ConditionValue, context map[string]*policy.ConditionValue) (bool, error) {
switch conditionOperator {
case "StringLike":
if !areAllConditionValuesSingular(context) {
return false, fmt.Errorf("non-singular value for %s, got %v", conditionOperator, context)
result, err := evalStringLike(conditionDetails, context)
if err != nil {
return false, fmt.Errorf("operator StringLike encountered %s", err)
}
for sConditionKey, sConditionValue := range conditionDetails {
contextValue, exists := context[sConditionKey]
if !exists {
return false, fmt.Errorf("condition key '%s' was not set in request context", sConditionKey)
}
if !isConditionMetForStringLike(sConditionValue, contextValue) {
return false, nil
}
return result, err
case "StringNotLike":
result, err := evalStringLike(conditionDetails, context)
if err != nil {
return false, fmt.Errorf("operator StringLike encountered %s", err)
}
return !result, err
default:
return false, fmt.Errorf("unsupported condition: '%s'", conditionOperator)
}
//No unmet condition
return true, nil
}


Expand Down Expand Up @@ -164,7 +180,13 @@ func (e *PolicyEvaluator) Evaluate(a iamAction) (isAllowed bool, reason evalReas
reason = reasonActionIsAllowed
}
case policy.EffectDeny:
panic("Not implemented yet")
relevant, err := isRelevantFor(s, a)
if err != nil {
return false, reasonErrorEncountered, err
}
if relevant {
return false, reasonExplicitDeny, err
}
}
}
return
Expand Down
67 changes: 66 additions & 1 deletion cmd/policy-evaluation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,39 @@ var testPolScen2AllowListingBucketWithinPrefix = fmt.Sprintf(`
}
`, IAMActionS3ListBucket, testBucketARN, testAllowedPrefix)


var testPolAllowAllIfTestDepartmentOtherwiseDenyAll = `
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Allow all if test department",
"Effect": "Allow",
"Action": [
"*"
],
"Resource": "*",
"Condition" : {
"StringLike" : {
"aws:PrincipalTag/department": "test"
}
}
},
{
"Sid": "Deny all if not test department",
"Effect": "Deny",
"Action": [
"*"
],
"Resource": "*",
"Condition" : {
"StringNotLike" : {
"aws:PrincipalTag/department": "test"
}
}
}
]
}
`


func TestPolicyEvaluations(t *testing.T) {
Expand Down Expand Up @@ -121,6 +153,39 @@ func TestPolicyEvaluations(t *testing.T) {
false,
reasonNoStatementAllowingAction,
},
{
"Any action should be allowed if we run with test department session tag",
testPolAllowAllIfTestDepartmentOtherwiseDenyAll,
newIamAction(
IAMActionS3GetObject,
testBucketARN,
testSessionDataTestDepartment,
),
true,
reasonActionIsAllowed,
},
{
"Any action should be allowed if we run with test department session tag 2",
testPolAllowAllIfTestDepartmentOtherwiseDenyAll,
newIamAction(
IAMActionS3ListAllMyBuckets,
testBucketARN,
testSessionDataTestDepartment,
),
true,
reasonActionIsAllowed,
},
{
"Any action should be disallowed if we run with deparment session tag different from test",
testPolAllowAllIfTestDepartmentOtherwiseDenyAll,
newIamAction(
IAMActionS3GetObject,
testBucketARN,
testSessionDataQaDeparment,
),
false,
reasonExplicitDeny,
},
}

for _, policyTest := range policyTests {
Expand Down
32 changes: 24 additions & 8 deletions cmd/policy-generation.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,20 +133,36 @@ func (m *PolicyManager) getPolicyTemplate(arn string) (tmpl *template.Template,
return
}

type policyTemplateData struct {
Claims map[string]string

type PolicySessionClaims struct {
Subject string
Issuer string
}


//This is the structure that will be made available during templating and
//thus is available to be used in policies.
type PolicySessionData struct {
Claims PolicySessionClaims
Tags AWSSessionTags
}

func PolicyTemplateDataFromClaims(sc *SessionClaims) policyTemplateData{
return policyTemplateData{
Claims: map[string]string{
"Issuer": sc.IIssuer,
"Subject": sc.Subject,
func GetPolicySessionDataFromClaims(claims *SessionClaims) *PolicySessionData {
issuer := claims.IIssuer
if issuer == "" {
issuer = claims.Issuer
}
return &PolicySessionData{
Claims: PolicySessionClaims{
Subject: claims.Subject,
Issuer: issuer,
},
Tags: claims.Tags,
}
}

func (m *PolicyManager) GetPolicy(arn string, data policyTemplateData) (string, error) {

func (m *PolicyManager) GetPolicy(arn string, data *PolicySessionData) (string, error) {
tmpl, err := m.getPolicyTemplate(arn)
if err != nil {
return "", err
Expand Down
Loading