Keycloak shenanigans
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/labhelper) (push) Successful in 17s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/labhelper-data-loader) (push) Successful in 3s

This commit is contained in:
2026-02-25 22:24:44 +01:00
parent 88ff6ddae5
commit 4ad03403aa
6 changed files with 29 additions and 20 deletions

View File

@@ -47,7 +47,7 @@ Go to **Clients → labhelper → Client scopes** tab → click the dedicated sc
|---|---|---| |---|---|---|
| Name | `groups` | Label for this mapper | | Name | `groups` | Label for this mapper |
| Token Claim Name | `groups` | The claim name the app reads from the token | | Token Claim Name | `groups` | The claim name the app reads from the token |
| Full group path | Off | Sends `Lab Administrators` instead of `/Lab Administrators`. The app strips leading slashes anyway, but this is cleaner | | Full group path | Off | Sends `LabHelper Administrators` instead of `/LabHelper Administrators`. The app strips leading slashes anyway, but this is cleaner |
| Add to ID token | On | | | Add to ID token | On | |
| Add to access token | On | | | Add to access token | On | |
| Add to userinfo | On | The app fetches userinfo after the token exchange | | Add to userinfo | On | The app fetches userinfo after the token exchange |
@@ -56,9 +56,9 @@ Go to **Clients → labhelper → Client scopes** tab → click the dedicated sc
Go to **Groups** (left sidebar) and create these three groups with exactly these names — they map to the existing Django groups: Go to **Groups** (left sidebar) and create these three groups with exactly these names — they map to the existing Django groups:
- `Lab Administrators` — gets `is_staff=True` in Django (admin access) - `LabHelper Administrators` — gets `is_staff=True` in Django (admin access)
- `Lab Staff` - `LabHelper Staff`
- `Lab Viewers` - `LabHelper Viewers`
### 6. Ensure users have an email address ### 6. Ensure users have an email address
@@ -120,7 +120,7 @@ Overrides `OIDCAuthenticationBackend` to:
- Use `preferred_username` from Keycloak as the Django username - Use `preferred_username` from Keycloak as the Django username
- Set `first_name` and `last_name` from `given_name` / `family_name` claims - Set `first_name` and `last_name` from `given_name` / `family_name` claims
- Sync group memberships on every login — if a user is added to or removed from a Keycloak group, it takes effect at their next login - Sync group memberships on every login — if a user is added to or removed from a Keycloak group, it takes effect at their next login
- Set `is_staff=True` for members of `Lab Administrators` (grants Django admin access) - Set `is_staff=True` for members of `LabHelper Administrators` (grants Django admin access)
`django.contrib.auth.backends.ModelBackend` is kept as a fallback so the Django admin login form still works with a local username/password (useful for emergency superuser access without Keycloak). `django.contrib.auth.backends.ModelBackend` is kept as a fallback so the Django admin login form still works with a local username/password (useful for emergency superuser access without Keycloak).

View File

@@ -18,6 +18,7 @@ data:
OIDC_OP_BASE_URL: "https://sso.baumann.gr/realms/homelab" OIDC_OP_BASE_URL: "https://sso.baumann.gr/realms/homelab"
OIDC_RP_CLIENT_ID: "labhelper" OIDC_RP_CLIENT_ID: "labhelper"
LOGIN_REDIRECT_URL: "index" LOGIN_REDIRECT_URL: "index"
LOGOUT_REDIRECT_URL: "login" LOGOUT_REDIRECT_URL: "/login/"
OIDC_AUTHENTICATION_FAILURE_REDIRECT_URL: "/login/"
GUNICORN_OPTS: "--access-logfile -" GUNICORN_OPTS: "--access-logfile -"
IMAGE_TAG: "0.077" IMAGE_TAG: "0.078"

View File

@@ -27,7 +27,7 @@ spec:
mountPath: /data mountPath: /data
containers: containers:
- name: web - name: web
image: git.baumann.gr/adebaumann/labhelper:0.077 image: git.baumann.gr/adebaumann/labhelper:0.078
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- containerPort: 8000 - containerPort: 8000
@@ -117,6 +117,11 @@ spec:
configMapKeyRef: configMapKeyRef:
name: django-config name: django-config
key: LOGOUT_REDIRECT_URL key: LOGOUT_REDIRECT_URL
- name: OIDC_AUTHENTICATION_FAILURE_REDIRECT_URL
valueFrom:
configMapKeyRef:
name: django-config
key: OIDC_AUTHENTICATION_FAILURE_REDIRECT_URL
- name: GUNICORN_OPTS - name: GUNICORN_OPTS
valueFrom: valueFrom:
configMapKeyRef: configMapKeyRef:

View File

@@ -2,16 +2,16 @@ from mozilla_django_oidc.auth import OIDCAuthenticationBackend
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
# Keycloak group name → Django group name mapping. # Keycloak group name → Django group name mapping.
# Keycloak may send group paths with a leading slash (e.g. "/Lab Administrators"); # Keycloak may send group paths with a leading slash (e.g. "/LabHelper Administrators");
# these are stripped before comparison. # these are stripped before comparison.
KEYCLOAK_GROUP_MAP = { KEYCLOAK_GROUP_MAP = {
'Lab Administrators': 'Lab Administrators', 'LabHelper Administrators': 'LabHelper Administrators',
'Lab Staff': 'Lab Staff', 'LabHelper Staff': 'LabHelper Staff',
'Lab Viewers': 'Lab Viewers', 'LabHelper Viewers': 'LabHelper Viewers',
} }
# Members of these groups receive is_staff=True (Django admin access) # Members of these groups receive is_staff=True (Django admin access)
STAFF_GROUPS = {'Lab Administrators'} STAFF_GROUPS = {'LabHelper Administrators'}
class KeycloakOIDCBackend(OIDCAuthenticationBackend): class KeycloakOIDCBackend(OIDCAuthenticationBackend):
@@ -35,7 +35,7 @@ class KeycloakOIDCBackend(OIDCAuthenticationBackend):
user.first_name = claims.get('given_name', user.first_name) user.first_name = claims.get('given_name', user.first_name)
user.last_name = claims.get('family_name', user.last_name) user.last_name = claims.get('family_name', user.last_name)
# Keycloak sends group paths like "/Lab Administrators"; normalise them. # Keycloak sends group paths like "/LabHelper Administrators"; normalise them.
raw_groups = claims.get('groups', []) raw_groups = claims.get('groups', [])
keycloak_groups = {g.lstrip('/') for g in raw_groups} keycloak_groups = {g.lstrip('/') for g in raw_groups}

View File

@@ -9,9 +9,9 @@ class Command(BaseCommand):
self.stdout.write('Creating default users and groups...') self.stdout.write('Creating default users and groups...')
groups = { groups = {
'Lab Administrators': 'Full access to all lab functions', 'LabHelper Administrators': 'Full access to all lab functions',
'Lab Staff': 'Can view and search items, add things to boxes', 'LabHelper Staff': 'Can view and search items, add things to boxes',
'Lab Viewers': 'Read-only access to view and search', 'LabHelper Viewers': 'Read-only access to view and search',
} }
for group_name, description in groups.items(): for group_name, description in groups.items():
@@ -22,9 +22,9 @@ class Command(BaseCommand):
self.stdout.write(f'Group already exists: {group_name}') self.stdout.write(f'Group already exists: {group_name}')
users = { users = {
'admin': ('Lab Administrators', True), 'admin': ('LabHelper Administrators', True),
'staff': ('Lab Staff', False), 'staff': ('LabHelper Staff', False),
'viewer': ('Lab Viewers', False), 'viewer': ('LabHelper Viewers', False),
} }
for username, (group_name, is_superuser) in users.items(): for username, (group_name, is_superuser) in users.items():

View File

@@ -178,5 +178,8 @@ OIDC_OP_LOGOUT_ENDPOINT = os.environ.get('OIDC_OP_LOGOUT_ENDPOINT', f'{_oidc_con
# Store the ID token in the session so Keycloak logout can use id_token_hint # Store the ID token in the session so Keycloak logout can use id_token_hint
OIDC_STORE_ID_TOKEN = True OIDC_STORE_ID_TOKEN = True
# Redirect to the static login page on auth failure instead of looping back into OIDC
OIDC_AUTHENTICATION_FAILURE_REDIRECT_URL = os.environ.get('OIDC_AUTHENTICATION_FAILURE_REDIRECT_URL', '/login/')
# Exempt AJAX endpoints from the session-refresh middleware redirect # Exempt AJAX endpoints from the session-refresh middleware redirect
OIDC_EXEMPT_URLS = ['search_api'] OIDC_EXEMPT_URLS = ['search_api']