Amazon Verified Permissions (AVP): Static vs. Template Policies, RBAC vs. ABAC, and When to Use What
If you’ve ever hardcoded if (user.role === 'lead') checks scattered across a dozen API handlers, you’ve felt the pain AVP is built to solve. This post walks through what AVP is, the two policy types it offers, how to decide between them, and how to actually write the Cedar policies — using a Jira/Asana-style project management tool as the running example so it stays concrete.
1. What is AVP?
Amazon Verified Permissions is a managed, fine-grained authorization service built on Cedar, AWS’s open-source policy language. Instead of scattering authorization logic through your application code, you centralize it into a policy store.
The core loop:
- Define a schema — your entity types (
User,Role,Project,Task), their attributes, and the actions that can be performed on them. - Write policies against that schema.
- Your application calls the
IsAuthorized(orIsAuthorizedWithTokenfor Cognito-integrated apps) API at request time. - AVP evaluates the relevant policies and returns an
ALLOWorDENYdecision.
Cedar supports both RBAC (role/group-based) and ABAC (attribute-based) logic in the same policy language, which is part of what makes it powerful — you’re not locked into one authorization model.
Recommended pattern: one policy store per application, or one per tenant if you’re building a multi-tenant product.
Running example for this post: a project management tool with entities User, Role, Project, and Task — think Jira, Asana, or Linear.
2. Policy types: static vs. template-linked
AVP gives you two ways to write a policy:
- Static policies — fully self-contained. The principal, resource, action, and any conditions are all hardcoded directly into the policy.
- Template-linked policies — created from a policy template, which is a policy with placeholders (
?principal,?resource) instead of hardcoded values. You instantiate a lightweight “linked” policy per user or resource that needs it.
An important mechanic: a template by itself can’t authorize anything. You must create a template-linked policy from it before it participates in any authorization decision. And once it does, updating the template automatically updates every policy linked to it — instantly, across the board.
Example — static policy: alice needs one-off edit access to a single ticket, say because it was shared with her as an external contractor.
Example — template: A reusable “Project Lead” pattern: anyone in that role, on their own project, gets assign/edit rights. You write it once and instantiate it per project.
Static policies are how you implement ACL
If you’ve worked with authorization before, you’ve probably run into ACL (Access Control List) — one of the oldest and simplest authorization models. An ACL is just an explicit list of (subject, resource, permissions) tuples: “this user can do these actions on this resource.” No role, no attribute logic — just a direct grant, decided per subject-resource pair.
Classic examples: Unix file permissions (chmod, per-user/group/other bits) or a shared Google Doc’s “who has access” list.
In Cedar/AVP, every ACL entry is a static policy, because by definition an ACL grant is individual and direct — there’s no shared pattern to extract into a template. For example:
permit (
principal == User::"alice-uuid",
action in [Action::"ViewTask", Action::"EditTask"],
resource == Task::"TASK-991"
);
This is a pure ACL entry: one user, a specific set of actions, one resource. If bob needs a different set of actions on a different task, that’s a completely independent policy — there’s no generalization implied, and there shouldn’t be.
Quick comparison:
| Model | How permission is decided |
|---|---|
| ACL | Per subject-resource pair, listed explicitly, no grouping |
| RBAC | Per role — subject inherits permission by membership |
| ABAC | Computed from attributes at request time, no explicit list |
When ACL-as-static is correct vs. when it’s a smell: if each user’s action set is genuinely different from the next (tech A can list/view forms, tech B can only view, tech C can also submit), that’s real ACL territory — keep it static, and don’t force it into a template just because it “feels repetitive.” You’d lose the per-user granularity that made it necessary in the first place. But if you notice many ACL entries converging on the exact same shape — same actions, just different user/resource pairs — that’s the signal it may be worth promoting to a template instead.
3. Principal — the detail that decides everything
The principal is the actor requesting access — a user, a role, a service. It’s also the single biggest signal for deciding whether a policy should be static or templated.
Ask yourself: does this privilege belong to this one person, or to anyone who holds this role?
- Tied to an individual (e.g., “alice specifically can edit TASK-991”) → the principal is a specific entity → a static policy fits naturally.
- Tied to a role/group membership (e.g., “anyone who is a Project Lead can edit tasks on their project”) → the principal is expressed as a role or a template placeholder (
?principal) → this is where templates shine.
Cedar gives you three ways to scope a principal:
| Scoping method | Example | Typical use |
|---|---|---|
| Exact entity | principal == User::"alice" |
Static, individual grant |
| Group/role membership | principal in Role::"ProjectLead" |
RBAC |
Unconstrained + filtered in when |
principal (then filtered via attributes) |
ABAC |
Getting the principal scoping right upfront saves you from painful policy rewrites later — remember, you can’t convert a static policy into a template-linked one after the fact (more on that in the drawbacks section).
4. Use cases: static vs. template, decided by “who does the change affect?”
The clearest way to decide is to ask: if I change this rule, should it affect one user, or an entire population of users at once?
Case A — Role-wide, dynamic privilege → use a template
“Anyone in Role::"ProjectLead" can assign and edit tasks on their own project.”
This is built as a template with a ?principal placeholder and a condition tying the action to resource.projectId == principal.projectId. If leadership later decides project leads should also be able to close tasks, you edit the template once — every lead, across every project, gets the updated permission instantly. No per-user cleanup, no drift.
Case B — One-off, single-user privilege → use a static policy
“alice gets temporary edit access to TASK-991 only.”
This is scoped to exactly one principal-resource pair and was never meant to generalize. A static policy keeps it isolated — it won’t accidentally apply to anyone else, and it won’t get swept up in a future template edit meant for actual project leads.
Case C — Mixed: broad role, narrow exception → static override on top of a template
“Project leads can edit any task on their project, except ones marked locked pending finance sign-off.”
Here you keep the template-linked permit for the general rule, and layer a static forbid policy on top for the exception. Cedar always evaluates forbid as an override — it wins regardless of what any permit policy says.
Rule of thumb: if a change should ripple across a population of principals, template it. If it only ever applies to one principal-resource pair, keep it static.
5. Writing Cedar: RBAC vs. ABAC
The core mental model: RBAC filters in the policy’s scope (the principal/action/resource head). ABAC filters in the when (or unless) condition block, using entity attributes.
RBAC example — a Viewer role gets read access across the workspace:
permit (
principal in Role::"Viewer",
action == Action::"Read",
resource
);
The decision is made entirely by role membership — no attribute comparison needed.
ABAC example — a project lead can only touch tasks on their own project:
permit (
principal,
action in [Action::"Assign", Action::"Edit"],
resource
)
when {
resource.projectId == principal.projectId
};
Here the scope is unconstrained (principal, resource) — all the real filtering happens in the when clause, comparing an attribute on the resource against an attribute on the principal.
Static forbid override — the Case C exception from above:
forbid (
principal,
action == Action::"Edit",
resource
)
when {
resource.status == "locked"
};
This is a static policy (no placeholders), and because it’s a forbid, it overrides the template-linked permit for project leads whenever resource.status == "locked" — no matter who the principal is.
6. Drawbacks and gotchas
- Static policy edits are limited. You can only directly change the action or a condition clause. Changing the effect (
permit/forbid), principal, or resource requires deleting and recreating the policy. So ifalice’s one-off grant toTASK-991needs to become a grant to a different task, you’re recreating it, not editing it. - No static → template conversion. Once alice’s grant is written as a static policy, you can’t later convert it into a template-linked policy. If you realize this should have been a role-wide pattern, you’re rebuilding it as a template from scratch.
- Template edits ripple immediately, with no staged rollout. Editing the ProjectLead template updates every project’s leads at once — powerful, but there’s no built-in canary or gradual rollout mechanism. Test template changes carefully before pushing.
- Cedar has a learning curve, especially the mental split between “filter in scope” (RBAC) vs. “filter in
when” (ABAC) — worth spending time on with your team upfront. - Mostly console/CLI/SDK-driven. There’s no built-in bulk visual policy editor for managing large policy sets at scale.
7. Limits (current quotas)
Straight from AWS’s documentation as of writing:
- 30,000 policy stores per Region per account (adjustable)
- 40 policy templates per policy store (adjustable)
- 1 identity source per policy store (fixed, not adjustable)
- 1 MB maximum authorization request size
- 10,000 bytes maximum size per individual policy
- 100,000 bytes maximum schema size per policy store
- 200,000 bytes default cap on total policy size scoped to a single resource (adjustable)
- 200 TPS default quota on
IsAuthorized/IsAuthorizedWithTokencalls (raised from 30 TPS in 2024; adjustable via a quota increase request)
If you’re running a policy store per tenant in a multi-tenant PM tool, the policy-store and template-per-store quotas are the ones to watch as you scale.
8. Worth knowing: a recent addition (April 2026)
AVP added support for policy store aliases and named policies/templates. This removes the need to maintain your own ID-mapping tables to associate tenant identifiers with policy store IDs, or to track individual policy/template IDs manually — a real pain point if you’re running one policy store per tenant, which is exactly the pattern many multi-tenant PM tools use.
TL;DR
- Static policy = one specific principal-resource pair, doesn’t generalize.
- Template-linked policy = a reusable pattern for a role, instantiated per resource/principal, edited once to update everywhere.
- RBAC = filter in the scope. ABAC = filter in the
whenclause. - Forbid always overrides permit, regardless of policy type — use it for exceptions.
- Decide static vs. template by asking: if this rule changes, should it affect one user or a whole population?