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
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:
@@ -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).
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
Reference in New Issue
Block a user