From bce832c63b6fa40f4140891c4d66da6d044a79b6 Mon Sep 17 00:00:00 2001 From: Josh Siviour Date: Tue, 18 Aug 2026 14:21:38 +1000 Subject: [PATCH] fix(bedrock): authorize runtime/agent actions under the bedrock: IAM namespace (#820) The gateway derived each request's IAM action prefix from the wire-service name, so bedrock-runtime, bedrock-agent and bedrock-agent-runtime calls were checked as bedrock-runtime:ConverseStream, bedrock-agent-runtime:Retrieve, etc. AWS namespaces every Bedrock action under bedrock:, so an AWS-shaped scoped policy never matched and only a wildcard admitted the call. Collapse the Bedrock family to the shared bedrock: namespace in IAMAction, the single choke point for policy decisions; recordResolvedAction keeps the wire name for telemetry. --- spinifex/gateway/policy/actions.go | 27 ++++++++++++++++++++++--- spinifex/gateway/policy/actions_test.go | 27 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 spinifex/gateway/policy/actions_test.go diff --git a/spinifex/gateway/policy/actions.go b/spinifex/gateway/policy/actions.go index df92be8b2..395d0411c 100644 --- a/spinifex/gateway/policy/actions.go +++ b/spinifex/gateway/policy/actions.go @@ -1,6 +1,27 @@ package policy -// IAMAction formats a service and action as the IAM policy string "service:ActionName". -func IAMAction(service, action string) string { - return service + ":" + action +// bedrockIAMFamily lists the Bedrock wire/signing service names that all +// authorize under the single "bedrock:" IAM namespace in AWS (runtime, agent, +// and agent-runtime actions are namespaced as bedrock: on AWS). +var bedrockIAMFamily = map[string]bool{ + "bedrock-runtime": true, + "bedrock-agent": true, + "bedrock-agent-runtime": true, +} + +// iamNamespace maps a wire service name to its IAM policy namespace, collapsing +// the Bedrock family to the shared "bedrock:" namespace. Every other service +// keeps its own name. +func iamNamespace(service string) string { + if bedrockIAMFamily[service] { + return "bedrock" + } + return service +} + +// IAMAction formats a service and action as the IAM policy string +// "namespace:ActionName", collapsing the Bedrock family to the shared bedrock: +// namespace so AWS-shaped policies authorize as they do on AWS. +func IAMAction(service, action string) string { + return iamNamespace(service) + ":" + action } diff --git a/spinifex/gateway/policy/actions_test.go b/spinifex/gateway/policy/actions_test.go new file mode 100644 index 000000000..c63c23c3c --- /dev/null +++ b/spinifex/gateway/policy/actions_test.go @@ -0,0 +1,27 @@ +package policy + +import "testing" + +// TestIAMAction_BedrockNamespace asserts the Bedrock wire-service family +// collapses to the shared "bedrock:" IAM namespace, while other services keep +// their own prefix, so AWS-shaped policies authorize as they do on AWS. +func TestIAMAction_BedrockNamespace(t *testing.T) { + cases := []struct { + service, action, want string + }{ + {"bedrock", "Converse", "bedrock:Converse"}, + {"bedrock-runtime", "ConverseStream", "bedrock:ConverseStream"}, + {"bedrock-runtime", "InvokeModel", "bedrock:InvokeModel"}, + {"bedrock-agent", "CreateKnowledgeBase", "bedrock:CreateKnowledgeBase"}, + {"bedrock-agent-runtime", "Retrieve", "bedrock:Retrieve"}, + {"bedrock-agent-runtime", "RetrieveAndGenerate", "bedrock:RetrieveAndGenerate"}, + {"ecr", "GetAuthorizationToken", "ecr:GetAuthorizationToken"}, + {"ecs", "RunTask", "ecs:RunTask"}, + {"s3", "GetObject", "s3:GetObject"}, + } + for _, c := range cases { + if got := IAMAction(c.service, c.action); got != c.want { + t.Errorf("IAMAction(%q, %q) = %q, want %q", c.service, c.action, got, c.want) + } + } +}