diff --git a/cmd/almost-e2e_test.go b/cmd/almost-e2e_test.go index 937a129..c24aef3 100644 --- a/cmd/almost-e2e_test.go +++ b/cmd/almost-e2e_test.go @@ -280,11 +280,34 @@ var testPolicyAllowTeamFolder string = fmt.Sprintf(`{ ] }`, testingBucketNameBackenddetails, testTeamFile, testTeamTag, testAllowedTeam) +var testPolicyAllowAllInRegion1ARN string = "arn:aws:iam::000000000000:role/AllowAllInRegion1" + +//This policy is to test whether a policy can be scoped to a specific region +//since our proxy uses region to determine a backend this makes sure to be able +//to have different permissions for different backends. This is used in test cases +//that start with TestPolicyAllowAllInRegion1 +var testPolicyAllowAllInRegion1 string = fmt.Sprintf(`{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "s3:*", + "Resource": "*", + "Condition" : { + "StringLike" : { + "aws:RequestedRegion": "%s" + } + } + } + ] +}`, testRegion1) + func NewTestPolicyManagerAlmostE2EPolicies() *PolicyManager { return NewPolicyManager( TestPolicyRetriever{ testPolicies: map[string]string{ testPolicyAllowTeamFolderARN: testPolicyAllowTeamFolder, + testPolicyAllowAllInRegion1ARN: testPolicyAllowAllInRegion1, }, }, ) @@ -327,7 +350,7 @@ func TestPolicyAllowTeamFolderIDPClaimsCanBeUsedInPolicyEvaluationPrincipalWithI t.Error("We should have gotten a Forbidden error but no error was raised.") } if err.ErrorCode() != "AccessDenied" { - t.Errorf("Expected Forbidden, got %s", err.ErrorCode()) + t.Errorf("Expected AccessDenied, got %s", err.ErrorCode()) } } @@ -347,6 +370,36 @@ func TestPolicyAllowTeamFolderIDPClaimsCanBeUsedInPolicyEvaluationPrincipalWitho t.Error("We should have gotten a Forbidden error but no error was raised.") } if err.ErrorCode() != "AccessDenied" { - t.Errorf("Expected Forbidden, got %s", err.ErrorCode()) + t.Errorf("Expected AccessDenied, got %s", err.ErrorCode()) } +} + +func TestPolicyAllowAllInRegion1ConditionsOnRegionAreEnforced(t *testing.T) { + tearDown, getSignedToken := testingFixture(t) + defer tearDown() + token := getSignedToken("mySubject", time.Minute * 20, AWSSessionTags{PrincipalTags: map[string][]string{"org": {"a"}}}) + //Given the policy Manager that has our test policies + pm = *NewTestPolicyManagerAlmostE2EPolicies() + //Given credentials that use the policy that allow everything in Region1 + creds := getCredentialsFromTestStsProxy(t, token, "my-session", testPolicyAllowAllInRegion1ARN) + + //WHEN we get an object in region 1 + regionContent, err := getRegionObjectContent(t, testRegion1, creds) + //THEN it should just succeed as any action is allowed + if err != nil { + t.Errorf("Could not get region content due to error %s", err) + } else if regionContent != testRegion1 { + t.Errorf("when retrieving region file for %s we got %s", testRegion1, regionContent) + } + + //WHEN we get an object in region2 + regionContent2, err2 := getRegionObjectContent(t, testRegion2, creds) + //THEN we expect it to give an access denied as no explicit allow exists for which region is not excluded via a condition + if err2 == nil { + t.Errorf("Could get region content %s but policy should have limited to %s", regionContent2, testRegion1) + } else { + if err2.ErrorCode() != "AccessDenied" { + t.Errorf("Expected AccessDenied, got %s", err.ErrorCode()) + } + } } \ No newline at end of file diff --git a/cmd/handler-builder.go b/cmd/handler-builder.go index d390849..01b68c0 100644 --- a/cmd/handler-builder.go +++ b/cmd/handler-builder.go @@ -91,7 +91,7 @@ func cleanHeadersThatAreNotSignedInAuthHeader(ctx context.Context, req *http.Req // Authorize an S3 action // maxExpiryTime is an upperbound for the expiry of the session token -func authorizeS3Action(ctx context.Context, sessionToken string, action S3ApiAction, w http.ResponseWriter, r *http.Request, maxExpiryTime time.Time) (allowed bool) { +func authorizeS3Action(ctx context.Context, sessionToken, targetRegion string, action S3ApiAction, w http.ResponseWriter, r *http.Request, maxExpiryTime time.Time) (allowed bool) { allowed = false sessionClaims, err := ExtractTokenClaims(sessionToken, s3ProxyKeyFunc) if err != nil { @@ -112,6 +112,7 @@ func authorizeS3Action(ctx context.Context, sessionToken string, action S3ApiAct } policySessionData := GetPolicySessionDataFromClaims(sessionClaims) + policySessionData.RequestedRegion = targetRegion 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) @@ -223,7 +224,7 @@ func (hb handlerBuilder) Build(action S3ApiAction, presigned bool) (http.Handler //To have a valid signature r.Header.Add(constants.AmzContentSHAKey, constants.EmptyStringSHA256) - if authorizeS3Action(ctx, creds.SessionToken, action, w, r, getCutoffForPresignedUrl()){ + if authorizeS3Action(ctx, creds.SessionToken, targetBackendId, action, w, r, getCutoffForPresignedUrl()){ hb.proxyFunc(ctx, w, r, targetBackendId) } return @@ -271,7 +272,7 @@ func (hb handlerBuilder) Build(action S3ApiAction, presigned bool) (http.Handler targetRegion := requestutils.GetRegionFromRequest(r, globalBackendsConfig.defaultBackend) //Authn done time to perform authorization - if authorizeS3Action(ctx, creds.SessionToken, action, w, r, time.Now().UTC()){ + if authorizeS3Action(ctx, creds.SessionToken, targetRegion, action, w, r, time.Now().UTC()){ hb.proxyFunc(ctx, w, r, targetRegion) } return diff --git a/cmd/policy-generation.go b/cmd/policy-generation.go index 45b5834..671723d 100644 --- a/cmd/policy-generation.go +++ b/cmd/policy-generation.go @@ -145,6 +145,7 @@ type PolicySessionClaims struct { type PolicySessionData struct { Claims PolicySessionClaims Tags AWSSessionTags + RequestedRegion string } func GetPolicySessionDataFromClaims(claims *SessionClaims) *PolicySessionData { diff --git a/cmd/policy-iam-action.go b/cmd/policy-iam-action.go index 97858cd..4e62e5d 100644 --- a/cmd/policy-iam-action.go +++ b/cmd/policy-iam-action.go @@ -63,6 +63,7 @@ func getS3ObjectFromRequest(req *http.Request) (bucketName string, objectKey str //Add context keys that are added to nearly all requests that contain information about the current session func addGenericSessionContextKeys(context map[string]*policy.ConditionValue, session *PolicySessionData) { addAwsPrincipalTagConditionKeys(context, session) + addAwsRequestedRegionConditionKey(context, session) } //Add aws:PrincipalTag/tag-key keys that are added to nearly all requests that contain information about the current session @@ -76,6 +77,17 @@ func addAwsPrincipalTagConditionKeys(context map[string]*policy.ConditionValue, } } +//Add aws:RequestedRegion key that are added to all requests +//https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_aws_deny-requested-region.html +func addAwsRequestedRegionConditionKey(context map[string]*policy.ConditionValue, session *PolicySessionData) { + if session == nil { + return + } + if session.RequestedRegion != "" { + context["aws:RequestedRegion"] = policy.NewConditionValueString(true, session.RequestedRegion) + } +} + //Buid a new IAM action based out of an HTTP Request. The IAM action should resemble the required //Permissions. The api_action is passed in as a string argument func newIamActionsFromS3Request(api_action S3ApiAction, req *http.Request, session *PolicySessionData) (actions []iamAction, err error) {