sso: provision vaultwarden users

This commit is contained in:
Brad Stein 2026-01-02 21:03:44 -03:00
parent 727d8cfd48
commit 5437cebb9e
6 changed files with 167 additions and 3 deletions

View File

@ -0,0 +1,98 @@
#!/usr/bin/env python3
from __future__ import annotations
import sys
from typing import Any, Iterable
import httpx
from atlas_portal import settings
from atlas_portal.keycloak import admin_client
from atlas_portal.vaultwarden import invite_user
def _iter_keycloak_users(page_size: int = 200) -> Iterable[dict[str, Any]]:
client = admin_client()
if not client.ready():
raise RuntimeError("keycloak admin client not configured")
url = f"{settings.KEYCLOAK_ADMIN_URL}/admin/realms/{settings.KEYCLOAK_REALM}/users"
first = 0
while True:
headers = client.headers()
params = {"first": str(first), "max": str(page_size)}
with httpx.Client(timeout=settings.HTTP_CHECK_TIMEOUT_SEC) as http:
resp = http.get(url, params=params, headers=headers)
resp.raise_for_status()
payload = resp.json()
if not isinstance(payload, list) or not payload:
return
for item in payload:
if isinstance(item, dict):
yield item
if len(payload) < page_size:
return
first += page_size
def _email_for_user(user: dict[str, Any]) -> str:
email = (user.get("email") if isinstance(user.get("email"), str) else "") or ""
if email.strip():
return email.strip()
username = (user.get("username") if isinstance(user.get("username"), str) else "") or ""
username = username.strip()
if not username:
return ""
return f"{username}@{settings.MAILU_DOMAIN}"
def main() -> int:
processed = 0
created = 0
skipped = 0
failures = 0
for user in _iter_keycloak_users():
username = (user.get("username") if isinstance(user.get("username"), str) else "") or ""
username = username.strip()
if not username:
skipped += 1
continue
enabled = user.get("enabled")
if enabled is False:
skipped += 1
continue
if user.get("serviceAccountClientId") or username.startswith("service-account-"):
skipped += 1
continue
email = _email_for_user(user)
if not email:
print(f"skip {username}: missing email", file=sys.stderr)
skipped += 1
continue
processed += 1
result = invite_user(email)
if result.ok:
created += 1
print(f"ok {username}: {result.status}")
else:
failures += 1
print(f"err {username}: {result.status} {result.detail}", file=sys.stderr)
print(
f"done processed={processed} created_or_present={created} skipped={skipped} failures={failures}",
file=sys.stderr,
)
return 0 if failures == 0 else 2
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -71,7 +71,7 @@ spec:
name: atlas-portal-db name: atlas-portal-db
key: PORTAL_DATABASE_URL key: PORTAL_DATABASE_URL
- name: HTTP_CHECK_TIMEOUT_SEC - name: HTTP_CHECK_TIMEOUT_SEC
value: "10" value: "20"
- name: ACCESS_REQUEST_SUBMIT_RATE_LIMIT - name: ACCESS_REQUEST_SUBMIT_RATE_LIMIT
value: "30" value: "30"
- name: ACCESS_REQUEST_SUBMIT_RATE_WINDOW_SEC - name: ACCESS_REQUEST_SUBMIT_RATE_WINDOW_SEC

View File

@ -13,4 +13,13 @@ resources:
- frontend-service.yaml - frontend-service.yaml
- backend-deployment.yaml - backend-deployment.yaml
- backend-service.yaml - backend-service.yaml
- vaultwarden-cred-sync-cronjob.yaml
- ingress.yaml - ingress.yaml
configMapGenerator:
- name: vaultwarden-cred-sync-script
namespace: bstein-dev-home
files:
- vaultwarden_cred_sync.py=../../scripts/vaultwarden_cred_sync.py
options:
disableNameSuffixHash: true

View File

@ -0,0 +1,57 @@
# services/bstein-dev-home/vaultwarden-cred-sync-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: vaultwarden-cred-sync
namespace: bstein-dev-home
spec:
schedule: "*/15 * * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 0
template:
spec:
serviceAccountName: bstein-dev-home
restartPolicy: Never
nodeSelector:
kubernetes.io/arch: arm64
node-role.kubernetes.io/worker: "true"
imagePullSecrets:
- name: harbor-bstein-robot
containers:
- name: sync
image: registry.bstein.dev/bstein/bstein-dev-home-backend:0.1.1-49 # {"$imagepolicy": "bstein-dev-home:bstein-dev-home-backend"}
imagePullPolicy: Always
command:
- python
- /scripts/vaultwarden_cred_sync.py
env:
- name: KEYCLOAK_ENABLED
value: "true"
- name: KEYCLOAK_REALM
value: atlas
- name: KEYCLOAK_ADMIN_URL
value: http://keycloak.sso.svc.cluster.local
- name: KEYCLOAK_ADMIN_REALM
value: atlas
- name: KEYCLOAK_ADMIN_CLIENT_ID
value: bstein-dev-home-admin
- name: KEYCLOAK_ADMIN_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: bstein-dev-home-keycloak-admin
key: client_secret
- name: HTTP_CHECK_TIMEOUT_SEC
value: "20"
volumeMounts:
- name: vaultwarden-cred-sync-script
mountPath: /scripts
readOnly: true
volumes:
- name: vaultwarden-cred-sync-script
configMap:
name: vaultwarden-cred-sync-script
defaultMode: 0555

View File

@ -2,7 +2,7 @@
apiVersion: batch/v1 apiVersion: batch/v1
kind: Job kind: Job
metadata: metadata:
name: keycloak-ldap-federation-4 name: keycloak-ldap-federation-5
namespace: sso namespace: sso
spec: spec:
backoffLimit: 2 backoffLimit: 2

View File

@ -2,7 +2,7 @@
apiVersion: batch/v1 apiVersion: batch/v1
kind: Job kind: Job
metadata: metadata:
name: keycloak-realm-settings-9 name: keycloak-realm-settings-10
namespace: sso namespace: sso
spec: spec:
backoffLimit: 0 backoffLimit: 0