Skip to content

ADR-152: Acquire policies grant the item actions they write with

Status: Accepted Date: 2026-10-20 Issue: #750, #756 Supersedes: ADR-124 (its permission matrix only; the policy names stand) Related: ADR-117, ADR-150

Context

ADR-124 renamed the managed policies and fixed their permission matrix. That matrix gave AcquireOnlyPolicy (-acq) TransactWriteItems but no PutItem or UpdateItem, on the assumption that TransactWriteItems authorises the items inside a transaction. The namespace-scoped NamespaceAcquirePolicy (-ns-acq) was given UpdateItem but no PutItem.

AWS does not authorise a transaction that way. Each item is authorised by its own action: "Permissions for Put, Update, Delete, and Get actions are governed by the permissions used for the underlying PutItem, UpdateItem, DeleteItem, and GetItem operations. For the ConditionCheck action, you can use the dynamodb:ConditionCheckItem permission" (Using IAM with DynamoDB transactions). acquire() writes with UpdateItem (the speculative fast path, adjustments, rollbacks, and every Update in the slow path's transaction) and with PutItem (every bucket create). A principal holding only -acq could therefore not write at all, and one holding only -ns-acq could never create a bucket. IAM defines no TransactWriteItems action at all: the DynamoDB service reference lists none, so the entry granted nothing. The optional {stack}-app role carries -acq alone. moto and LocalStack do not evaluate IAM, so no test below real AWS could see it.

Decision

The policy names from ADR-124 are unchanged. The table-level permission matrix must be the following. Each namespace-scoped policy (-ns-read, -ns-acq, -ns-full) mirrors its table-level counterpart within its dynamodb:LeadingKeys condition: it must grant the same item actions (GetItem, BatchGetItem, Query, ConditionCheckItem, PutItem, UpdateItem, DeleteItem, as the row allows), and leaves out the table-wide ones (Scan, DescribeTable). -ns-full also grants BatchWriteItem (#742: delete_entity deletes with it); IAM checks LeadingKeys on every item of a batch, so it reaches only the keys PutItem and DeleteItem already reach. No policy lists TransactWriteItems, which IAM does not define.

Action ReadOnly (-read) AcquireOnly (-acq) FullAccess (-full)
GetItem ✅ ✅ ✅
BatchGetItem ✅ ✅ ✅
Query ✅ ✅ ✅
Scan ✅ ❌ ✅
DescribeTable ✅ ❌ ✅
ConditionCheckItem ❌ ✅ ✅
PutItem ❌ ✅ bucket items ✅
UpdateItem ❌ ✅ bucket items ✅
DeleteItem ❌ ❌ ✅
BatchWriteItem ❌ ❌ ✅

-acq gains PutItem and UpdateItem; -ns-acq gains PutItem. An acquire policy must grant every item action acquire() issues, whether alone or inside a transaction, and must grant PutItem and UpdateItem on bucket items only: dynamodb:LeadingKeys */BUCKET#* (and {namespace id}/BUCKET#* for -ns-acq), with Null false on the key. Every write acquire() issues is a bucket item, whose partition key is {ns}/BUCKET#…; namespace and entity ids contain no / or #, so the only other items the grant reaches are those sharing a bucket's partition: ADR-149's operation records (SK=#OP#…, in shard 0's partition). An acquire principal can therefore write such a record, which can only make a later reset or top-up reusing that key return the stored result instead of applying (an under-grant); it raises no balance it could not already raise (see Negative).

Consequences

Positive: - acquire() works under the template's acquire policies and the {stack}-app role, as their names promise. - The matrix states what AWS actually evaluates, and a real-AWS test pins it.

Negative: - A principal holding only an acquire policy cannot create entities, set limits, reset or top up a bucket, or register a namespace on a first Repository.open(): those write config, entity, audit, registry or version items, which are operator actions (-full, -ns-full). Breaking for an application that did them with an acquire policy alone. - Least privilege, not a boundary: a principal that may acquire still writes its own bucket items, so it can raise their balances with a direct write (ADR-149).

Alternatives Considered

Keep ADR-124's matrix and document that -acq cannot write

Rejected because: a policy named for acquire() that cannot run acquire() is the misleading name ADR-124 set out to remove.

Grant the write actions on every item

Rejected because: it lets an application role write config, entity records, audit events and the version record (#756). This ADR first rejected the bucket-only pattern on the belief that a table-level pattern cannot single out buckets; */BUCKET#* does, since LeadingKeys patterns match across the namespace id.

Route every acquire() write through TransactWriteItems

Rejected because: AWS authorises a transaction's items by the same per-item actions, so it would still need PutItem / UpdateItem, and it doubles the cost of every write.