A production-oriented Quarkus gRPC service implementing fine-grained document access control using both RBAC (Role-Based Access Control) and ABAC (Attribute-Based Access Control).
Most access control systems stop at roles. Roles tell you who a user is — but not whether this specific user can access this specific document right now.
This service layers ABAC on top of RBAC to answer both questions:
| Layer | Question answered | Example |
|---|---|---|
| RBAC | Does this user have the document:read role? |
ADMIN, EDITOR, VIEWER |
| ABAC | Does this user own this document, or belong to the org that does? | owner_id == user_id, doc.sensitivity <= user.clearance |
This separation keeps role definitions simple while enabling fine-grained, context-aware decisions at runtime — without redeployment.
Client
│
│ gRPC request (DocumentRequest)
▼
┌─────────────────────────────────┐
│ document-service │
│ │
│ ┌──────────────────────────┐ │
│ │ gRPC Endpoint │ │
│ │ (DocumentServiceImpl) │ │
│ └────────────┬─────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────┐ │
│ │ Auth interceptor │ │ ◄── JWT validation (Keycloak)
│ │ (token + claims check) │ │
│ └────────────┬─────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────┐ │
│ │ Policy Engine │ │
│ │ │ │
│ │ 1. RBAC check │ │ role extracted from JWT claims
│ │ 2. ABAC check │ │ doc attributes vs user attributes
│ └────────────┬─────────────┘ │
│ │ │
│ ALLOW / DENY │
│ │ │
│ ▼ │
│ ┌──────────────────────────┐ │
│ │ Document Repository │ │ ◄── PostgreSQL
│ └──────────────────────────┘ │
└─────────────────────────────────┘
Why ABAC on top of RBAC, not just RBAC?
Pure RBAC breaks when permissions depend on data attributes — e.g. "a user can only read documents they created" or "a document marked CONFIDENTIAL requires clearance level 2+". Encoding these as roles creates role explosion. ABAC evaluates these as policy rules against live attributes, keeping the role model flat.
How policies are evaluated
Each incoming request is evaluated in two phases:
- RBAC phase — the user's roles (from JWT
realm_access.roles) are checked against the required permission for the operation. - ABAC phase — if RBAC passes, document and user attributes are evaluated against a policy rule set. A request is allowed only if both phases pass.
JWT + Keycloak integration
Tokens are issued by Keycloak and validated on every request via Quarkus's built-in OIDC support. User roles and attributes are extracted from JWT claims — no separate user lookup on the hot path.
| Concern | Technology |
|---|---|
| Runtime | Quarkus (JVM + native-ready) |
| Transport | gRPC (Protocol Buffers) |
| Auth | Keycloak (OIDC / JWT) |
| Database | PostgreSQL |
| Build | Maven |
- Java 17+
- Docker + Docker Compose
- Maven (or use
./mvnw)
# Starts PostgreSQL + Keycloak
docker-compose up -dNote: A
docker-compose.ymlis in progress. Until then, start PostgreSQL and Keycloak manually and configureapplication.propertiesaccordingly.
./mvnw quarkus:devDev UI available at: http://localhost:8080/q/dev/
./mvnw test./mvnw package
java -jar target/quarkus-app/quarkus-run.jar./mvnw package -Dquarkus.package.jar.type=uber-jar
java -jar target/*-runner.jar./mvnw package -Dnative
./target/document-service-1.0-SNAPSHOT-runner./mvnw package -Dnative -Dquarkus.native.container-build=trueKey properties in src/main/resources/application.properties:
# Keycloak OIDC
quarkus.oidc.auth-server-url=http://localhost:8180/realms/your-realm
quarkus.oidc.client-id=document-service
# Database
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=postgres
quarkus.datasource.password=postgres
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/documentdb-
docker-compose.ymlwith Keycloak + PostgreSQL - Policy rule DSL for ABAC definitions
- Audit log for access decisions
- gRPC reflection support for dev tooling