RBAC — Role-Based Access Control¶
AKKO implements a 4-layer security model that enforces access control from authentication through query execution:
Keycloak (authentication) → OPA (authorization) → Trino (query enforcement) → Superset (dashboard filtering)
Every user authenticates via Keycloak SSO, receives role claims in their JWT token, and those roles are enforced by OPA policies in Trino, role mappings in Superset/Airflow/Dashboards, and network-level gating via OAuth2-Proxy for services without native OIDC.
Kubernetes-first
All RBAC configuration is deployed via Helm. Keycloak realm is imported from realm-akko-k3d.json, OPA policies from a ConfigMap, and Trino rules from mounted files. No manual steps required after helm install.
Roles¶
AKKO defines five platform-wide realm roles in Keycloak:
| Role | Description | Persona | Access Level |
|---|---|---|---|
akko-admin |
Platform administrator | Alice Chen -- SRE / platform owner | Full access to all services, admin consoles, DDL/DML, no masking |
akko-engineer |
Data engineer | Bob Martin -- builds pipelines | CREATE/INSERT/SELECT on Iceberg, pipeline management, no PII masking |
akko-analyst |
Senior data analyst | Carol Santos -- explores data | SELECT-only, full PII visibility, dashboards, notebooks |
akko-user |
Standard user / compliance | Eve Dupont -- audits data | SELECT-only, PII columns masked (email, phone, SSN, date of birth) |
akko-viewer |
Executive / dashboard viewer | Dave Kim -- views dashboards | SELECT on limited schemas, row-filtered, PII masked |
Default role
New users are automatically assigned akko-analyst via the default-roles-akko composite role. This includes offline_access and uma_authorization.
Access Matrix¶
Service-Level Access¶
| Service | akko-admin | akko-engineer | akko-analyst | akko-user | akko-viewer |
|---|---|---|---|---|---|
| Keycloak (admin console) | Full | -- | -- | -- | -- |
| Trino (SQL) | Full DDL/DML | DDL/DML on designated schemas | SELECT only | SELECT, PII masked | SELECT, filtered + masked |
| Superset | Admin role | Alpha role | Gamma role | Gamma role | Public role |
| Airflow | Admin | User | Viewer | Viewer | Viewer |
| Dashboards | Admin | Editor | Viewer | Viewer | Viewer |
| JupyterHub | Admin panel + spawn | Spawn | Spawn | Spawn | Spawn |
| MLflow | Full | Full | Full | Full | Full |
| LiteLLM | Full (master key) | Full | Full | Full | Full |
| object storage | Root | Root | Root | Root | Root |
| OpenMetadata | Admin | Browse + edit | Browse + edit | Browse | Browse |
Services without fine-grained RBAC
MLflow, LiteLLM, object storage, and Spark currently use shared credentials. Access is gated at the network level (OAuth2-Proxy for MLflow, master key for LiteLLM, root credentials for object storage). Per-user RBAC for these services is planned.
Trino Catalog Access¶
| Role | Iceberg | PostgreSQL | System |
|---|---|---|---|
| akko-admin | all (owner) | all (owner) | all |
| akko-engineer | all (DDL on raw, staging, analytics, sandbox) |
all (DDL) | read-only |
| akko-analyst | read-only (all schemas) | read-only | read-only |
| akko-user | read-only (all schemas, PII masked) | read-only | read-only |
| akko-viewer | read-only (analytics, reporting, public only) |
-- | read-only |
| (default) | -- | -- | read-only |
Trino Table Privileges (Iceberg)¶
| Role | Schemas | Privileges |
|---|---|---|
| akko-admin | all | SELECT, INSERT, DELETE, UPDATE, OWNERSHIP, GRANT_SELECT |
| akko-engineer | raw, staging, analytics, sandbox |
SELECT, INSERT, DELETE, UPDATE |
| akko-analyst | all | SELECT |
| akko-user | all | SELECT (PII masked) |
| akko-viewer | analytics, reporting, public |
SELECT (row-filtered, PII masked) |
Keycloak Configuration¶
Realm Structure¶
All AKKO identity configuration lives in a single Keycloak realm named akko, imported from realm-akko-k3d.json on first startup.
Key settings:
| Setting | Value |
|---|---|
| Realm name | akko |
| Login theme | akko (custom) |
| Access token lifespan | 300s (5 min) |
| SSO session max | 36000s (10 hours) |
| Registration | Disabled |
| Brute force protection | Enabled (5 failures, 5 min lockout) |
Role Definition¶
Roles are defined as realm roles (not client roles). Each role maps to a Keycloak group claim via the groups protocol mapper, which emits role names into the JWT groups array.
The groups mapper is configured in the microprofile-jwt and service-specific client scopes:
{
"name": "groups",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"config": {
"multivalued": "true",
"claim.name": "groups",
"id.token.claim": "true",
"access.token.claim": "true"
}
}
This ensures that when a user logs in, their roles appear in the JWT token as groups, which OPA, Trino, and other services use for authorization decisions.
OAuth2 Clients¶
13 OAuth2 clients are pre-configured in the realm:
| Client | Type | Maps Roles |
|---|---|---|
jupyterhub |
Confidential | Yes (admin flag) |
superset |
Confidential | Yes (Admin/Alpha/Gamma/Public) |
airflow |
Confidential | Yes (Admin/User/Viewer) |
grafana |
Confidential | Yes (Admin/Editor/Viewer) |
trino |
Confidential | Yes (via OPA groups) |
openmetadata |
Confidential | Partial |
polaris |
Confidential | No |
oauth2-proxy |
Confidential | No (network gate) |
minio |
Confidential | No |
account |
Public | No (Keycloak internal) |
account-console |
Public (PKCE) | No (Keycloak internal) |
admin-cli |
Public | No (Keycloak internal) |
broker |
Confidential | No (Keycloak internal) |
realm-management |
Confidential | No (Keycloak internal) |
security-admin-console |
Public | No (Keycloak internal) |
No cockpit client
The AKKO cockpit uses the Keycloak JavaScript adapter with the account or account-console public clients for session detection. It does not have its own dedicated OAuth2 client in the realm.
Assigning a User to a Role¶
- Open the Keycloak admin console:
https://identity.akko.local/admin/ - Select the akko realm
- Navigate to Users and select the user (or create a new one)
- Go to the Role mapping tab
- Click Assign role and filter by realm roles
- Select the desired role (
akko-admin,akko-engineer,akko-analyst,akko-user, orakko-viewer)
emailVerified must be true
All users must have emailVerified=true. The oauth2-proxy service rejects users without a verified email, causing login failures for JupyterHub and other proxied services.
OPA Policies¶
OPA (Open Policy Agent) provides policy-as-code authorization for Trino. Policies are written in Rego and deployed as a ConfigMap.
Architecture¶
Trino delegates three types of decisions to OPA:
- Authorization -- can this user execute this operation?
- Row filtering -- which rows should this user see?
- Column masking -- should sensitive columns be redacted?
Trino-OPA Integration¶
Trino is configured to call OPA for every query:
access-control.name=opa
opa.policy.uri=http://akko-akko-opa:8181/v1/data/trino/allow
opa.policy.row-filters-uri=http://akko-akko-opa:8181/v1/data/trino/rowFilters
opa.policy.column-masking-uri=http://akko-akko-opa:8181/v1/data/trino/columnMask
Access Control Policy¶
The main policy (trino_access.rego) maps operations to roles. This is the actual policy deployed via ConfigMap at helm/akko/charts/akko-opa/templates/configmap.yaml:
package trino
import rego.v1
default allow := false
# Admins can do everything
allow if {
"akko-admin" in input.context.identity.groups
}
# Engineers: SELECT, INSERT, CREATE TABLE, DROP TABLE, ALTER
allow if {
"akko-engineer" in input.context.identity.groups
input.action.operation in [
"ExecuteQuery", "AccessCatalog", "FilterCatalogs",
"FilterSchemas", "FilterTables", "FilterColumns",
"SelectFromColumns", "ShowSchemas", "ShowTables",
"ShowColumns", "ShowCreateTable", "ShowFunctions",
"ShowStats", "CreateTable", "InsertIntoTable",
"AddColumn", "DropTable", "RenameTable"
]
}
# Analysts (senior): SELECT only, full data visibility (no masking)
allow if {
"akko-analyst" in input.context.identity.groups
input.action.operation in [
"ExecuteQuery", "AccessCatalog", "FilterCatalogs",
"FilterSchemas", "FilterTables", "FilterColumns",
"SelectFromColumns", "ShowSchemas", "ShowTables",
"ShowColumns", "ShowCreateTable", "ShowFunctions",
"ShowStats"
]
}
# Standard users (compliance): SELECT only, PII masked (see column_masking.rego)
allow if {
"akko-user" in input.context.identity.groups
input.action.operation in [
"ExecuteQuery", "AccessCatalog", "FilterCatalogs",
"FilterSchemas", "FilterTables", "FilterColumns",
"SelectFromColumns", "ShowSchemas", "ShowTables",
"ShowColumns", "ShowCreateTable", "ShowFunctions",
"ShowStats"
]
}
# Viewers (executives): SELECT + metadata, row-filtered + PII masked
allow if {
"akko-viewer" in input.context.identity.groups
input.action.operation in [
"ExecuteQuery", "AccessCatalog", "FilterCatalogs",
"FilterSchemas", "FilterTables", "FilterColumns",
"SelectFromColumns", "ShowSchemas", "ShowTables",
"ShowColumns", "ShowCreateTable", "ShowFunctions",
"ShowStats"
]
}
# Row-filter identity: Trino re-checks permissions using the filter identity
allow if {
input.context.identity.user == "viewer_active_only"
}
Column Masking Policy¶
PII columns are masked for akko-user and akko-viewer roles. Admins, engineers, and analysts see data in clear:
# Text PII columns: email, phone, ssn, medical_record_number
columnMask := {"expression": expression, "identity": identity} if {
input.action.resource.column.columnName in [
"email", "phone", "ssn", "medical_record_number"
]
not "akko-admin" in input.context.identity.groups
not "akko-engineer" in input.context.identity.groups
not "akko-analyst" in input.context.identity.groups
expression := "'***MASKED***'"
identity := "mask_pii"
}
# Date PII columns: date_of_birth (type-safe NULL)
columnMask := {"expression": expression, "identity": identity} if {
input.action.resource.column.columnName in ["date_of_birth"]
not "akko-admin" in input.context.identity.groups
not "akko-engineer" in input.context.identity.groups
not "akko-analyst" in input.context.identity.groups
expression := "CAST(NULL AS DATE)"
identity := "mask_pii"
}
Masking summary:
| Column | admin | engineer | analyst | user | viewer |
|---|---|---|---|---|---|
email |
clear | clear | clear | ***MASKED*** |
***MASKED*** |
phone |
clear | clear | clear | ***MASKED*** |
***MASKED*** |
ssn |
clear | clear | clear | ***MASKED*** |
***MASKED*** |
medical_record_number |
clear | clear | clear | ***MASKED*** |
***MASKED*** |
date_of_birth |
clear | clear | clear | NULL |
NULL |
Row-Level Filtering Policy¶
Row filters restrict which rows a role can see:
# Viewers can only see active accounts
rowFilters contains {"expression": expression, "identity": identity} if {
"akko-viewer" in input.context.identity.groups
input.action.resource.table.tableName == "accounts"
expression := "status = 'active'"
identity := "viewer_active_only"
}
Currently, only the accounts table has a row filter for akko-viewer. Additional filters can be added per table as needed.
Adding a New OPA Policy¶
OPA policies are defined inline in the ConfigMap template at helm/akko/charts/akko-opa/templates/configmap.yaml. The ConfigMap contains three Rego files:
| File | Purpose |
|---|---|
trino_access.rego |
Operation-level allow/deny rules per role |
column_masking.rego |
PII column masking rules |
row_filter.rego |
Row-level security filters |
Step-by-step: Add a new access rule
- Open
helm/akko/charts/akko-opa/templates/configmap.yaml -
Add your Rego rule in the appropriate
.regosection. For example, to add row filtering on thetransactionstable for viewers: -
If the row filter uses a custom identity (like
viewer_small_txn_only), add a corresponding allow rule intrino_access.rego: -
Deploy the updated policy:
-
OPA hot-reloads policies from the ConfigMap -- no pod restart is needed. Verify the policy is loaded:
# Check OPA has the updated policy kubectl exec -n akko deploy/akko-akko-opa -- \ curl -s http://localhost:8181/v1/policies | python3 -m json.tool # Test the policy with a sample input kubectl exec -n akko deploy/akko-akko-opa -- \ curl -s -X POST http://localhost:8181/v1/data/trino/allow \ -d '{"input":{"context":{"identity":{"user":"dave","groups":["akko-viewer"]}},"action":{"operation":"SelectFromColumns","resource":{"table":{"catalogName":"iceberg","schemaName":"banking","tableName":"transactions"}}}}}' \ | python3 -m json.tool
Testing policies locally
Before deploying, test Rego syntax locally:
Trino Enforcement¶
In addition to OPA, Trino uses file-based access control for catalog/schema/table permissions. Both systems work together:
- OPA handles operation-level authorization (allow/deny), column masking, and row filtering
- File-based rules (
rules.json) enforce catalog/schema/table-level access with fine-grained privileges
Group Mappings¶
Trino maps users to groups via group.txt:
akko-admin:admin,alice,trino,airflow
akko-engineer:bob
akko-analyst:carol
akko-user:eve
akko-viewer:dave
Service users
The trino and airflow users must be in the akko-admin group. Trino uses trino for internal operations, and Airflow connects as airflow to run pipeline queries.
Standard user group
The akko-user group maps to Eve (compliance / standard user). When OPA evaluates column masking rules, it checks the groups claim in the JWT -- the Trino group.txt file is used for file-based access control rules, not OPA decisions.
Rules Evaluation¶
Rules in rules.json are evaluated top-to-bottom, first match wins. The file has three sections: catalogs, schemas, and tables. A catch-all deny rule at the end blocks unmatched access:
Refresh Period¶
Both rules.json and group.txt are refreshed every 5 minutes. Changes take effect without a Trino restart.
Superset Row-Level Security¶
Superset maps Keycloak roles to its internal roles:
| Keycloak Role | Superset Role | Capabilities |
|---|---|---|
akko-admin |
Admin | All dashboards, SQL Lab, datasets, admin settings |
akko-engineer |
Alpha | Create dashboards, SQL Lab, datasets |
akko-analyst |
Gamma | View dashboards, SQL Lab, view datasets |
akko-user |
Gamma | View dashboards, SQL Lab, view datasets |
akko-viewer |
Public | View public dashboards only, no SQL Lab |
Superset connects to Trino using the trino service user (which has akko-admin privileges). This means that OPA column masking and row filtering are not applied at the Superset-to-Trino level. Instead, Superset enforces access via its own role system:
- Dashboard visibility -- Public role can only see dashboards explicitly marked as published
- SQL Lab -- only Admin, Alpha, and Gamma roles have SQL Lab access
- Dataset access -- datasets are gated by Superset role permissions
End-to-end enforcement
For full per-user data filtering through Superset, the recommended approach is to configure Superset to pass the actual user identity to Trino (instead of the shared trino user), so OPA policies apply to dashboard queries. This is planned for a future release.
Managing Users¶
Adding a New User (Web UI)¶
- Open the Keycloak admin console at
https://identity.akko.local/admin/ - Log in with the admin credentials (see
values-dev.yamlfor dev password, orglobal.auth.keycloakAdminPasswordfor production) - Select the akko realm (top-left dropdown)
- Navigate to Users > Add user
- Fill in the required fields:
- Username (required, lowercase, no spaces)
- Email (required -- OAuth2-Proxy rejects users without email)
- First name, Last name
- Toggle Email verified to ON
- Click Create
- Go to the Credentials tab:
- Click Set password
- Enter the password and confirm
- Toggle Temporary to OFF for service accounts or test users
- Click Save
- Go to the Role mapping tab:
- Click Assign role
- In the filter dropdown, select Filter by realm roles
- Select the desired role (
akko-admin,akko-engineer,akko-analyst,akko-user, orakko-viewer) - Click Assign
Adding a New User (Keycloak Admin API)¶
For automation or CI/CD pipelines, use the Keycloak Admin REST API:
# 1. Get an admin token
ADMIN_TOKEN=$(kubectl exec -n akko deploy/akko-keycloak -- \
curl -s -X POST http://localhost:8080/realms/master/protocol/openid-connect/token \
-d "client_id=admin-cli&grant_type=password&username=admin&password=<admin-password>" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
# 2. Create the user
kubectl exec -n akko deploy/akko-keycloak -- \
curl -s -X POST http://localhost:8080/admin/realms/akko/users \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "newuser",
"email": "newuser@company.com",
"firstName": "New",
"lastName": "User",
"enabled": true,
"emailVerified": true,
"credentials": [{"type": "password", "value": "strong-password", "temporary": false}]
}'
# 3. Get the user ID
USER_ID=$(kubectl exec -n akko deploy/akko-keycloak -- \
curl -s http://localhost:8080/admin/realms/akko/users?username=newuser \
-H "Authorization: Bearer $ADMIN_TOKEN" \
| python3 -c "import sys,json; print(json.load(sys.stdin)[0]['id'])")
# 4. Get the role ID (e.g., akko-analyst)
ROLE_ID=$(kubectl exec -n akko deploy/akko-keycloak -- \
curl -s http://localhost:8080/admin/realms/akko/roles/akko-analyst \
-H "Authorization: Bearer $ADMIN_TOKEN" \
| python3 -c "import sys,json; r=json.load(sys.stdin); print(r['id'])")
# 5. Assign the role
kubectl exec -n akko deploy/akko-keycloak -- \
curl -s -X POST \
"http://localhost:8080/admin/realms/akko/users/$USER_ID/role-mappings/realm" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "[{\"id\": \"$ROLE_ID\", \"name\": \"akko-analyst\"}]"
Port-forward alternative
If you prefer to run commands from your local machine instead of kubectl exec, port-forward the Keycloak pod first:
Verifying Access¶
After assigning a role, verify the user's token contains the correct claims:
# Get a token for the user (using the JupyterHub client, which has the groups mapper)
TOKEN=$(kubectl exec -n akko deploy/akko-keycloak -- \
curl -s -X POST http://localhost:8080/realms/akko/protocol/openid-connect/token \
-d "grant_type=password&client_id=jupyterhub&client_secret=akko-dev-jupyterhub-oidc&username=alice&password=alice123&scope=openid" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
# Decode the JWT and check the groups claim
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool
# Expected output includes: "groups": ["akko-admin", "default-roles-akko", ...]
What to check in the JWT
- The
groupsarray must contain the assigned role name (e.g.,akko-admin) - The
email_verifiedfield must betrue - The
preferred_usernamemust match the username - The
iss(issuer) must behttps://identity.akko.local/realms/akko
Revoking Access¶
Remove a role from a user (Web UI)¶
- Open
https://identity.akko.local/admin/and select the akko realm - Navigate to Users and select the user
- Go to the Role mapping tab
- Find the role to remove and click the X (unassign) button next to it
- The user must log out and log back in for the change to take effect (JWT tokens are valid until they expire -- 5 minutes by default)
Remove a role from a user (Admin API)¶
# Remove akko-engineer role from a user
kubectl exec -n akko deploy/akko-keycloak -- \
curl -s -X DELETE \
"http://localhost:8080/admin/realms/akko/users/$USER_ID/role-mappings/realm" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "[{\"id\": \"$ROLE_ID\", \"name\": \"akko-engineer\"}]"
Disable a user entirely¶
- Web UI: Navigate to the user, toggle Enabled to OFF, click Save
- Admin API:
Force logout (invalidate active sessions)¶
# Logout all sessions for a specific user
kubectl exec -n akko deploy/akko-keycloak -- \
curl -s -X POST \
"http://localhost:8080/admin/realms/akko/users/$USER_ID/logout" \
-H "Authorization: Bearer $ADMIN_TOKEN"
Token expiry delay
Revoking a role or disabling a user does not immediately block access. Existing JWT tokens remain valid until they expire (default: 5 minutes). Services that cache tokens (Superset, Dashboards) may require the user to log out and back in.
Adding a User to Trino File-Based Groups¶
If Trino uses file-based access control in addition to OPA, new users must be added to group.txt:
- Edit the Trino group file in the Helm chart (the exact location depends on your chart structure)
- Add the username to the appropriate group line:
- Upgrade the Helm release:
- Wait up to 5 minutes for Trino to reload the file (or restart the Trino coordinator pod)
Default Test Users¶
| Username | Role | Password | Full Name | |
|---|---|---|---|---|
alice |
akko-admin |
alice123 |
alice@akko.local | Alice Chen |
bob |
akko-engineer |
bob123 |
bob@akko.local | Bob Martin |
carol |
akko-analyst |
carol123 |
carol@akko.local | Carol Santos |
eve |
akko-user |
eve123 |
eve@akko.local | Eve Dupont |
dave |
akko-viewer |
dave123 |
dave@akko.local | Dave Kim |
All test users have emailVerified=true and enabled=true. Alice also has the realm-admin client role, giving her access to the Keycloak admin console.
Test passwords
In the development realm (realm-akko-k3d.json), passwords follow the pattern <username>123. Never use these credentials in production. Use strong passwords, external identity providers (LDAP, SAML), or Keycloak's password policies.
Troubleshooting¶
User Cannot Access a Service¶
- Check the JWT token -- verify the
groupsclaim contains the expected role - Check emailVerified -- must be
truefor OAuth2-Proxy-gated services - Check Keycloak session -- the user may have an old session with stale role claims. Log out and back in.
Wrong Permissions in Trino¶
- Check group.txt -- verify the user is in the correct Trino group
- Check OPA logs --
kubectl logs -n akko deploy/akko-akko-opato see policy evaluations - Check rules.json -- rules are first-match-wins; a rule higher in the list may be matching first
- Wait for refresh -- Trino refreshes group and rule files every 5 minutes
Column Masking Not Working¶
- Verify the column name matches one in the OPA policy (
email,phone,ssn,medical_record_number,date_of_birth) - Check the user's role -- only
akko-userandakko-viewersee masked values - Test via CLI -- use
curlto query Trino directly with the user's token to rule out Superset caching
Role Not Propagating to Superset/Airflow/Dashboards¶
These services map Keycloak roles to internal roles at login time. If a role changes:
- The user must log out of the service
- Clear the service's session (or use an incognito window)
- Log in again -- the new role will be mapped
Running RBAC Tests¶
# All RBAC integration tests
pytest tests/integration/ -m rbac -v
# Security tests (includes OPA policy enforcement)
pytest tests/integration/ -m security -v
# Specific service
pytest tests/integration/test_rbac_grafana.py -v
# Full test suite
bash tests/run-all.sh --fast
Architecture Decision Records¶
| ADR | Decision |
|---|---|
| Keycloak as IdP | Single source of truth for identity. All services authenticate via OIDC. |
| OPA for Trino ABAC | Policy-as-code enables row-level and column-level security without modifying Trino. |
| File-based Trino rules | Complements OPA for catalog/schema/table-level permissions. 5-minute refresh. |
| Realm roles (not client roles) | Simpler management. One role per user, visible across all clients via groups claim. |
| 5 roles | Covers admin, engineering, analysis, compliance, and executive personas. Extensible as needed. |