Skip to content

Commit

Permalink
Merge pull request VITObelgium#6 from pvbouwel/feature/issue4_session…
Browse files Browse the repository at this point in the history
…_tags_nested_claim_format

feature: process nested tag claims
  • Loading branch information
pvbouwel authored Nov 20, 2024
2 parents cecfc54 + 13251f5 commit b124d58
Show file tree
Hide file tree
Showing 10 changed files with 418 additions and 152 deletions.
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

0 comments on commit b124d58

Please sign in to comment.