diff --git a/infrastructure/lib/constructs/managed-kb/managed-kb-role-construct.ts b/infrastructure/lib/constructs/managed-kb/managed-kb-role-construct.ts index c2242aaa..047a5fc0 100644 --- a/infrastructure/lib/constructs/managed-kb/managed-kb-role-construct.ts +++ b/infrastructure/lib/constructs/managed-kb/managed-kb-role-construct.ts @@ -127,6 +127,25 @@ export function grantManagedKbProvisioning( 'bedrock:DeleteKnowledgeBase', 'bedrock:CreateDataSource', 'bedrock:DeleteDataSource', + // `CreateKnowledgeBase` is called WITH tags (provisioning.py passes + // `tags=build_tags(...)`), and AWS authorises the tagging as a separate + // `bedrock:TagResource` action against `knowledge-base/*` — the resource + // does not exist yet, so the wildcard is the only thing it can match. + // Without this the create fails outright: + // + // AccessDeniedException: not authorized to perform bedrock:TagResource + // + // and it fails *after* passing every review, because the create action + // itself is granted. Those tags are not decoration: they are what the + // reconciler and teardown match knowledge bases on, so creating untagged + // would be worse than failing. + 'bedrock:TagResource', + // The reconciler reads tags to decide what belongs to this project + // (`tombstones.iter_project_knowledge_bases` → `list_tags_for_resource`). + // It fails closed on a read error, so without this permission every + // knowledge base looks untagged, matches nothing, and the orphan sweep + // silently reports a clean account forever. + 'bedrock:ListTagsForResource', ], resources: [knowledgeBaseArnWildcard(config)], })); diff --git a/infrastructure/test/managed-kb.test.ts b/infrastructure/test/managed-kb.test.ts index 6da0eae1..c0a64004 100644 --- a/infrastructure/test/managed-kb.test.ts +++ b/infrastructure/test/managed-kb.test.ts @@ -321,10 +321,36 @@ describe('ManagedKbRoleConstruct — caller grants', () => { 'bedrock:DeleteKnowledgeBase', 'bedrock:CreateDataSource', 'bedrock:DeleteDataSource', + 'bedrock:TagResource', + 'bedrock:ListTagsForResource', ]); expect(s.Resource).toBe(KB_ARN_WILDCARD); }); + it('can tag a knowledge base at create time', () => { + // `CreateKnowledgeBase` is called WITH tags, and AWS authorises the tagging + // as a separate action. Missing it, provisioning failed in dev with + // + // AccessDeniedException: not authorized to perform bedrock:TagResource + // + // *after* passing review, because the create action itself was granted. The + // tags are what the reconciler and teardown match on, so an untagged + // knowledge base would be worse than a failed create. + const s = statementBySid(t, 'ManagedKbProvisionCrud'); + expect(s.Action).toContain('bedrock:TagResource'); + // Must be the wildcard: at create time the knowledge base has no ARN, so a + // resource-specific grant could never match. + expect(s.Resource).toBe(KB_ARN_WILDCARD); + }); + + it('can read tags, which is how the reconciler recognises its own resources', () => { + // `iter_project_knowledge_bases` fails closed on a tag read error, so + // without this the orphan sweep sees every knowledge base as untagged, + // matches nothing, and reports a clean account forever. + const s = statementBySid(t, 'ManagedKbProvisionCrud'); + expect(s.Action).toContain('bedrock:ListTagsForResource'); + }); + it('keeps the non-resource-scopable create/list actions in their own statement', () => { const s = statementBySid(t, 'ManagedKbProvisionCreateList'); expect(s.Action).toEqual(['bedrock:CreateKnowledgeBase', 'bedrock:ListKnowledgeBases']);