From 20617348740df65a51b4c0b93ae383683c9b3218 Mon Sep 17 00:00:00 2001 From: Saleh Date: Sat, 25 Jul 2026 21:02:24 +0300 Subject: [PATCH] events: add Condition field to IAMPolicyStatement IAMPolicyStatement is the element of the policy a Lambda custom authorizer returns (and of IoT policy documents), which follow the IAM JSON policy grammar where Condition is a first-class element. The struct had no Condition field, so any condition block was silently dropped on (un)marshal. Add Condition as map[string]map[string]interface{} with omitempty. The interface{} value type is required because a condition value may be a single string or an array of strings; omitempty keeps condition-less statements serializing unchanged (keeping the custom-authorizer response golden test green). Fixes #458 --- events/iam.go | 5 +++++ events/iam_test.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 events/iam_test.go diff --git a/events/iam.go b/events/iam.go index 090530dd..ef2276b2 100644 --- a/events/iam.go +++ b/events/iam.go @@ -11,4 +11,9 @@ type IAMPolicyStatement struct { Action []string Effect string Resource []string + // Condition is an optional IAM condition block. Its value type is + // interface{} because a condition value may be either a single string or an + // array of strings, and omitempty keeps statements without conditions + // serializing unchanged. + Condition map[string]map[string]interface{} `json:"Condition,omitempty"` } diff --git a/events/iam_test.go b/events/iam_test.go new file mode 100644 index 00000000..efb65dc4 --- /dev/null +++ b/events/iam_test.go @@ -0,0 +1,53 @@ +// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +package events + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIAMPolicyStatementConditionRoundTrip(t *testing.T) { + // A real IAM policy statement (as returned by an API Gateway custom + // authorizer) can carry a Condition block; it must survive a round trip. + const input = `{"Action":["execute-api:Invoke"],"Effect":"Deny","Resource":["arn:aws:execute-api:us-east-1:123456789012:apiId/stage/GET/path"],"Condition":{"IpAddress":{"aws:SourceIp":["1.2.3.4/24"]}}}` + + var stmt IAMPolicyStatement + require.NoError(t, json.Unmarshal([]byte(input), &stmt)) + + output, err := json.Marshal(stmt) + require.NoError(t, err) + assert.JSONEq(t, input, string(output)) + + // A JSON array condition value unmarshals to []interface{}. + assert.Equal(t, []interface{}{"1.2.3.4/24"}, stmt.Condition["IpAddress"]["aws:SourceIp"]) +} + +func TestIAMPolicyStatementScalarCondition(t *testing.T) { + // A condition value may also be a single scalar rather than an array, which + // is why the value type has to be interface{} and not []string. + const input = `{"Action":["execute-api:Invoke"],"Effect":"Allow","Resource":["*"],"Condition":{"Bool":{"aws:MultiFactorAuthPresent":"true"}}}` + + var stmt IAMPolicyStatement + require.NoError(t, json.Unmarshal([]byte(input), &stmt)) + assert.Equal(t, "true", stmt.Condition["Bool"]["aws:MultiFactorAuthPresent"]) + + output, err := json.Marshal(stmt) + require.NoError(t, err) + assert.JSONEq(t, input, string(output)) +} + +func TestIAMPolicyStatementWithoutConditionOmitted(t *testing.T) { + // A statement without a condition must not serialize "Condition":null, + // otherwise the custom-authorizer response golden round trip breaks. + output, err := json.Marshal(IAMPolicyStatement{ + Action: []string{"execute-api:Invoke"}, + Effect: "Allow", + Resource: []string{"*"}, + }) + require.NoError(t, err) + assert.JSONEq(t, `{"Action":["execute-api:Invoke"],"Effect":"Allow","Resource":["*"]}`, string(output)) +}