From 59a02bc7dc88995098a89a27a4459c16a2474f29 Mon Sep 17 00:00:00 2001 From: ivan Date: Thu, 5 Nov 2026 16:15:10 +0500 Subject: [PATCH] =?UTF-8?q?uralkal:=20django=20=E2=80=94=20Job=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D1=91=D1=82=20=D0=B2=20Zitadel=20actio?= =?UTF-8?q?n=20createUser=20=D0=BD=D0=B0=20External=20Authentication=20/?= =?UTF-8?q?=20Pre=20Creation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/django/uralkal/kustomization.yaml | 2 + .../uralkal/zitadel-action-configmap.yaml | 117 ++++++++++++++++++ apps/django/uralkal/zitadel-action-job.yaml | 63 ++++++++++ 3 files changed, 182 insertions(+) create mode 100644 apps/django/uralkal/zitadel-action-configmap.yaml create mode 100644 apps/django/uralkal/zitadel-action-job.yaml diff --git a/apps/django/uralkal/kustomization.yaml b/apps/django/uralkal/kustomization.yaml index 13ccf19..19ccdd1 100644 --- a/apps/django/uralkal/kustomization.yaml +++ b/apps/django/uralkal/kustomization.yaml @@ -4,6 +4,8 @@ resources: - ../base - kc-sync-configmap.yaml - kc-sync-cronjob.yaml + - zitadel-action-configmap.yaml + - zitadel-action-job.yaml patches: - path: nginx-configmap.yaml target: diff --git a/apps/django/uralkal/zitadel-action-configmap.yaml b/apps/django/uralkal/zitadel-action-configmap.yaml new file mode 100644 index 0000000..64660dc --- /dev/null +++ b/apps/django/uralkal/zitadel-action-configmap.yaml @@ -0,0 +1,117 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: zitadel-action-create-user + namespace: django +data: + createUser.js: | + function createUser(ctx, api) { + let http = require('zitadel/http'); + let logger = require('zitadel/log'); + + const BACKEND_HOST = 'http://backend-svc.django.svc.cluster.local:80/api' + const USER_COMPANIES = [1] + + const BASIC_AUTH = '__BASIC_AUTH__' + + logger.log('Creating external user in backend'); + logger.log('Context: ' + JSON.stringify(ctx)); + + const userData = { + first_name: ctx.v1.user.human.firstName, + last_name: ctx.v1.user.human.lastName, + email: ctx.v1.user.human.email, + username: ctx.v1.user.username, + companies: USER_COMPANIES, + departments: [], + user_permissions: [] + }; + + logger.log('Sending user data: ' + JSON.stringify(userData)); + + const response = http.fetch(BACKEND_HOST + '/core/admin/users/external/', { + method: 'POST', + body: userData, + headers: { + 'Content-Type': 'application/json', + 'Authorization': BASIC_AUTH + } + }); + + if (response.status != 200 && response.status != 201) { + logger.log('Error response status: ' + response.status); + logger.log('Error response body: ' + response.text()); + throw "Backend API error: " + response.status; + } + + const metadata = response.json(); + + logger.log('Metadata received: ' + JSON.stringify(metadata)); + + Object.keys(metadata).forEach(function(key) { + var value = metadata[key]; + logger.log('Setting metadata - Key: ' + key + ', Value: ' + value); + api.v1.user.appendMetadata(key, value); + }); + + logger.log('External user successfully created and metadata set'); + } + apply.py: | + import os + import sys + + import httpx + + ZITADEL = os.environ["ZITADEL_URL"] + HEADERS = { + "Host": os.environ["ZITADEL_HOST"], + "X-Forwarded-Proto": "https", + "Authorization": "Bearer " + open("/vault/secrets/zitadel-token").read().strip(), + "Content-Type": "application/json", + } + ORG_NAME = os.environ["ZITADEL_ORG_NAME"] + ACTION_NAME = os.environ["ACTION_NAME"] + FLOW_TYPE = os.environ["FLOW_TYPE"] + TRIGGER_TYPE = os.environ["TRIGGER_TYPE"] + + client = httpx.Client(base_url=ZITADEL, headers=HEADERS, timeout=30) + + + def call(method, path, body=None, org_id=None): + headers = {"x-zitadel-orgid": org_id} if org_id else {} + r = client.request(method, path, json=body, headers=headers) + if r.status_code >= 300: + print(f"{method} {path} -> {r.status_code}: {r.text}") + sys.exit(1) + return r.json() if r.content else {} + + + orgs = call("POST", "/admin/v1/orgs/_search", {"queries": []}).get("result", []) + org = next((o for o in orgs if o.get("name", "").lower() == ORG_NAME.lower()), None) + if org is None: + print(f"org '{ORG_NAME}' not found, available: {[o.get('name') for o in orgs]}") + sys.exit(1) + org_id = org["id"] + print(f"org {org['name']} id={org_id}") + + script = open("/opt/action/createUser.js").read().replace( + "__BASIC_AUTH__", "Basic " + open("/vault/secrets/backend-basic").read().strip() + ) + body = {"name": ACTION_NAME, "script": script, "timeout": "10s", "allowedToFail": False} + + found = call( + "POST", + "/management/v1/actions/_search", + {"queries": [{"actionNameQuery": {"name": ACTION_NAME, "method": "TEXT_QUERY_METHOD_EQUALS"}}]}, + org_id, + ).get("result", []) + if found: + action_id = found[0]["id"] + call("PUT", f"/management/v1/actions/{action_id}", body, org_id) + print(f"action {ACTION_NAME} updated id={action_id}") + else: + action_id = call("POST", "/management/v1/actions", body, org_id)["id"] + print(f"action {ACTION_NAME} created id={action_id}") + + call("POST", f"/management/v1/flows/{FLOW_TYPE}/trigger/{TRIGGER_TYPE}", {"actionIds": [action_id]}, org_id) + print(f"flow {FLOW_TYPE} trigger {TRIGGER_TYPE} -> {ACTION_NAME}") diff --git a/apps/django/uralkal/zitadel-action-job.yaml b/apps/django/uralkal/zitadel-action-job.yaml new file mode 100644 index 0000000..06ea3a3 --- /dev/null +++ b/apps/django/uralkal/zitadel-action-job.yaml @@ -0,0 +1,63 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: zitadel-action-create-user + namespace: django + annotations: + kustomize.toolkit.fluxcd.io/force: enabled +spec: + backoffLimit: 2 + template: + metadata: + annotations: + sidecar.istio.io/inject: "false" + vault.hashicorp.com/agent-inject: "true" + vault.hashicorp.com/agent-pre-populate-only: "true" + vault.hashicorp.com/auth-path: auth/kubernetes + vault.hashicorp.com/role: django + vault.hashicorp.com/agent-inject-secret-zitadel-token: secrets/data/vault/common/django_auth + vault.hashicorp.com/agent-inject-template-zitadel-token: |- + {{- with secret "secrets/data/vault/common/django_auth" -}} + {{ index .Data.data "django_zitadel_access_token" }} + {{- end -}} + vault.hashicorp.com/agent-inject-secret-backend-basic: secrets/data/vault/common/django_auth + vault.hashicorp.com/agent-inject-template-backend-basic: |- + {{- with secret "secrets/data/vault/common/django_auth" -}} + {{ index .Data.data "key" }} + {{- end -}} + spec: + serviceAccountName: django-vault + automountServiceAccountToken: true + restartPolicy: Never + imagePullSecrets: + - name: regcred + containers: + - name: apply + image: cr.yandex/crp3ccidau046kdj8g9q/backend:production_7d0ec48b + imagePullPolicy: IfNotPresent + command: ["python", "/opt/action/apply.py"] + env: + - name: ZITADEL_URL + value: http://zitadel-idp-contour.zitadel.svc.cluster.local:8080 + - name: ZITADEL_HOST + value: login.sarex.local.uralkali.com + - name: ZITADEL_ORG_NAME + value: zitadel + - name: ACTION_NAME + value: createUser + - name: FLOW_TYPE + value: "1" + - name: TRIGGER_TYPE + value: "2" + resources: + requests: + cpu: 20m + memory: 128Mi + volumeMounts: + - name: action + mountPath: /opt/action + readOnly: true + volumes: + - name: action + configMap: + name: zitadel-action-create-user