Skip to content

Commit

Permalink
feature: implement the Deny effect for policies
Browse files Browse the repository at this point in the history
feature: process nested tag claims

Allow IDPs to provide sessiont tags via the nested format.

This is for #4
  • Loading branch information
Peter Van Bouwel committed Nov 16, 2024
1 parent cecfc54 commit 9a1fe97
Show file tree
Hide file tree
Showing 9 changed files with 373 additions and 84 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
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
40 changes: 21 additions & 19 deletions cmd/policy-generation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,15 @@ var testPolicyRealistic = `
"Resource": "arn:aws:s3:::OpenEO-artifacts",
"Condition" : {
"StringLike" : {
"s3:prefix": "{{.Claims.Issuer}}/*"
"s3:prefix": "{{.Claims.Subject}}/*"
}
}
}
]
}
`

func NewTestPolicyRetriever() *TestPolicyRetriever {
return &TestPolicyRetriever{
testPolicies: map[string]string{
"policyRealistic": testPolicyRealistic,
},
}
}

func NewTestPolicyManager() *PolicyManager {
func newTestPolicyManager() *PolicyManager {
return NewPolicyManager(
TestPolicyRetriever{
testPolicies: map[string]string{
Expand Down Expand Up @@ -77,43 +69,53 @@ func (r TestPolicyRetriever) retrieveAllIdentifiers() ([]string, error) {

type policyGenerationTestCase struct {
PolicyName string
Claims policyTemplateData
Claims *SessionClaims
Expectedpolicy string
}

func buildTestSessionClaimsNoTags(issuer, subject string) (*SessionClaims) {
idpClaims := newIDPClaims(issuer, subject, time.Hour * 1, AWSSessionTags{})
return &SessionClaims{
RoleARN: "",
IIssuer: "",
IDPClaims: *idpClaims,
}
}

func TestPolicyGeneration(t *testing.T) {
testCases := []policyGenerationTestCase{
{
PolicyName: "policyRealistic",
Claims: policyTemplateData{Claims: map[string]string{"Issuer": "https://SuperIssuer"}},
Expectedpolicy: strings.Replace(testPolicyRealistic, "{{.Claims.Issuer}}", "https://SuperIssuer", -1),
Claims: buildTestSessionClaimsNoTags("", "userA"),
Expectedpolicy: strings.Replace(testPolicyRealistic, "{{.Claims.Subject}}", "userA", -1),
},
{
PolicyName: "now",
Claims: policyTemplateData{Claims: map[string]string{}},
Claims: buildTestSessionClaimsNoTags("", ""),
Expectedpolicy: YYYYmmdd(Now()),
},
{
PolicyName: "nowSlashed",
Claims: policyTemplateData{Claims: map[string]string{}},
Claims: buildTestSessionClaimsNoTags("", ""),
Expectedpolicy: YYYYmmddSlashed(Now()),
},
{
PolicyName: "tomorrow",
Claims: policyTemplateData{Claims: map[string]string{}},
Claims: buildTestSessionClaimsNoTags("", ""),
Expectedpolicy: YYYYmmdd(Now().Add(time.Hour * 24)),
},
{
PolicyName: "sha1",
Claims: policyTemplateData{Claims: map[string]string{"Issuer": "a", "Subject": "b"}},
Claims: buildTestSessionClaimsNoTags("a", "b"),
Expectedpolicy: sha1sum("a:b"),
},
}

tpm := NewTestPolicyManager()
tpm := newTestPolicyManager()

for _, tc := range testCases {
got, err := tpm.GetPolicy(tc.PolicyName, tc.Claims)
policyData := GetPolicySessionDataFromClaims(tc.Claims)
got, err := tpm.GetPolicy(tc.PolicyName, policyData)
if err != nil {
t.Errorf("Encountered for policy %s error %s", tc.PolicyName, err)
}
Expand Down
Loading

0 comments on commit 9a1fe97

Please sign in to comment.