← Back to list

Resources, Scopes, Permissions & Policies in Keycloak — A Practical Guide for Mapping Legacy RBAC

You’ve got ~250 “capabilities” today (e.g., viewAccount, viewTransaction) granted via user–group membership and the occasional direct…

Ajay Kumar · 2025-10-18 10:52 · 4 claps · 6.5 min read paywalled
#keycloak #permission #custom-policies #rbac-access-control #cybersecurity
Open on Medium ↗
Wiki topics: 🔒 · Cybersecurity

Resources, Scopes, Permissions & Policies in Keycloak — A Practical Guide for Mapping Legacy RBAC

You’ve got ~250 “capabilities” today (e.g., viewAccount, viewTransaction) granted via user–group membership and the occasional direct grant. You want to migrate this to Keycloak’s Authorization Services without drowning in a sea of objects. Good news: you can keep your mental model and end up with something cleaner, more scalable, and easier to reason about.

Below is a field-tested way to translate legacy RBAC into Keycloak’s resources/scopes/permissions/policies, plus a naming blueprint and migration checklist you can apply immediately.

Keycloak for Developer : https://tobiweissmann.gumroad.com/l/dixdgp

Summary (answers first)

Scopes: Prefer verb-style scopes reused across a resource type (e.g., view, create, update, delete) not “baked” scopes like viewAccount. Attach the same scope name to different resource types (e.g., account, transaction). This keeps scope names short and consistent while still being precise.

Permissions: Create scope-based permissions for operations (e.g., “View Account”) and resource-based permissions for ownership/location constraints. One permission per meaningful rule is fine; don’t try to encode the entire matrix into one mega-permission.

Conflicts / multiples: Final access is decided by Decision Strategy. If more than one permission applies, Affirmative means any “permit” wins; Unanimous means all relevant permissions must permit.

Groups & roles: You don’t need one policy per group. Use a Group-based or Role-based policy that lists multiple groups/roles. You can also layer Aggregated policies to compose rules.

Outcome: Your old viewAccount capability becomes:

a resource type account (+ many resource instances),

a view scope attached to the resource type,

a scope-based permission “View Account,”

with a policy referencing your “Help Desk” role/group (or any others). It’s conceptually the same, but now consistent and extensible.

A Clean Mental Model

Think of Keycloak Authorization like this:

Resource = “What” you guard (e.g., an account, a specific account/{id}, a transaction).

Scope = “What action/operation” on that resource (e.g., view, export, approve).

Policy = “Who or under what conditions” (e.g., “Help Desk role,” “Group = Finance,” “User attribute region=EU,” “Time of day,” “Owner is caller”).

Permission = “Glue” that applies one or more policies to a resource and/or scope (e.g., “View Account” permission = policy(Help Desk OR Account Owner) → scope(view) on resource type account).

This separation keeps your system maintainable as requirements evolve.

Scopes: Shared Verbs, Specific Resource Types

Your question: “Should I create one global view scope, or viewAccount, viewTransaction, etc.?”

Recommendation: Use shared verb scopes (e.g., view) and bind them to specific resource types (e.g., account, transaction). In practice:

Define resource types: account, transaction, customer, etc.

For each type, attach the verbs you need: view, create, update, delete, plus any domain-specific verbs (export, approve, reconcile, …).

Avoid baking the resource name into the scope (e.g., viewAccount). You’ll end up duplicating the same idea and exploding your scope count.

Why this works well

Consistency: Everyone knows what view means across the board.

Composability: You can express rules like “to view transactions you must also be allowed to view their parent account.”

Refactoring-friendly: Adding a new resource type rarely forces a scope explosion.

If you absolutely must disambiguate, prefer a namespaced style like account:view and transaction:view. But conceptually it’s still a verb reused across types.

Permissions: How Many, and Which Kind?

Keycloak gives you two key flavors:

Scope-based permission: Evaluate policies in the context of one or more scopes (optionally constrained to a resource or resource type).

Resource-based permission: Evaluate policies in the context of one or more resources (optionally with their scopes).

Rule of thumb

Use scope-based permissions to model operations (“Can X do view on account?”).

Use resource-based permissions to capture object-level conditions (e.g., “User is the owner of account/{id},” “Account region = user.region,” “Resource attribute sensitivity=low,” etc.).

This separation keeps policy logic small and testable.

Do I need a permission for every resource/scope pair? Not literally one per instance. Typically you’ll create permissions at the resource type level (e.g., a single “View Account” permission that applies to all account resources) and optionally add instance-level constraints via attributes/policies when needed.

What if Multiple Permissions Apply?

Keycloak evaluates all applicable permissions and policies, then combines results using Decision Strategy:

Affirmative (at least one permission says “permit” → allow)

Unanimous (all relevant permissions must say “permit” → allow)

Inside each permission, you can also choose its Decision Strategy for combining multiple policies. And any policy can be marked Negative (logical NOT) to invert its result.

Practical tip:

On your resource server (the client’s Authorization → Settings), start with Unanimous for safety.

Only switch to Affirmative if you intend “any green light is enough.”

Mapping Your Legacy Model

Today

A User

Is in one or more Groups

Groups (and sometimes users directly) have Capabilities like viewAccount, viewTransaction

Tomorrow (Keycloak)

Resources

Resource type: account → instances like account/{id}

Resource type: transaction → instances like transaction/{id}

Scopes

For account: view, update, export

For transaction: view, refund, void

Policies

Role-based (“Help Desk”, “Account Owner”, “Ops Manager”)

Group-based (“Group = Finance”, “Group = Tier-2 Support”)

Attribute-based (“user.region = resource.region”)

Time-based / script-based as needed Use Aggregated policies to OR/AND these together cleanly.

Permissions

Scope-based: “View Account” → applies to scope view on resource type account; uses policy Help Desk OR Account Owner.

Scope-based: “View Transaction” → scope view on resource type transaction; policy Help Desk OR Account Owner OR Auditor.

Resource-based: “Owns Account” → resource account (any), policy user.id == resource.ownerId (script/attributes).

Combine with Unanimous at the server level if you require both the operation right and the ownership right.

Why this mirrors your old system nicely Your viewAccount capability becomes a permission (“View Account”), enforced via a policy (“Help Desk membership” etc.) on a well-defined resource type (account) and scope (view). It reads like plain English and scales with you.

Do I Need a Policy per Legacy Group?

No. Use one Group-based policy that lists many groups, or a few policies grouped via an Aggregated policy. The same goes for roles. This avoids policy sprawl.

Example

Policy: Policy.HelpDesk (Group-based: helpdesk)

Policy: Policy.AccountOwner (Role-based: account_owner)

Aggregated policy: Policy.CanViewAccount = HelpDesk OR AccountOwner

Attach Policy.CanViewAccount to your “View Account” permission.

Worked Example: Accounts & Transactions

Let’s model two capabilities you mentioned:

Resources & Scopes

Resource type: account Scopes: view, update

Resource type: transaction Scopes: view, refund

Policies

Role.HelpDesk (Role-based: helpdesk)

Role.AccountOwner (Role-based: account_owner)

Attr.SameRegion (Script/Attribute-based: user.region == resource.region)

Aggregated:

CanViewAccount = Role.HelpDesk OR Role.AccountOwner

CanViewTransaction = Role.HelpDesk OR Role.AccountOwner

Permissions

Scope-based: Perm.ViewAccount

Scopes: view (on resource type account)

Apply Policy: CanViewAccount AND Attr.SameRegion

Decision Strategy: Unanimous

Scope-based: Perm.ViewTransaction

Scopes: view (on resource type transaction)

Apply Policy: CanViewTransaction AND Attr.SameRegion

Decision Strategy: Unanimous

Optional cross-dependency: require account visibility before transaction visibility

Add an Aggregated policy HasAccountVisibility that internally evaluates Perm.ViewAccount (or reproduces the same conditions).

Then set Perm.ViewTransaction to require HasAccountVisibility and CanViewTransaction.

Result

Help Desk and Account Owners can view accounts and transactions in their region.

Changing the condition (e.g., region) is a policy tweak, not a redesign.

Adding export later is simply adding a scope + permission, reusing policies.

Naming & Modeling Conventions (copy/paste ready)

Resource Types

Singular domain nouns: account, transaction, customer

Instances identified via URI or ID attribute: account/{id}.

Scopes (verbs)

CRUD-y: view, create, update, delete

Domain: export, approve, refund, transfer.

Optional namespacing: account:view if you need to disambiguate externally.

Policies

Role.<name>: role-based (client roles for app-specific, realm roles for cross-app)

Group.<name>: group-based

Attr.<rule>: attribute/script-based (e.g., Attr.SameRegion)

Agg.<business_meaning>: aggregated (“OR of these”, “AND of those”)

Permissions

Perm.<Verb><ResourceType> for scope-based (e.g., Perm.ViewAccount)

Perm.<Rule><ResourceType> for resource-based constraints (e.g., Perm.OwnsAccount)

Decision Strategy

Start Unanimous on the resource server and most permissions.

Use Affirmative only when any single green light should allow access.

Migration Playbook for 250 Capabilities

Inventory & Normalize

Turn every legacy capability into {resource_type, verb}. viewAccount{account, view}; viewTransaction{transaction, view}.

De-duplicate Verbs

Aim for a small, reusable verb set per resource type.

Define Resource Types & Instances

Create Keycloak resource types for your nouns; add attributes you’ll need for policy (owner, region, org, sensitivity).

Create Scopes

Attach your verbs to each resource type.

Model Policies

Translate legacy groups/roles into Role- or Group-based policies.

Add attribute policies for ownership, region, etc.

Compose with Aggregated policies.

Create Permissions

One scope-based permission per {resource_type, verb} you actually grant in real life.

Add resource-based permissions for object-level rules (ownership, region).

Set Decision Strategy

Use Unanimous globally; override per-permission if necessary.

Test with Evaluate

In the client’s Authorization → Evaluate, simulate users, roles, groups, and resources to confirm outcomes.

Iterate Safely

Roll out per module; keep logs; add deny-by-default fallbacks.

Common Pitfalls & How to Avoid Them

Scope explosion: Don’t encode the resource in scope names (viewAccount). Reuse verbs; use resource types to scope meaning.

Permission soup: One permission per meaningful rule is enough. Use Aggregated policies to compose, not copy-paste.

Overusing direct user grants: Prefer roles/groups + policies. Keep identity hygiene clean.

Accidental Affirms: If you meant “must be owner and have the role,” Unanimous is your friend.

Forgetting attributes: Many real rules are attribute-driven (owner, region, cost center). Model those on the resource and user.

Mini Blueprint (your viewAccount case)

Resource type: account

Scope: view

Policy: Role.HelpDesk (or Group.HelpDesk) and/or Role.AccountOwner

Permission (scope-based): Perm.ViewAccount

Scopes: view on account

Apply Policy: Role.HelpDesk OR Role.AccountOwner

Decision Strategy: Unanimous (with any additional constraints like Attr.SameRegion)

That’s it — clean, readable, scalable.

Final Thought

Your old world of “capabilities by group” maps 1:1 to scope-based permissions by policy in Keycloak. The biggest win you’ll get from this migration isn’t just parity — it’s clarity: consistent verbs, explicit resource types, and modular policies that you can combine like LEGO. Once you adopt that shape, adding the 251st capability is a five-minute change, not a re-design.

Keycloak for Developer : https://tobiweissmann.gumroad.com/l/dixdgp


메타데이터
post_id
fd5a60f392bf
slug
resources-scopes-permissions-policies-in-keycloak-a-practical-guide-for-mapping-legacy-rbac-fd5a60f392bf
url
https://medium.com/@trivajay259/resources-scopes-permissions-policies-in-keycloak-a-practical-guide-for-mapping-legacy-rbac-fd5a60f392bf
canonical_url
https://medium.com/@trivajay259/resources-scopes-permissions-policies-in-keycloak-a-practical-guide-for-mapping-legacy-rbac-fd5a60f392bf
author_url
https://medium.com/@trivajay259
status
ok
fetched_at
2026-08-24 02:07:13