diff --git a/scripts/vaultwarden_cred_sync.py b/scripts/vaultwarden_cred_sync.py new file mode 100644 index 0000000..8f844de --- /dev/null +++ b/scripts/vaultwarden_cred_sync.py @@ -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()) diff --git a/services/bstein-dev-home/backend-deployment.yaml b/services/bstein-dev-home/backend-deployment.yaml index b1b47c8..053c986 100644 --- a/services/bstein-dev-home/backend-deployment.yaml +++ b/services/bstein-dev-home/backend-deployment.yaml @@ -71,7 +71,7 @@ spec: name: atlas-portal-db key: PORTAL_DATABASE_URL - name: HTTP_CHECK_TIMEOUT_SEC - value: "10" + value: "20" - name: ACCESS_REQUEST_SUBMIT_RATE_LIMIT value: "30" - name: ACCESS_REQUEST_SUBMIT_RATE_WINDOW_SEC diff --git a/services/bstein-dev-home/kustomization.yaml b/services/bstein-dev-home/kustomization.yaml index 99b9443..3f3ebc0 100644 --- a/services/bstein-dev-home/kustomization.yaml +++ b/services/bstein-dev-home/kustomization.yaml @@ -13,4 +13,13 @@ resources: - frontend-service.yaml - backend-deployment.yaml - backend-service.yaml + - vaultwarden-cred-sync-cronjob.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 diff --git a/services/bstein-dev-home/vaultwarden-cred-sync-cronjob.yaml b/services/bstein-dev-home/vaultwarden-cred-sync-cronjob.yaml new file mode 100644 index 0000000..4acf673 --- /dev/null +++ b/services/bstein-dev-home/vaultwarden-cred-sync-cronjob.yaml @@ -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 diff --git a/services/keycloak/ldap-federation-job.yaml b/services/keycloak/ldap-federation-job.yaml index 5c59fff..9650468 100644 --- a/services/keycloak/ldap-federation-job.yaml +++ b/services/keycloak/ldap-federation-job.yaml @@ -2,7 +2,7 @@ apiVersion: batch/v1 kind: Job metadata: - name: keycloak-ldap-federation-4 + name: keycloak-ldap-federation-5 namespace: sso spec: backoffLimit: 2 diff --git a/services/keycloak/realm-settings-job.yaml b/services/keycloak/realm-settings-job.yaml index 2b106d1..ae9b8d1 100644 --- a/services/keycloak/realm-settings-job.yaml +++ b/services/keycloak/realm-settings-job.yaml @@ -2,7 +2,7 @@ apiVersion: batch/v1 kind: Job metadata: - name: keycloak-realm-settings-9 + name: keycloak-realm-settings-10 namespace: sso spec: backoffLimit: 0