Role-Based Access Control
The previous two lessons both used the all-powerful root principal — exactly what you should not do outside a scratch environment. This lesson is what makes handing Polaris credentials to a real engine or a real teammate safe.
The chain, end to end
Create a Principal, create a Principal Role, assign the role to the principal; create a Catalog Role inside a specific catalog, assign that catalog role to the principal role; then grant a privilege (e.g. CATALOG_MANAGE_CONTENT, or narrower ones like TABLE_READ_DATA / TABLE_WRITE_DATA / NAMESPACE_CREATE) to the catalog role.
Privileges are additive and scoped to what they name — catalog-level (CATALOG_MANAGE_ACCESS, CATALOG_MANAGE_CONTENT, CATALOG_MANAGE_METADATA), table-level (TABLE_CREATE, TABLE_DROP, TABLE_READ_DATA, TABLE_WRITE_DATA), namespace-level (NAMESPACE_CREATE, NAMESPACE_LIST), and view-level privileges are all separate grants — granting one doesn't imply another.
403 Forbidden naming the operation it rejected, not a 404 pretending the catalog doesn't exist. This is server-side authorization checked on every REST call, which is why Lesson 5's Spark session, given a scoped-down credential, would be just as restricted as a raw curl call with the same token.
Exercise
ROOT_TOKEN="principal:root;password:<root-secret>;realm:default-realm;role:ALL"
curl -s -X POST "http://localhost:8181/api/management/v1/principals" -H "Authorization: Bearer $ROOT_TOKEN" \
-H 'Content-Type: application/json' -d '{"name": "course_engineer", "type": "user"}'
curl -s -X POST "http://localhost:8181/api/management/v1/principal-roles" -H "Authorization: Bearer $ROOT_TOKEN" \
-H 'Content-Type: application/json' -d '{"principalRole": {"name": "engineer_role"}}'
curl -s -X PUT "http://localhost:8181/api/management/v1/principals/course_engineer/principal-roles" -H "Authorization: Bearer $ROOT_TOKEN" \
-H 'Content-Type: application/json' -d '{"principalRole": {"name": "engineer_role"}}'
curl -s -X POST "http://localhost:8181/api/management/v1/catalogs/coursecatalog/catalog-roles" -H "Authorization: Bearer $ROOT_TOKEN" \
-H 'Content-Type: application/json' -d '{"catalogRole": {"name": "catalog_writer"}}'
curl -s -X PUT "http://localhost:8181/api/management/v1/principal-roles/engineer_role/catalog-roles/coursecatalog" -H "Authorization: Bearer $ROOT_TOKEN" \
-H 'Content-Type: application/json' -d '{"catalogRole": {"name": "catalog_writer"}}'
curl -s -X PUT "http://localhost:8181/api/management/v1/catalogs/coursecatalog/catalog-roles/catalog_writer/grants" -H "Authorization: Bearer $ROOT_TOKEN" \
-H 'Content-Type: application/json' -d '{"grant": {"type": "catalog", "privilege": "CATALOG_MANAGE_CONTENT"}}'
# Now prove the new principal actually works, scoped to what it was granted:
USER_TOKEN="principal:course_engineer;password:<its-secret>;realm:default-realm;role:ALL"
curl -s -i "http://localhost:8181/api/catalog/v1/coursecatalog/namespaces" -H "Authorization: Bearer $USER_TOKEN"
# ...and prove it's denied against a catalog it was never granted anything on:
curl -s -i "http://localhost:8181/api/catalog/v1/lockedcatalog/namespaces" -H "Authorization: Bearer $USER_TOKEN"
HTTP/1.1 200 OK
{"namespaces":[["course_db"]],"next-page-token":null}
HTTP/1.1 403 Forbidden
{"error":{"message":"Principal 'course_engineer' with activated PrincipalRoles '[]' and activated grants via '[engineer_role]' is not authorized for op LIST_NAMESPACES","type":"ForbiddenException","code":403}}
Retrieval check
You grant TABLE_READ_DATA to a Principal Role directly. What happens?
Correct. A Principal Role is purely a grouping label; the grant has no effect until a privilege is attached to a Catalog Role and that Catalog Role is assigned to the Principal Role.
Not quite. The API doesn't attach privileges to Principal Roles at all — the grant chain always routes through a Catalog Role.
A principal gets a 403 hitting a catalog. What should you check first?
Correct. Polaris's 403 message names the exact operation and the activated roles it checked — read it before guessing, since it's usually enough to spot the missing grant directly.
Not quite. A 403 means the catalog exists and was reached — it's an authorization failure, not a not-found. Read the message's operation and role details first.
Practice
- Reproduce both outcomes — one authorized call, one real
403— with your own principal and catalog names. - In one sentence, state which single object in the chain you'd delete to revoke
course_engineer's access without deleting the principal itself. - Replace
CATALOG_MANAGE_CONTENT(broad) with the narrowest privilege that would satisfy the same read-only exercise, and confirm it still works.
catalog_writer but leave the principal role and principal assignment intact, does course_engineer lose access immediately or only after its token expires?" Next lesson looks at where Polaris's table model stops assuming Iceberg: Generic Tables.