1. Einleitung

Die Endpunkte eines Webservers sollen häufig nur für ausgewählte Nutzer zugänglich sein. In diesem Tutorial werden zwei Methoden der Zugriffskontrolle für ein Quarkus Backend gezeigt: RBAC (Role Based Access Control) und PBAC (Policy Based Access Control).

1.1. RBAC

Bei der rollenbasierten Zugriffskontrolle werden die Zugriffsrechte eines Nutzers anhand seiner Rolle innerhalb der Organisation festgelegt.

1.1.1. Vorteile

  • Flexibilität: Rollen können schnell und einfach einem Nutzer zugeordnet und von einem Nutzer entfernt werden.

  • Verminderter administrativer Aufwand: Administratoren müssen Nutzern ihre Rechte nicht mehr einzeln zuordnen, sondern können Bündel an Rechten in Form von Rollen verteilen. rbac

1.1.2. Nachteile

  • Initialer Aufwand: Die Organisationsstruktur in entsprechende Rollen abzubilden ist mit großem initialen Aufwand verbunden.

  • Wartungsaufwand: Wenn sich die Rollen, die auf eine Ressource zugreifen können, ändern, muss der Quellcode der zugehörigen Applikation angepasst werden.

1.2. PBAC

Bei der policybasierten Zugriffskontrolle wird die Berechtigung für einen Zugriff anhand vordefinierter Richtlinien bestimmt. Es kann als eine Erweiterung der rollenbasierten Zugriffskontrolle gesehen werden, bei der die Zugriffskontrolle von der Applikation auf einen Policy Server (in diesem Fall Keycloak) verlagert wird.

1.2.1. Vorteile

  • Niedrigerer Wartungsaufwand: Bei Änderungen der Rollenrechte muss der Quellcode der Applikation nicht geändert werden.

  • Zentralisierte Verwaltung: PBAC ermöglicht eine zentrale Verwaltung von Zugriffsrechten.

1.2.2. Nachteile

  • Komplexität: Die Erstellung und Verwaltung von Richtlinien kann komplex sein.

  • Erschwerte Fehlersuche: Wenn Zugriffsprobleme auftreten, kann es schwierig sein, die Ursache zu identifizieren, da viele Richtlinien beteiligt sein können.

2. Keycloak development Server und Datenbank

Der Keycloak Server und die Postgres Datenbank werden für die Entwicklung mithilfe von Docker ausgeführt. Für die Orchestrierung der Container wird Docker Compose verwendet. In einem Ordner compose wird das dafür benötigte docker-compose.yaml abgelegt. Zusätzlich wird ein Shellscript zur Initialisierung der Datenbank erstellt.

2.1. Ordnerstruktur

Ordnerstruktur

2.2. Docker Compose

compose/docker-compose.yaml
services:
  db:
    container_name: postgres
    image: postgres:17.4-alpine3.21
    restart: unless-stopped
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_MULTIPLE_DATABASES: keycloak,db
    ports:
      - 5432:5432
    volumes:
      - ./db-postgres/db:/var/lib/postgresql/data
      - ./db-postgres/import:/import
      - ./pg-init-scripts:/docker-entrypoint-initdb.d
  keycloak:
    depends_on:
      - db
    container_name: keycloak
    image: quay.io/keycloak/keycloak:26.1
    restart: no
    environment:
      - KEYCLOAK_ADMIN=admin
      - KEYCLOAK_ADMIN_PASSWORD=password
      - KC_DB=postgres
      - KC_DB_URL=jdbc:postgresql://db:5432/keycloak
      - KC_DB_USERNAME=app
      - KC_DB_PASSWORD=app
    command:
      - start-dev
    ports:
      - 8000:8080

2.3. Postgres init script

compose/pg-init-scripts/pg-multiple-databases.sh
#!/bin/bash

set -e
set -u

function create_user_and_database() {
  local database=$1
  echo "  Creating user and database '$database'"
  psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
      CREATE USER $database;
      CREATE DATABASE $database;
      GRANT ALL PRIVILEGES ON DATABASE $database TO $database;
EOSQL
}

if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then
  echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES"
  for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do
    create_user_and_database $db
  done
  echo "Multiple databases created"
fi

2.4. Start

Um die Container zu starten, muss folgender Shellbefehl im compose Ordner ausgeführt werden.

docker compose up

2.5. Keycloak Konfiguration

Die Keycloak Adminoberfläche ist unter http://localhost:8000 erreichbar. Auf der Anmeldeseite muss sich mit User admin und dem Passwort password angemeldet werden.

keycloak admin login

2.5.1. Realm erstellen

Ein Realm ist ein eigener Bereich in dem Nutzer, Rollen und vieles mehr verwaltet werden. Realms sind voneinander abgekapselt. Für dieses Demoprojekt muss ein neuer Realm erstellt werden. Das Keycloak master Realm, dient ausschließlich zur Erstellung anderer Realms und sollte nicht für andere Zwecke verwendet werden.

Für die Erstellung eines neuen Realms muss auf das Dropdown links oben geklickt werden.

realm create button
Figure 1. Realm erstellen Knopf

Anschließend wird der Name des Realms (demo) eingetragen und die Erstellung bestätigt.

realm data
Figure 2. Realm Name eintragen und erstellen

Für alle zukünftigen Veränderungen muss der neu erstellte demo Realm ausgewählt sein.

realm selected

2.5.2. Rollen erstellen

Für dieses Beispiel müssen zwei Rollen erstellt werden: administrator und user. Die genauen Rechte der Rollen folgen in den jeweiligen Applikationen. Die Erstellung der Rollen erfolgt im Menüpunkt Realm roles.

create roles button
Figure 3. Rolle erstellen
Administrator

Die Rolle des Administrators trägt den Namen admin.

admin role input data
Figure 4. Administrator Rolle

Die Rollenerstellung wird mit Save bestätigt.

Nutzerrolle

Die Rolle eines normalen Nutzers trägt den Namen user.

user role input data
Figure 5. User Rolle

2.5.3. Nutzer erstellen

Neben den Rollen werden für die Applikation auch zwei Nutzer angelegt. Dies geschieht über den Menüpunkt Users.

create user button
Figure 6. Nutzer erstellen
Nutzer 1

Für den ersten Nutzer John Doe müssen folgende Werte eingetragen werden.

create user john data
Figure 7. Nutzerdaten eintragen

Nach der Erstellung des Nutzers kann dieser über den Role mapping Tab zu den Rollen user und admin hinzugefügt werden.

role mapping john assign role
Figure 8. Rollen hinzufügen

Im gezeigten Pop-Up muss die Filterung auf Realm Rollen umgestellt werden.

ream role filter
Figure 9. Realm Rollen Filter

Anschließend werden die gewünschten Rollen admin und user ausgewählt und schließlcih auf assign geklickt.

realm role assignment
Figure 10. Rollen zuordnen

Zuletzt muss für den Nutzer noch ein Passwort festgelegt werden.

user set credentials
Figure 11. Credential hinzufügen
user set password
Figure 12. Passwort setzen
user confirm password
Figure 13. Passwort bestätigen
Nutzer 2

Das Prozedere der Erstellung eines Nutzers muss für den zweiten Nutzer wiederholt werden. Die Optionen lauten:

  • Email verified: on

  • Username: jane

  • Email: jane.doe@example.com

  • First Name: Jane

  • Last Name: Doe

  • Password: password

  • Rollen: user (KEINE admin Rolle)

jane doe data
Figure 14. Nutzer 2 Daten
jane doe roles
Figure 15. Nutzer 2 Rollen
user 2 set pw
Figure 16. Nutzer 2 Passwort

2.6. Clients erstellen

Auf dem Keycloak Server stellen Clients Applikationen dar, die Authentifizierung eines Nutzers beantragen können. Jeder Client wird durch eine eindeutige id identifiziert. Nicht öffentlich verteilte Clients benötigen zusätzlich zur id ein secret, um mit dem Keycloak Server kommunizieren zu können.

Für dieses Beispiel werden zwei Clients angelegt:

  • backend: Ein nicht öffentlich verteilter Client, der das RBAC/PBAC Quarkus Backend darstellt.

  • frontend: Ein öffentlich verteilter Client, der eine Benutzeroberfläche (z.B. Website) darstellt. Dieser Client wird in diesem Tutorial nicht implementiert. Allerdings wird der Client dazu verwendet, um einen JSON Web Token für den Test der Endpunkte zu erhalten.

create client button
Figure 17. Client erstellen

2.6.1. Backend Client

backend client create
Figure 18. Backend Client erstellen Grunddaten
backend client auth
Figure 19. Backend Client Capabilities

Beim Backend Client wird die Client Authentication aktiviert, da es sich um einen nicht öffentlich verteilten Client handelt.

Außerdem wird die Authorization aktiviert, um feine Kontrolle über die Rechtevergabe zu erlangen.

backend client login
Figure 20. Backend Client Login

Während der Entwicklung werden außerdem alle URIs zugelassen (*).

2.6.2. Frontend Client

frontend general data
Figure 21. Frontend Client erstellen Grunddaten
Frontend Client Capabilities

frontend caps

Für das Frontend wird Client authentication nicht aktiviert, da ein Frontend (z.B. Website) öffentlich verteilt wird.

Frontend Login

frontend login

Für das Frontend werden hier erneut alle URIs erlaubt. In Produktivsystemen sollte dies allerdings auf die tatsächliche URL des Frontends beschränkt werden.

3. Zugriffskontrolle mit RBAC

3.1. Quarkus Projekt erstellen

Über die Maven CLI kann ein neues Quarkus Projekt mit allen benötigten Abhängigkeiten erstellt werden:

mvn io.quarkus.platform:quarkus-maven-plugin:3.21.0:create \
    -DprojectGroupId=at.htl \
    -DprojectArtifactId=backend-rbac \
    -Dextensions='rest-jackson,oidc,hibernate-orm-panache,jdbc-postgresql,keycloak-authorization'
cd backend-rbac

Alternativ können auch folgende Einträge im pom.xml ergänzt werden:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-keycloak-authorization</artifactId>
</dependency>

3.2. application.properties

Für die Anbindung an die Datenbank und an den Keycloak Server müssen folgende einträge in die application.properties Datei des Quarkus Backends eingetragen werden:

src/main/resources/application.properties
#### datasource configuration ####(1)
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=app
quarkus.datasource.password=app
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/db

#### hibernate configuration ####
quarkus.hibernate-orm.database.generation=drop-and-create
(2)
quarkus.hibernate-orm.physical-naming-strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy

#### keycloak configuration ####
(3)
quarkus.oidc.auth-server-url=http://localhost:8000/realms/demo
(4)
quarkus.oidc.client-id=backend
(5)
quarkus.oidc.credentials.secret=9xCZUZAkn4XPcraKU2vIGg3qiiJ2ieCa

#### other ####
(6)
quarkus.devservices.enabled=false
1 In diesem Beispiel wird Postgres als Datenbank verwendet. In dieser sektion müssen gegebenenfalls die Anmeldedaten und der Datenbanktyp angepasst werden.
2 Durch setzten der physical-naming-strategy auf CamelCaseToUnderscoresNamingStrategy werden die Attributnamen der Entitäten automatisch von camelCase zu snake_case umgewandelt, wenn die Tabellen in der Datenbank angelegt werden.
3 URL des Keycloak Servers. Hier wird am Ende der URL auch der gewünschte Realm angegeben. Die URL folgt dem Format: <keycloak_url>/realms/{realm}
4 Id des Keycloak clients.
5 Secret des Keycloak Clients. Das secret muss mit dem Wert aus der Keycloak Admin Oberfläche ersetzt werden. client secret
6 Wird das Quarkus OIDC Plugin verwendet, startet Quarkus automatisch einen Keycloak Docker Container. Dieses Verhalten ist für dieses Beispiel unerwünscht und wird daher deaktiviert.

3.3. Persistenzschicht

Zu Demonstrationszwecken wird für dieses Tutorial ein kleines Datenmodell erstellt. Bei bereits bestehenden Projekten kann diese Sektion problemlos übersprungen werden.

3.3.1. Entität: Vehicle

src/main/java/at/htl/feature/vehicle/Vehicle.java
package at.htl.feature.vehicle;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Vehicle {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long id;

    public String make;
    public String model;
    public long year;
}

Um das Beispiel kompakt zu halten wurde hier auf Getter und Setter verzichtet.

3.3.2. VehicleRepository

src/main/java/at/htl/feature/vehicle/VehicleRepository.java
package at.htl.feature.vehicle;

import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class VehicleRepository implements PanacheRepository<Vehicle> {
}

3.3.3. Beispieldaten

Über die import.sql Datei können bei Start der Applikation automatisch Testdaten in die Datenbank eingefügt werden.

src/main/resources/import.sql
insert into vehicle (make, model, year) values ('Honda', 'Civic', 1990);
insert into vehicle (make, model, year) values ('Renault', 'Clio', 2001);
insert into vehicle (make, model, year) values ('BMW', 'x5', 2003);

3.4. WebAPI

In der WebAPI werden für dieses Beispiel zwei Endpunkte registriert:

  • all: liefert eine Liste aller Fahrzeuge zurück und soll nur für Administratoren zugänglich sein.

  • byId: liefert das Fahrzeug mit der im Pfad übergebenen Id zurück. Dieser Endpunkt ist nur für Nutzer zugänglich.

src/main/java/at/htl/feature/vehicle/VehicleResource.java
package at.htl.feature.vehicle;

import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;

import java.util.List;

@Path("/vehicle/")
public class VehicleResource {
    @Inject
    VehicleRepository vehicleRepository;

    @GET
    @RolesAllowed("admin") (1)
    public List<Vehicle> all() {
        return vehicleRepository.listAll();
    }

    @GET
    @Path("/{id}")
    @RolesAllowed("user") (2)
    public Vehicle byId(@PathParam("id") long id) {
        return vehicleRepository
                .findById(id);
    }
}

Über die Annotation @RolesAllowed(…​) werden die Rollen festgelegt, für die ein Endpunkt zugänglich sein soll. Soll der Endpunkt für mehrere Rollen zugänglich sein, kann der Annotation auch ein Array übergeben werden: @RolesAllowed({"user", "admin"})

3.5. Keycloak Konfiguration

Für RBAC muss keine weitere Konfiguration von Keycloak vorgenommen werden.

3.6. Test

Für den Test muss die Quarkus Applikation gestartet werden.

mvn clean quarkus:dev

Anschließend kann folgendes Shellscript ausgeführt werden:

#!/usr/bin/bash

set -e

kc_url="http://localhost:8000"
backend_url="http://localhost:8080"
client_id="frontend"
normal_user="jane"
admin_user="john"
password="password"


cmd_resp=""
function show_cmd() {
   echo $ $@
   cmd_resp=$(eval "$@")
}

echo Querying jwt token endpoint...
show_cmd "curl --silent $kc_url/realms/demo/.well-known/openid-configuration | jq -r '.token_endpoint'"
tok_url=$cmd_resp
echo Found token endpoint at: $tok_url

echo -e "\nRequesting access token for normal user..."
show_cmd "curl --silent -d 'client_id=$client_id' -d 'username=$admin_user' -d 'password=$password' -d 'grant_type=password' $tok_url | jq -r '.access_token'"
token=$cmd_resp

echo -e "\nRequesting all vehicles"
echo "$ curl -H 'Authorization: Bearer ...' $backend_url/vehicle/"
curl -H "Authorization: Bearer $token" $backend_url/vehicle/
echo

echo -e "\nRequesting vehicle id=1"
echo "$ curl -H 'Authorization: Bearer ...' $backend_url/vehicle/1"
curl -H "Authorization: Bearer $token" $backend_url/vehicle/1
echo

Ein erfolgreicher Output sieht folgendermaßen aus:

Querying jwt token endpoint...
$ curl --silent http://localhost:8000/realms/demo/.well-known/openid-configuration | jq -r '.token_endpoint'
Found token endpoint at: http://localhost:8000/realms/demo/protocol/openid-connect/token

Requesting access token for normal user...
$ curl --silent -d 'client_id=frontend' -d 'username=john' -d 'password=password' -d 'grant_type=password' http://localhost:8000/realms/demo/protocol/openid-connect/token | jq -r '.access_token'

Requesting all vehicles
$ curl -H 'Authorization: Bearer ...' http://localhost:8080/vehicle/
[{"id":1,"make":"Honda","model":"Civic","year":1990},{"id":2,"make":"Renault","model":"Clio","year":2001},{"id":3,"make":"BMW","model":"x5","year":2003}]

Requesting vehicle id=1
$ curl -H 'Authorization: Bearer ...' http://localhost:8080/vehicle/1
{"id":1,"make":"Honda","model":"Civic","year":1990}

Werden keine Vehicles ausgegeben, besteht ein Konfigurationsfehler.

4. Zugriffskontrolle mit PBAC

4.1. Quarkus Projekt erstellen

Über die Maven CLI kann ein neues Quarkus Projekt mit allen benötigten Abhängigkeiten erstellt werden:

mvn io.quarkus.platform:quarkus-maven-plugin:3.21.0:create \
    -DprojectGroupId=at.htl \
    -DprojectArtifactId=backend-policy \
    -Dextensions='rest-jackson,oidc,hibernate-orm-panache,jdbc-postgresql,keycloak-authorization'
cd backend-policy

Alternativ können auch folgende Einträge im pom.xml ergänzt werden:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-keycloak-authorization</artifactId>
</dependency>

4.2. application.properties

Ein großteil der application.properties Date wurde bereits im Abschnitt application.properties beschrieben

src/main/resources/application.properties
#### datasource configuration ####
quarkus.datasource.db-kind = postgresql
quarkus.datasource.username = app
quarkus.datasource.password = app
quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:5432/db

#### hibernate configuration ####
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.physical-naming-strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy

#### keycloak configuration ####
quarkus.oidc.auth-server-url=http://localhost:8000/realms/demo
quarkus.oidc.client-id=backend
(1)
quarkus.oidc.credentials.secret=9xCZUZAkn4XPcraKU2vIGg3qiiJ2ieCa
(2)
quarkus.keycloak.policy-enforcer.enable=true

#### other ####
quarkus.devservices.enabled=false
1 Secret des Keycloak Clients. Das secret muss mit dem Wert aus der Keycloak Admin Oberfläche ersetzt werden. client secret
2 Für die Zugriffskontrolle mit PBAC muss in der application.properties Datei eine zusätzliche Option quarkus.keycloak.policy-enforcer.enable gesetzt werden.

4.3. Persistenzschicht

Die Elemente der Persistenzschicht können aus der Persistenzschicht Sektion des ersten Projekts übernommen werden.

4.4. WebAPI

In der WebAPI werden wie im RBAC Beispiel zwei Endpunkte angeboten:

  • all: liefert eine Liste aller Fahrzeuge zurück und soll nur für Administratoren zugänglich sein.

  • byId: liefert das Fahrzeug mit der im Pfad übergebenen Id zurück. Dieser Endpunkt ist nur für Nutzer zugänglich.

src/main/java/at/htl/feature/vehicle/VehicleResource.java
package at.htl.feature.vehicle;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;

import java.util.List;

@Path("/vehicle/")
public class VehicleResource {
    @Inject
    VehicleRepository vehicleRepository;

    @GET
    public List<Vehicle> all() {
        return vehicleRepository.listAll();
    }

    @GET
    @Path("/{id}")
    public Vehicle byId(@PathParam("id") long id) {
        return vehicleRepository
                .findById(id);
    }
}

Im Vergleich zum RBAC Beispiel müssen in der Ressource keine @RolesAllowed Annotationen hinzugefügt werden. Die Konfiguration erfolgt ausschließlich über die Keycloak Administratoroberfläche.

4.5. Keycloak Konfiguration

Um die Vehicle Ressource zu schützen, müssen am Keycloak Server einige Regeln festgelegt werden.

4.5.1. Ressourcen

Im ersten Schritt müssen die zu schützenden Ressourcen des Clients am Keycloak Server eingetragen werden.

create resource
Figure 22. Ressource erstellen
Get all vehicles

all resource

Get vehicle by id

vehicle by id

Pfadparameter werden im Format {<name>} abgebildet.

4.5.2. Policies

Eine Policy stellt eine Regel dar, deren Auswertung zu einer Menge von Nutzern führt. Policies werden mithilfe von Permissions mit Ressourcen verbunden, um der ausgewählten Menge an Nutzern Zugriff zu ermöglichen.

Policy erstellen

create policy

Es gibt unterschiedliche Typen von Policies. Da in diesem Beispiel bereits Rollen erstellt wurden, wird hier der Typ Role gewählt.

Policy Typ

role policy

Only Administrators
Policy Name eintragen

only admins policy

Policy Rollen Filter

only admins assign roles

Policy Rollen Auswahl

only admins role select

Policy Rolle erfordern

only admins required

Only Users

Für Nutzer wird dasselbe wie für Administratoren (Sektion Only Administrators) wiederholt, nur mit der Nutzerrolle.

Policy Daten

only users data

4.5.3. Permissions

Eine Permission gibt an welche Policies für den Zugriff auf eine bestimmte Ressource benötigt werden. Es stellt also das Bindeglied Zwischen Policies und Ressourcen dar.

Permission erstellen

create permission

Vehicle Administrator
Administrator Permission erstellen

vehicle admin perm

Vehicle User
User Permission erstellen

only user perm

4.6. Test

Für den Test kann erneut das in der Sektion Test beschriebene Shellscript verwendet werden. Die Policy basierte Version sollte genau dasselbe Verhalten und Ergebnis aufweisen.