feat: add homelab-wide Authentik RBAC model and k8s OIDC auth wiring
Adds permissions claim + per-service admin groups in Authentik, scoped Role/RoleBinding per service, public PKCE kubernetes OAuth2 client, and kube-apiserver OIDC extraArgs. Also fixes paperless OIDC signup permissions via adapter override and adds CoreDNS rewrite for authentik.riotpiao.com.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
# Overrides paperless-ngx's own paperless/adapter.py at the same import path
|
||||
# (mounted via subPath in deployment.yaml) - settings.py hardcodes
|
||||
# SOCIALACCOUNT_ADAPTER = "paperless.adapter.CustomSocialAccountAdapter", so
|
||||
# no Django setting needs to change, just the file content underneath it.
|
||||
#
|
||||
# Stock CustomSocialAccountAdapter.populate_user() is a stub ("kept in case
|
||||
# global default permissions are implemented in the future" - they aren't),
|
||||
# so every OIDC signup lands with zero permissions and 403s on every API
|
||||
# endpoint. This adds the actual mapping: Authentik's "permissions" claim
|
||||
# (via the permissions scope, requested in PAPERLESS_SOCIALACCOUNT_PROVIDERS,
|
||||
# computed server-side from group membership by authentik-provision.py) ->
|
||||
# "paperless:write" or "*" (homelab-admins) grants is_staff+is_superuser,
|
||||
# same convention already used for MinIO's policy claim and Grafana's
|
||||
# role_attribute_path. Checking the permission string rather than a literal
|
||||
# group name decouples "what grants access" from which group happens to
|
||||
# hold it - same pattern applies to every other service's Role/RoleBinding
|
||||
# in k8s/infra/rbac/.
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: paperless-adapter
|
||||
data:
|
||||
adapter.py: |
|
||||
from urllib.parse import quote
|
||||
|
||||
from allauth.account.adapter import DefaultAccountAdapter
|
||||
from allauth.core import context
|
||||
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
||||
from django.conf import settings
|
||||
from django.forms import ValidationError
|
||||
from django.urls import reverse
|
||||
|
||||
REQUIRED_PERMISSIONS = {"paperless:write", "*"}
|
||||
|
||||
|
||||
class CustomAccountAdapter(DefaultAccountAdapter):
|
||||
def is_open_for_signup(self, request):
|
||||
allow_signups = super().is_open_for_signup(request)
|
||||
return getattr(settings, "ACCOUNT_ALLOW_SIGNUPS", allow_signups)
|
||||
|
||||
def pre_authenticate(self, request, **credentials):
|
||||
if settings.DISABLE_REGULAR_LOGIN:
|
||||
raise ValidationError("Regular login is disabled")
|
||||
return super().pre_authenticate(request, **credentials)
|
||||
|
||||
def is_safe_url(self, url):
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
|
||||
allowed_hosts = {context.request.get_host()} | set(settings.ALLOWED_HOSTS)
|
||||
if "*" in allowed_hosts:
|
||||
allowed_hosts.remove("*")
|
||||
allowed_hosts.add(context.request.get_host())
|
||||
return url_has_allowed_host_and_scheme(url, allowed_hosts=allowed_hosts)
|
||||
return url_has_allowed_host_and_scheme(url, allowed_hosts=allowed_hosts)
|
||||
|
||||
def get_reset_password_from_key_url(self, key):
|
||||
if settings.PAPERLESS_URL is None:
|
||||
return super().get_reset_password_from_key_url(key)
|
||||
path = reverse(
|
||||
"account_reset_password_from_key",
|
||||
kwargs={"uidb36": "UID", "key": "KEY"},
|
||||
)
|
||||
path = path.replace("UID-KEY", quote(key))
|
||||
return settings.PAPERLESS_URL + path
|
||||
|
||||
|
||||
class CustomSocialAccountAdapter(DefaultSocialAccountAdapter):
|
||||
def is_open_for_signup(self, request, sociallogin):
|
||||
allow_signups = super().is_open_for_signup(request, sociallogin)
|
||||
return getattr(settings, "SOCIALACCOUNT_ALLOW_SIGNUPS", allow_signups)
|
||||
|
||||
def get_connect_redirect_url(self, request, socialaccount):
|
||||
return reverse("base")
|
||||
|
||||
def populate_user(self, request, sociallogin, data):
|
||||
user = super().populate_user(request, sociallogin, data)
|
||||
perms = set(sociallogin.account.extra_data.get("permissions") or [])
|
||||
if perms & REQUIRED_PERMISSIONS:
|
||||
user.is_staff = True
|
||||
user.is_superuser = True
|
||||
return user
|
||||
|
||||
def save_user(self, request, sociallogin, form=None):
|
||||
# populate_user() sets the flags on the in-memory user, but
|
||||
# allauth's default save_user() re-derives is_staff from
|
||||
# ACCOUNT_DEFAULT_HTTP_PROTOCOL-independent defaults and can
|
||||
# overwrite them on save - re-apply after super().save_user()
|
||||
# persists the row, matching the permissions check above exactly.
|
||||
user = super().save_user(request, sociallogin, form)
|
||||
perms = set(sociallogin.account.extra_data.get("permissions") or [])
|
||||
if perms & REQUIRED_PERMISSIONS and not (user.is_staff and user.is_superuser):
|
||||
user.is_staff = True
|
||||
user.is_superuser = True
|
||||
user.save(update_fields=["is_staff", "is_superuser"])
|
||||
return user
|
||||
@@ -82,6 +82,13 @@ spec:
|
||||
mountPath: /usr/src/paperless/data
|
||||
- name: consume
|
||||
mountPath: /usr/src/paperless/consume
|
||||
# Overrides paperless-ngx's own adapter.py in place - settings.py
|
||||
# hardcodes the import path, so no Django setting changes, just
|
||||
# the file content underneath it (see adapter-configmap.yaml).
|
||||
- name: adapter
|
||||
mountPath: /usr/src/paperless/src/paperless/adapter.py
|
||||
subPath: adapter.py
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: media
|
||||
persistentVolumeClaim:
|
||||
@@ -91,3 +98,6 @@ spec:
|
||||
claimName: paperless-data
|
||||
- name: consume
|
||||
emptyDir: {}
|
||||
- name: adapter
|
||||
configMap:
|
||||
name: paperless-adapter
|
||||
|
||||
@@ -9,6 +9,8 @@ resources:
|
||||
- service.yaml
|
||||
- ingress.yaml
|
||||
- backup-cronjob.yaml
|
||||
- adapter-configmap.yaml
|
||||
- rbac.yaml
|
||||
# postgres: paperless-db CNPG Cluster, deployed by k8s/infra/databases (wave 2,
|
||||
# before this app at wave 8) - not duplicated here. Same for the paperless-oidc
|
||||
# and paperless-minio-creds Secrets, written by PostSync provisioning Jobs in
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Scoped operator access for paperless-admins: restart/config-edit rights on
|
||||
# just this service's own resources, nothing CNPG-managed (paperless-db-*)
|
||||
# or provisioning-managed (paperless-oidc, paperless-minio-creds). Inert
|
||||
# until kube-apiserver's OIDC wiring lands (--oidc-groups-claim=groups,
|
||||
# --oidc-groups-prefix=oidc:) - subject name below assumes that prefix.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: paperless-operator
|
||||
rules:
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments"]
|
||||
resourceNames: ["paperless"]
|
||||
verbs: ["get", "list", "watch", "update", "patch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["configmaps"]
|
||||
resourceNames: ["paperless-config"]
|
||||
verbs: ["get", "list", "watch", "update", "patch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
resourceNames: ["paperless-secrets"]
|
||||
verbs: ["get", "list", "watch", "update", "patch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: paperless-admins-binding
|
||||
subjects:
|
||||
- kind: Group
|
||||
name: "oidc:paperless-admins"
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: paperless-operator
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
Reference in New Issue
Block a user