IB-SIA-13 Intermediate Est. 4 to 5 hours
Identity Bytes // Senior IAM Architect Track

RBAC to ABAC: From Roles to Attributes

Hit the role explosion wall that broke Northgate's access model after the Harborview acquisition, then rebuild the same decisions as attribute and context aware policies using Keycloak Authorization Services, and see the one decision roles cannot express at all.

01Lab Metadata

FieldValue
Lab IDIB-SIA-13
TrackIdentity Bytes, Senior IAM Architect Track
PhasePhase 3, Modern Authorization
DifficultyIntermediate
Estimated Time4 to 5 hours
Core TechnologiesRBAC (NIST model), ABAC (NIST SP 800-162), Keycloak Authorization Services, UMA 2.0 grant, Python 3.11, curl, jq
Builds OnLab 01 (users and group attributes in OpenLDAP), Lab 02 (realm roles and client configuration), Lab 09 (reading the permission claims in the returned token)
Feeds IntoLab 14 (OPA/Rego, externalising the policy decision), Lab 15 (ReBAC/OpenFGA), Lab 16 (externalised authorization at the edge)

02Lab Title and Description

RBAC to ABAC: From Roles to Attributes

For most of its history Northgate Financial has controlled access with roles. A user is a member of finance-team or it-admins, a role grants a fixed set of permissions, and access is decided by asking a single question: does this user have this role? Role-Based Access Control is simple, auditable and widely understood, and for a stable organisation it works well. Then Northgate acquired Harborview Wealth, and the model began to strain. Harborview had its own departments, its own seniority tiers, and rules that depend on things a role cannot capture: a wealth adviser may view a client portfolio only if it belongs to their own book, only during market hours, and only up to their approval limit. Expressing that with roles alone means inventing a new role for every combination, and the number of roles began to grow faster than anyone could govern.

This is role explosion, and it is the classic signal that an organisation has outgrown pure RBAC. The answer is Attribute-Based Access Control (ABAC), described in NIST SP 800-162. Instead of asking only "what role does the user have", ABAC evaluates a policy over attributes of the subject (department, clearance, book of business), the resource (owner, classification, value), the action, and the environment (time of day, network, risk score from Lab 08). One policy can then replace dozens of narrow roles.

In this lab you first model Northgate's access in RBAC and watch the role count balloon as you try to honour the Harborview rules. Then you rebuild the same decisions using Keycloak Authorization Services, defining resources with attributes, and policies over user attributes and time, combined with aggregate logic. Finally you evaluate authorization decisions for two users who hold the same role but get different answers, because their attributes and context differ, the decision RBAC cannot express. You will also see clearly where Keycloak's built-in policies stop and why a dedicated policy engine, the subject of Lab 14, is the next step.

Estimated completion time: 4 to 5 hours, including RBAC modelling, Authorization Services configuration, and decision testing.

03Prerequisites

Completed Prior Labs

LabWhy it is required
IB-SIA-01, OpenLDAP directoryProvides the users and the group and attribute structure. This lab adds attributes (department, clearance) that ABAC policies evaluate.
IB-SIA-02, Keycloak realm and OIDC federationProvides the northgate realm and the realm roles used in the RBAC phase, and the client that becomes an Authorization Services resource server.
IB-SIA-09, JWT, JWS and JWE Deep DiveYou decode the Requesting Party Token returned by the authorization decision to read the granted permissions. The token-reading skills from Lab 09 are used directly.

System Requirements

ResourceMinimum
OSUbuntu 22.04 LTS, macOS 13+, or Windows 11 with WSL2
RAM7 GB free (the Keycloak HA cluster from Lab 06 accounts for most of this)
Disk2 GB free
CPU2 cores sufficient
NetworkAccess to the running Keycloak cluster on ib-lab-net

Required Tools

ToolExact Version
Docker Engine25.0 or later
Python3.11.x (for decoding the decision token)
curl8.x
jq1.7
Install and verify: Ubuntu/Debian
sudo apt update
sudo apt install -y python3.11 jq curl

mkdir -p ~/ib-labs/ib-abac
cd ~/ib-labs/ib-abac

python3.11 --version   # Expect: Python 3.11.x
jq --version            # Expect: jq-1.7 or later
docker --version        # Expect: Docker version 25.x or later
Install and verify: macOS
brew install python@3.11 jq
mkdir -p ~/ib-labs/ib-abac && cd ~/ib-labs/ib-abac
python3.11 --version
jq --version
Install and verify: Windows 11 (WSL2)
# Run inside your WSL2 Ubuntu distribution, not PowerShell
wsl --install -d Ubuntu-22.04
# Then follow the Ubuntu/Debian instructions above.
INFO RBAC and ABAC are not rivals so much as points on a spectrum. NIST SP 800-162 describes ABAC and explicitly notes that roles can be modelled as one attribute among many. A mature deployment is often hybrid: roles for coarse, stable grants, and attributes for the fine, contextual decisions roles handle poorly. This lab teaches the transition, not a wholesale replacement.

04Real World Problem Statement

Pure role-based access forces every access rule to be expressed as membership in a group. When rules depend on context, ownership or thresholds, the only way to encode them in roles is to multiply roles until the model becomes ungovernable. ABAC replaces that combinatorial growth with policies that read attributes directly.

Risk

Role explosion produces roles no one fully understands, which leads to over-granting "to be safe" and to orphaned roles that are never revoked. Both are direct paths to excess privilege, the root cause behind a large share of insider and lateral-movement incidents.

Compliance

Access recertification, required under ISO 27001 and FCA expectations, becomes impractical when there are thousands of near-duplicate roles. Reviewers cannot meaningfully attest to grants they cannot understand, which turns recertification into a rubber stamp.

Productivity

Every new business rule under RBAC means a role design change, a provisioning change and a recertification change. Under ABAC, a rule such as "only during market hours" is one policy, changed in one place, without touching any user's role assignments.

Security Posture

ABAC lets Northgate express least privilege precisely: access limited to the exact combination of subject, resource and context that the business rule intends, rather than the nearest available role that happens to be broad enough.

Concrete scenario: After absorbing Harborview Wealth, Northgate needed to express: a wealth adviser may read a client portfolio only if the portfolio's owning desk matches the adviser's own desk, and only during UK market hours. In RBAC this became a proliferation of roles like wealth-adviser-desk-a-markethours, one per desk and context combination. In this lab you build that RBAC model, count the roles it demands, then replace it with a single attribute and time policy in Keycloak Authorization Services, and prove two advisers on different desks get different answers for the same portfolio.

05Skills Mapped to Production Solutions

Skill LearnedReal-World Enterprise Application
Recognising and quantifying role explosion in an RBAC modelDiagnosing when an organisation has outgrown pure RBAC, a common finding in IAM maturity assessments and access governance reviews
Modelling access as ABAC policies over subject, resource, action and environment attributesDesigning scalable authorization for organisations with contextual, ownership-based or threshold-based rules
Configuring Keycloak Authorization Services: resources, scopes, policies and permissionsImplementing fine-grained, centrally managed authorization on Keycloak-protected applications
Using the UMA 2.0 grant to request and read an authorization decisionBuilding resource servers that delegate the access decision to Keycloak rather than hard-coding it
Identifying the limits of built-in policy enginesMaking the architectural call on when a dedicated external policy engine (OPA, the subject of Lab 14) is warranted
Designing hybrid role-plus-attribute modelsPragmatic authorization architecture that keeps roles for coarse grants and attributes for contextual decisions

06Architecture Overview

RBAC: role explosion wealth-adviser-desk-a-markethours wealth-adviser-desk-b-markethours wealth-adviser-desk-a-afterhours wealth-adviser-desk-b-afterhours ... one role per combination ... desks x contexts x limits = N roles grows multiplicatively, ungovernable ABAC: one policy ALLOW read(portfolio) IF subject.desk == resource.desk AND env.time in market_hours AND subject.limit >= resource.value one rule covers every desk add a desk: no new policy needed grows additively, governable PEP resource server enforces decision (policy enforcement point) KEYCLOAK PDP Authorization Services resources, scopes, policies, permissions (policy decision point) PIP attribute sources user attrs, resource attrs, clock 1 ask (UMA) 2 read attrs 3 permit / deny PEP, PDP, PIP: the standard ABAC decision architecture (NIST SP 800-162)

Component Breakdown

ComponentPurposeTechnologyDeploymentPortsKey Configuration
Policy Decision Point (PDP)Evaluates policies over attributes and returns permit or denyKeycloak Authorization ServicesExisting HA cluster from Lab 068443Authorization enabled on a confidential client acting as the resource server
Policy Enforcement Point (PEP)Asks the PDP for a decision and enforces it on the protected resourceResource server (represented here by the UMA ticket flow via curl)Conceptual in this lab; a real PEP is application code or a gatewayN/ARequests decisions with the UMA 2.0 grant
Policy Information Point (PIP)Supplies the attributes policies read: user attributes, resource attributes, and the clockKeycloak user attributes, resource attributes, time policyAttributes stored on users and resources in KeycloakN/AUser attributes desk and approval_limit; resource attributes set on the portfolio resource
RBAC baselineThe role model built first to demonstrate role explosionKeycloak realm rolesSame realmN/ARoles created and counted in Phase 1

Data Flow

  1. The enforcement point receives a request to act on a resource and asks Keycloak for a decision using the UMA 2.0 ticket grant, presenting the user's access token.
    Why: delegating the decision to a central PDP means the rule lives in one governed place, not scattered through application code where it drifts out of sync.
  2. Keycloak gathers the relevant attributes, the user's attributes from the token and directory, the resource's attributes, and the current time.
    Why: an attribute decision is only as good as its inputs; the PDP must pull from authoritative attribute sources rather than trusting the caller to supply them.
  3. Keycloak evaluates the permission's policies, combining an attribute policy and a time policy with aggregate logic, and returns permit or deny.
    Why: combining policies lets one permission express a compound rule ("right desk AND market hours") that would need many roles to approximate.
  4. The enforcement point honours the decision. On permit it returns a Requesting Party Token listing the granted permissions; on deny it refuses the request.
    Why: the token gives the resource server a verifiable statement of exactly what was granted, which it can log for audit alongside the decision.

Security Considerations

ConcernLab Approach
Attribute integrityUser attributes are set through Keycloak and surfaced as verifiable token claims; the PDP does not trust attributes supplied by the requesting client.
Default denyPermissions default to deny unless a policy explicitly permits, so a gap in policy coverage fails safe.
Policy combinationAggregate policies use explicit AND or affirmative logic, so a compound rule cannot be satisfied by meeting only part of it.
Decision auditabilityEach decision can be logged with the attributes that drove it, giving a defensible audit trail, which matters more under ABAC where the rule is not visible in a simple role name.

07Step by Step Implementation

Phase 1: Model RBAC and Hit the Wall

Step 1.1: Build the naive RBAC role set for the Harborview rules

Purpose: encode the wealth adviser rules the only way RBAC allows, as combinatorial roles Context: this deliberately demonstrates the problem before solving it
Create roles for every desk and context combination
# The Harborview rule: an adviser may read portfolios for their own desk,
# and the permitted action differs in and out of market hours. RBAC forces
# one role per combination. With 4 desks and 2 time contexts that is 8 roles
# for this ONE rule; add an approval-limit tier (3 bands) and it is 24.
for desk in desk-a desk-b desk-c desk-d; do
  for ctx in markethours afterhours; do
    docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh create roles \
      -r northgate -s name="wealth-adviser-${desk}-${ctx}"
  done
done

# Count how many roles now exist that encode this single business rule.
docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh get roles -r northgate \
  --fields name | jq '[.[] | select(.name | startswith("wealth-adviser-"))] | length'
VERIFICATION The count query should return 8 for four desks and two contexts. Note that this is one business rule. Adding a third context, a new desk, or an approval-limit dimension multiplies the count. This multiplicative growth, not the absolute number, is role explosion.
SECURITY WARNING Every one of these roles must be assigned, governed and recertified. When a desk closes or an adviser moves, several roles must change in lockstep. Missing one leaves a stale grant. Role explosion is not only inconvenient; it directly produces the orphaned and excess grants that audits flag and attackers exploit.

Step 1.2: Set the attributes ABAC will use instead

Purpose: give users the attributes that will replace the combinatorial roles Context: one desk attribute and one limit attribute per user, versus many roles
Set desk and approval_limit user attributes
# Two advisers on different desks. In ABAC these two attributes, plus the
# resource's own desk, express what eight-plus roles tried to encode.
AID=$(docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh get users \
  -r northgate -q username=asmith --fields id | jq -r '.[0].id')
JID=$(docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh get users \
  -r northgate -q username=jpatel --fields id | jq -r '.[0].id')

docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh update users/$AID \
  -r northgate -s 'attributes.desk=["desk-a"]' \
  -s 'attributes.approval_limit=["500000"]'

docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh update users/$JID \
  -r northgate -s 'attributes.desk=["desk-b"]' \
  -s 'attributes.approval_limit=["100000"]'
Map the desk attribute into the token as a claim
# For a policy to read the desk, it must appear as a token claim. Add a
# user-attribute protocol mapper on a client scope (or the client) so that
# 'desk' is included in the access token.
docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh create \
  clients/RESOURCE_CLIENT_UUID/protocol-mappers/models -r northgate \
  -s name=desk-mapper \
  -s protocol=openid-connect \
  -s protocolMapper=oidc-usermodel-attribute-mapper \
  -s 'config."user.attribute"=desk' \
  -s 'config."claim.name"=desk' \
  -s 'config."access.token.claim"=true' \
  -s 'config."jsonType.label"=String'
VERIFICATION Obtain a token for asmith and decode it as in Lab 09; confirm it now contains a desk claim of desk-a. If the claim is absent, confirm the mapper was created on the correct client (the one you enable authorization on in Step 2.1) and that access.token.claim is true.

Phase 2: Rebuild as ABAC with Keycloak Authorization Services

Step 2.1: Enable Authorization Services on a resource-server client

Purpose: turn a confidential client into a policy decision point Context: this client hosts the resources, policies and permissions
Create the resource-server client
docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh create clients \
  -r northgate \
  -s clientId=portfolio-api \
  -s enabled=true \
  -s protocol=openid-connect \
  -s publicClient=false \
  -s serviceAccountsEnabled=true \
  -s authorizationServicesEnabled=true \
  -s 'redirectUris=["http://localhost:8082/*"]'

# Capture the client UUID (RESOURCE_CLIENT_UUID used elsewhere).
docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh get \
  clients?clientId=portfolio-api -r northgate --fields id | jq -r '.[0].id'
VERIFICATION Confirm authorization is on: docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh get clients/RESOURCE_CLIENT_UUID -r northgate | jq '.authorizationServicesEnabled' returns true. An Authorization tab now appears for this client in the Admin Console.

Step 2.2: Define the resource, scope and attribute policy

Purpose: represent a portfolio as a resource with a desk attribute, and a policy that matches it Context: the Admin Console is used for policy authoring because names and options are version-specific
Author the resource and policies (Admin Console)
# In the Admin Console, Clients, portfolio-api, Authorization tab:
#
# 1) Resources: create "portfolio-desk-a"
#      Type: urn:portfolio:resource:portfolio
#      Scopes: portfolio:read
#      Attributes: desk = desk-a   (the resource's owning desk)
#
# 2) Scopes: confirm "portfolio:read" exists.
#
# 3) Policies: create a Time policy "market-hours"
#      Not Before / Not On or After, or the hour range fields, set to
#      represent UK market hours (for example 08:00 to 16:30). The exact
#      field layout differs by version; the Console shows the current fields.
#
# 4) Policies: create a Regex policy "desk-matches-desk-a"
#      Target claim: desk
#      Pattern: ^desk-a$
#      This permits when the user's desk claim equals desk-a.
#
#      NOTE: a Regex policy compares a claim to a fixed pattern. Comparing
#      the SUBJECT's desk to the RESOURCE's desk dynamically (desk == desk)
#      is beyond built-in policies; that gap is addressed in Step 2.4 and is
#      exactly why Lab 14 introduces an external policy engine.
echo "Author resources and policies in the Authorization tab as described."
INFO Keycloak's built-in policy types include role, group, user, client, client-scope, time, regex and aggregate. Regex and time cover a large amount of ABAC. What they do not cover well is relating one attribute to another at decision time, for example "subject.desk equals resource.desk". That relational comparison is the natural boundary where a dedicated policy language such as Rego (Lab 14) earns its place.

Step 2.3: Combine the policies into a permission

Purpose: require both desk match and market hours for the read scope Context: aggregate logic expresses the compound rule as one permission
Create an aggregate policy and a scope permission (Admin Console)
# Still in the Authorization tab:
#
# 5) Policies: create an Aggregate policy "adviser-may-read-desk-a"
#      Apply Policy: desk-matches-desk-a AND market-hours
#      Decision strategy: Unanimous (all must permit)
#
# 6) Permissions: create a Scope permission "read-portfolio-desk-a"
#      Resource: portfolio-desk-a
#      Scope: portfolio:read
#      Apply Policy: adviser-may-read-desk-a
#      Decision strategy: Unanimous
echo "Combine policies into a unanimous scope permission as described."
VERIFICATION In the Authorization tab's Evaluate sub-tab, select user asmith and scope portfolio:read on resource portfolio-desk-a. During market hours the evaluation should return PERMIT; the same evaluation for jpatel (desk-b) should return DENY. The Evaluate tool is the fastest way to confirm policy logic before testing the live grant.

Step 2.4: Note the relational gap honestly

Purpose: mark precisely where built-in policies stop Context: sets up the motivation for Lab 14 without hand-waving
PRODUCTION CONSIDERATION The policy above hard-codes desk-a. To avoid a separate permission per desk, you want one rule that says "permit if the subject's desk equals the resource's desk", evaluated dynamically. Keycloak can express this with a JavaScript (script) policy, but script policies are disabled by default and gated behind a preview feature for a good security reason: uploaded scripts run inside the server. This tension, wanting relational attribute logic without running arbitrary code in the identity server, is precisely the architectural pressure that leads to an external, purpose-built policy engine. That is Lab 14.

Phase 3: Evaluate Live Decisions

Step 3.1: Request a decision with the UMA grant for the matching adviser

Purpose: get a real permit decision for asmith on desk-a during market hours Context: the UMA ticket grant is how a resource server asks Keycloak to decide
Obtain a token, then request an RPT for the resource and scope
# Get asmith's access token (direct grant for lab convenience).
ASMITH_TOKEN=$(curl -sk -X POST \
  "https://localhost:8443/realms/northgate/protocol/openid-connect/token" \
  -d "client_id=portfolio-api" -d "client_secret=REPLACE_WITH_PORTFOLIO_SECRET" \
  -d "grant_type=password" -d "username=asmith" \
  -d "password=REPLACE_WITH_ASMITH_PASSWORD" -d "scope=openid" | jq -r '.access_token')

# Ask Keycloak for a decision on portfolio:read for the desk-a resource.
curl -sk -X POST \
  "https://localhost:8443/realms/northgate/protocol/openid-connect/token" \
  -H "Authorization: Bearer $ASMITH_TOKEN" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
  -d "audience=portfolio-api" \
  -d "permission=portfolio-desk-a#portfolio:read" \
  -d "response_mode=decision" | jq
VERIFICATION During market hours the response should be {"result": true}, a permit for asmith on desk-a. If you receive false while confident the user and time are correct, use the Admin Console Evaluate tool to see which policy denied, then reconcile the live claim (is desk present in the token?) with the policy pattern.

Step 3.2: Request the same decision for the non-matching adviser

Purpose: show the same role, same action, different answer, driven by attributes Context: this is the decision RBAC cannot make without a distinct role
Repeat for jpatel on the desk-a resource
JPATEL_TOKEN=$(curl -sk -X POST \
  "https://localhost:8443/realms/northgate/protocol/openid-connect/token" \
  -d "client_id=portfolio-api" -d "client_secret=REPLACE_WITH_PORTFOLIO_SECRET" \
  -d "grant_type=password" -d "username=jpatel" \
  -d "password=REPLACE_WITH_JPATEL_PASSWORD" -d "scope=openid" | jq -r '.access_token')

curl -sk -X POST \
  "https://localhost:8443/realms/northgate/protocol/openid-connect/token" \
  -H "Authorization: Bearer $JPATEL_TOKEN" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
  -d "audience=portfolio-api" \
  -d "permission=portfolio-desk-a#portfolio:read" \
  -d "response_mode=decision" | jq
VERIFICATION The response for jpatel should be {"result": false}. Two advisers, the same requested action on the same resource, and a different decision, entirely because jpatel is on desk-b. No role encoded this; the desk attribute did. This is the ABAC capability that role membership alone cannot provide.

Step 3.3: Show the environment attribute at work

Purpose: confirm the same matching adviser is denied outside market hours Context: demonstrates the environment dimension of ABAC
Evaluate outside the time window
# If it is currently outside your configured market-hours window, re-run the
# asmith decision from Step 3.1 and observe it now returns false. If it is
# inside the window, temporarily narrow the market-hours time policy in the
# Admin Console to a range that excludes the current time, re-run, then
# restore the correct window.
#
# Alternatively use the Evaluate tool and set a custom evaluation time.
echo "Re-run the asmith decision outside market hours; expect result: false."
VERIFICATION Outside market hours, asmith, who was permitted in Step 3.1, is now denied for the same resource and action. The only thing that changed is the environment (the time), demonstrating that ABAC decisions incorporate context that no static role assignment can represent.
What just happened? You watched RBAC break and then rebuilt the same access rule in a way that scales. First you encoded one Harborview rule as roles and saw the count multiply with every desk and context, the textbook role explosion. Then you expressed the same rule as attributes: a desk on the user, a desk on the resource, a time window on the environment, combined into one permission. The proof was in three decisions: asmith permitted on her own desk during market hours, jpatel denied on a desk that is not his despite holding the same job, and asmith herself denied once the clock moved outside market hours. Same roles, different answers, driven by attributes and context. You also marked, without hand-waving, the exact boundary where Keycloak's built-in policies stop: relating one attribute to another at decision time, which is where Lab 14's dedicated policy engine begins.

08Testing and Validation

End-to-End Test Scenarios

ScenarioStepsExpected Result
Role explosion countCreate the desk and context roles in Step 1.1 and count themEight roles for one rule; count grows multiplicatively with each new dimension
Matching adviser permitRequest a decision for asmith on desk-a in market hours{"result": true}
Non-matching adviser denyRequest the same decision for jpatel{"result": false}
Environment denyRequest asmith's decision outside market hours{"result": false}

Negative Tests

TestExpected Result
Remove the desk claim mapper, then request asmith's decisionDeny, because the regex policy cannot match an absent claim; restore the mapper afterwards
Request a scope not covered by any permissionDeny by default, demonstrating default-deny behaviour
Set the aggregate decision strategy to Affirmative instead of Unanimous and re-test jpatelBehaviour changes: confirm you understand why, then restore Unanimous, since Affirmative would permit on desk match OR time rather than both
Supply a desk claim value not matching the pattern (for example desk-z)Deny, confirming the regex anchors match exactly

Common Failure Modes

SymptomLikely CauseResolution
Every decision returns falseThe desk claim is not in the token, so the regex policy never matchesConfirm the protocol mapper from Step 1.2 is on the portfolio-api client and included in the access token
Every decision returns true regardless of deskThe permission or aggregate uses Affirmative logic, or the desk policy is not attachedSet decision strategies to Unanimous and confirm both policies are applied to the permission
Time policy never deniesThe time window spans the whole day, or the server timezone differs from your expectationCheck the server's timezone and narrow the window; verify with the Evaluate tool using a custom time
UMA request returns an error rather than a decisionWrong resource#scope string, or authorization not enabled on the audience clientConfirm the permission parameter matches the exact resource and scope names, and that portfolio-api has authorization enabled

09Security Analysis

What Makes This Implementation Secure

What Is Intentionally Simplified for the Lab

Production Hardening Recommendations

AreaRecommendation
Attribute governanceTreat the attributes that drive decisions as security-relevant data: control who can set desk or approval_limit, and audit changes to them as closely as role assignments
Policy testingBuild an automated suite of permit and deny cases and run it on every policy change, since an ABAC rule is harder to eyeball than a role name
Decision loggingLog each decision with the attributes that drove it, so a permit or deny can be explained after the fact during an audit or incident
Hybrid designKeep roles for coarse, stable grants and reserve attribute policies for the contextual decisions roles handle poorly, rather than converting everything to ABAC
Engine selectionWhen rules require relational attribute logic or portability across many services, evaluate a dedicated policy engine, the architectural decision Lab 14 informs

10Cleanup

Remove the demonstration roles created to show role explosion
# Delete the combinatorial roles; they existed only to illustrate the problem.
for desk in desk-a desk-b desk-c desk-d; do
  for ctx in markethours afterhours; do
    docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh delete \
      roles/wealth-adviser-${desk}-${ctx} -r northgate 2>/dev/null || true
  done
done
Optionally remove the portfolio-api client and its authorization config
# Keep portfolio-api if you intend to reuse it as the enforcement target in
# Lab 14 or Lab 16; otherwise remove it and its resources/policies in one go.
docker exec -it ib-keycloak-1 /opt/keycloak/bin/kcadm.sh delete \
  clients/RESOURCE_CLIENT_UUID -r northgate
VERIFICATION Confirm the wealth-adviser-* roles are gone with the count query from Step 1.1, which should now return 0. The user attributes on asmith and jpatel are harmless to leave in place and are reused conceptually in Lab 14. The Keycloak cluster is otherwise ready for the next lab.

11Recommended Learning Links

12Portfolio Publishing Guide

Sanitise Before Publishing

This lab handles a client secret and user tokens. Confirm none reach your repository.

Sanitisation checklist commands
grep -R "REPLACE_WITH" . --include="*.sh" --include="*.md" || echo "Clean"

cat >> .gitignore <<'EOF'
*.token
.env
EOF

git status

README for the Repository

README.md skeleton
# IB-SIA-13: RBAC to ABAC

Demonstrates role explosion in a pure RBAC model, then rebuilds the same
access rule as attribute and time based policies using Keycloak
Authorization Services, and evaluates live permit/deny decisions via the
UMA 2.0 grant.

## Stack
Keycloak 24.x (Authorization Services), Python 3.11, curl, jq

## What it demonstrates
- Role explosion: one rule, many combinatorial roles
- ABAC: one policy over subject, resource and environment attributes
- Same role, different decision, driven by attributes and context
- The boundary where built-in policies end and an external engine begins

## Part of the Identity Bytes Senior IAM Architect Track
Lab 13 of 36. See identity-bytes.com for the full curriculum.

Git Commands

Commit and push
git add README.md .gitignore *.sh
git commit -m "IB-SIA-13: RBAC to ABAC with Keycloak Authorization Services"
git push origin main

Track Index Line

Add the following line to your master portfolio index:

IB-SIA-13 | RBAC to ABAC | Intermediate | Role explosion, attribute policies, PEP/PDP/PIP, Keycloak Authorization Services

LinkedIn Draft

The moment you create a role called "adviser-desk-a-markethours-under500k", your access model has already lost.

Role-Based Access Control is clean until the business rules stop being about who someone is and start being about context: which desk owns this, what time is it, how much is at stake. Encode those with roles and you get role explosion, one new role for every combination, growing multiplicatively until no reviewer can honestly recertify what they are approving.

This week I took one real rule ("an adviser may read a portfolio for their own desk, during market hours") and built it both ways. In RBAC it became eight roles for four desks and two time windows, and that was before adding approval limits. In ABAC it became a single policy over three attributes: the user's desk, the resource's desk, and the clock.

The demonstration that makes it click: two advisers with the identical job title ask to read the same portfolio. One is permitted, one is denied, purely because of their desk attribute. Then the permitted one is denied an hour later, purely because the market closed. No role can express that. Attributes and context can.

I also drew the honest boundary. Built-in policy engines handle a lot of ABAC, but comparing one attribute to another at decision time is where they stop and a dedicated policy engine begins. That is the next thing I am building.

How many of your roles are really a context rule wearing a role's clothing?

Next: IB-SIA-14, OPA and Rego
Lab 14 externalises the decision into Open Policy Agent, writing the relational attribute logic in Rego that Keycloak's built-in policies could not express, and running it as a dedicated policy decision point.