Business Challenge
#35 walked the seven gates an AWS API request passes through. Verified Permissions is the same question asked about your application's resources — a transaction, a document, a button — which IAM has no vocabulary for. It runs Cedar; AWS states the version: “Verified Permissions currently uses Cedar version 4.7”.
I promised this post would explain why a policy store's schema is the thing that constrains authorisation. Having read the documentation, that promise was wrong, and the correction is the most useful thing here: the schema constrains policy authoring, optionally, at write time. It is not consulted when a decision is made.
The decision is binary — “Valid Values: ALLOW | DENY” — and
determiningPolicies carries the provenance. Three different situations, one word:
“if there are two matching policies, where one is a forbid and the other is a permit, then the forbid policy will be the determining policy” — somebody wrote a rule against this, and it beat a permit.
“In the case that no policies match, and hence the response is DENY, there would be no determining policies” — nobody granted it. Nobody forbade it either.
Those are different incidents. One is a control working. The other is usually a policy that was never written, or was written against an entity type the request does not actually carry. The decision is the same word, and the empty array is the only thing that distinguishes them.
FixLog determiningPolicies with every DENY, and alert differently on an empty one. "Denied by policy X" and "denied because nothing matched" belong in different queues.
“If the action is successful, the service sends back an HTTP 200 response.” And among the fields it sends back:
“errors — Errors that occurred while making an authorization decision, for example, a policy references an Entity or entity Attribute that does not exist in the slice.”
So errors are not a failure mode of the call; they are a field in a successful response. A permit policy that references an attribute your request did not supply produces an entry there, does not contribute to the decision, and the caller gets a perfectly well-formed DENY.
The practical shape: somebody adds an attribute condition to a permit policy, the application is not updated to pass that attribute, and access quietly stops working — with a 200, a DENY, and an explanation sitting in a field the call site has to go looking for.
FixTreat a non-empty errors array as an alarm condition, not as diagnostics. It means your policies and your request shape have drifted apart, and the symptom is denial rather than error.
This is AWS's own warning and it deserves quoting whole:
“if user jane leaves the company, and you later let someone else use the name jane, then
that new user automatically gets access to everything granted by policies that still
reference User::"jane". Cedar can't distinguish between the
new user and the old.”
A policy is durable and an identifier is just a name in it. Hence the instruction: “Always use identifiers that are guaranteed unique and never reused”, with UUIDs “strongly” recommended — and note the scope, because it is easy to read this as an identity problem only: “This applies to both principal and resource identifiers.”
A recycled document reference inherits the permissions written for whatever previously held that reference, with no change to any policy.
FixUUIDs for principals and resources, with AWS's own readability remedy: follow the UUID with a // comment naming the entity, so the policy is still legible to a human.
Architecture
A request you assemble, policies evaluated independently, and a response with three fields that most callers reduce to one.
What you have to assemble, and what gets evaluated
“You must submit principal, action, resource, context, and entities elements”, and then “Verified Permissions evaluates your request against all policies in the requested policy store that apply to the entities in the request”.
The word carrying the weight is entities — the slice of your application's data
that you pass in so Cedar has something to reason over. Authorisation quality is bounded by what you
chose to send, which is a different failure surface from IAM, where the evaluator already knows the
resource. And the errors field exists precisely because a policy can reference something the
slice does not contain.
Evaluation itself is simple and worth stating: “A policy is a statement that either permits or forbids a principal to take one or more actions on a resource. Each policy is evaluated independently of every other policy.” No ordering, no precedence numbers, no inheritance. Independence is what makes the combination rules above — forbid beats permit, multiple permits all count — the whole of the arbitration.
The response, as three fields
| Field | What it tells you | What is lost by ignoring it |
|---|---|---|
decision |
“ALLOW | DENY” | Nothing — but it is the whole of what most call sites keep |
determiningPolicies |
Which policy decided, if any did | Whether a DENY was an explicit forbid or nobody permitting |
errors |
Policies that could not be evaluated against this slice | That a policy meant to grant access did not take part |
Read as a set, those three answer "why" as well as "whether", which is unusual for an authorisation API and is the reason this post exists. Read as one boolean, they answer only the easy half.
“The IsAuthorizedWithToken operation generates an authorization request from user data in JSON web tokens (JWTs)… Verified Permissions populates all attributes to the principal in your request from the claims in users' ID or access tokens.”
And it is not optional which data goes where: “You can't include information about group or user principal types in an IsAuthorizedWithToken request. You must populate all principal data to the JWT that you provide.”
#78 established that a token's claims are a snapshot fixed at issuance β cognito:groups included. So on this path the authorisation decision is made from attributes that were true when the token was signed, and the staleness window is the token lifetime. Fine-grained authorisation does not shorten it; it inherits it.
What the schema actually does
“A schema is a declaration of the structure of the entity types supported by your application, and the actions your application may provide in authorization requests.”
Then three qualifications, each of which narrows it:
| AWS says | So the schema… |
|---|---|
| “The use of schemas in Verified Permissions is optional” | …may not exist at all |
| “control whether policy changes are validated against the schema”, Strict or Off | …does nothing unless a mode is set |
| “all attempts to create or update a policy or policy template are validated” | …is a check on writes |
| “then all new policies must conform with the schema” | …does not revalidate what is already stored |
That last row is the one to sit with, and it is the same shape as #76's unchanged objects never being re-analysed and #49's settings applying from the next cycle. Turning validation on secures the next policy you write. It makes no claim about the ones already there.
And the default position is deliberately permissive: “we recommend leaving validation off while you're developing your application and turning it on for testing and leaving it on while your application is in production”. Sensible advice, and it means the policies written during the period when the schema was not enforced are the ones that have not been checked against it — and will not be until something edits them.
Why This Architecture Holds Up
The provenance fields are a genuinely good design
Most authorisation systems give you a boolean and leave you to reconstruct the reasoning from logs.
Returning determiningPolicies inline means "why was this denied" is answerable at the call
site, in the same response, without a second query. For debugging a policy set, that is worth a great
deal.
It also makes a specific class of audit question answerable: a permit decision with “multiple determining policies” is a request that more than one rule allowed, which is usually fine and occasionally a sign that a broad policy is doing work a narrow one was supposed to do.
Turning validation off has an interface asymmetry worth knowing
In the console, disabling validation requires an explicit typed acknowledgement: “Type confirm to confirm that updates to policies will no longer be validated against your schema.”
Through the API it is a parameter: “You can change the validation mode for a policy store by using the UpdatePolicyStore operation and specifying a different value for the ValidationSettings parameter.”
The guardrail lives in one interface. That is a common and defensible pattern — a human clicking
needs the speed bump, automation needs to be scriptable — and it means the control you actually
want is a Service Control Policy or a Config rule on UpdatePolicyStore, not the console
prompt. The console prompt protects against a slip; it does not protect against a pipeline.
“Do not include personally identifying, confidential, or sensitive information as part of the unique identifier for your principals or resources. These identifiers are included in log entries shared in AWS CloudTrail trails.”
Two instructions converge here and they reinforce each other. Use UUIDs because names get reused; use UUIDs also because the identifier ends up in an audit log with a different access policy and a different retention period from your application database. An email address as a principal identifier fails both tests at once.
This is the same concern as #50, where project names in cost allocation tags travel into finance reports. An identifier chosen for one system's convenience is read by systems you did not have in mind.
Policy names are a usability feature with a uniqueness rule
“Policy names must be unique for all policies within the policy store and prefixed with
name/… You can use a policy name in place of the policy ID in control plane
operations that accept a policyId parameter.”
Useful, because a policy ID in an incident channel is unreadable. Worth pairing with the identifier warning though: a policy name is required to be unique within the store, while an entity identifier has no such guarantee — uniqueness there is entirely your discipline. The system enforces it for the thing it owns and asks you to enforce it for the thing you own.
Batch is about quota, and AWS says so
Of BatchIsAuthorized, AWS writes that it “groups requests into a single batch
operation that minimizes quota usage and returns authorization decisions for each of up
to 30 complex nested actions”.
Which tells you the intended UX pattern: deciding which of thirty buttons to render is one call, not thirty. It also sets the ceiling — a page with more than thirty independently authorised elements needs either several calls or a coarser authorisation model, and that is a design constraint worth knowing before the screen is built rather than after.
Key Architecture Decisions
| Decision | Choice | Reasoning |
|---|---|---|
| What to log on a DENY | decision and determiningPolicies |
An empty list is the only signal that nothing matched rather than something forbade. |
Handling the errors array |
Alarm on non-empty | It arrives with a 200 and a decision; a policy that errored did not participate. |
| Principal and resource identifiers | UUIDs, never reused, with a // comment |
“Cedar can't distinguish between the new user and the old”, and it applies to resources too. |
| What must not be in an identifier | Anything personal or confidential | They appear “in log entries shared in AWS CloudTrail trails”. |
| Schema | Write one, and know it is optional | “optional, but… highly recommended for production software”. |
| Validation mode in production | Strict | AWS's own recommendation, and without it the schema constrains nothing. |
| When switching validation on | Re-check the policies written before | It applies to “all new policies” β existing ones are not revalidated. |
| Protecting validation mode | Guard UpdatePolicyStore, not the console |
The typed confirmation exists only in the console; the API takes a parameter. |
| Token-based authorisation | Accept that staleness equals token lifetime | Principal attributes come “from the claims”, and all principal data must be in the JWT. |
| UX permission checks | Batch them | Up to 30 per call, and AWS frames it as minimising quota usage. |
| Pinning Cedar behaviour | Record the version you tested against | AWS names it — “Cedar version 4.7” — so it is a moving dependency. |
The audit worth running
Grep your policy store for quoted identifiers that are not UUIDs. Every one is a name that could be
reissued, and AWS's jane example is the exact failure. Then do the same for resource
identifiers, which is the half people skip.
Then check the policy store's validation mode and compare it against the creation dates of your policies. Anything authored before Strict was switched on has never been checked against the schema, and nothing will check it until somebody edits it.
Closing Thought
Verified Permissions is unusually forthcoming. It returns the deciding policy, it returns evaluation errors, it tells you the Cedar version, and it warns about identifier reuse with a worked example involving a named employee. Almost everything in this post is a quotation rather than an inference.
What the service cannot do is make a caller read three fields instead of one. A DENY is immediately actionable as a boolean, and the two fields explaining it are optional in practice even though they are present in every response. So the most likely way to be wrong about your own authorisation system is to have discarded the part of the answer that says why.
Which rhymes with the five posts before it, and then diverges in a way worth naming. #74 through #77 were numbers answering adjacent questions. #78 was an action taking effect in the wrong system. This one is the opposite problem: the information is in the response, correct and complete, and gets dropped on the floor by the code receiving it. A gap in the documentation can be closed by reading. A gap in what your own code keeps can only be closed by changing the code.
Security & Identity — AWS Resource Access Manager: what a share actually grants once it is accepted, why a resource share and a resource policy are not interchangeable, and what happens to access when a share is deleted.
Official AWS Reference
- IsAuthorized API reference — the ALLOW and DENY values, the
determiningPoliciessemantics including forbid beating permit and the empty list when no policies match, theerrorselement, and the HTTP 200 on success - Amazon Verified Permissions policies — independent evaluation of each policy, the identifier-reuse warning and its
janeexample, the UUID and CloudTrail guidance, and the policy-name uniqueness rule - Policy store schema — the definition of a schema, that its use is optional though recommended for production, and that activating validation requires all new policies to conform
- Enabling policy validation mode — what validation gates, the Strict and Off modes, the recommendation to develop with it off, the typed console confirmation, and the
UpdatePolicyStoreparameter - Implementing authorization — the elements an
IsAuthorizedrequest must carry, how token-based authorisation populates principal attributes from JWT claims, and the 30-item batch limit - What is Amazon Verified Permissions — the Cedar version in use
- Policy templates — further reading on parameterising policies, which interacts with the identifier guidance above; no claims in this post are drawn from it
Comments