add fix eage case
This commit is contained in:
parent
9dd8287777
commit
43c5a015ca
69
Makefile
69
Makefile
|
|
@ -83,13 +83,27 @@ deps-up-smtp: ## 起 Mongo + Redis + MailHog
|
||||||
$(COMPOSE) --profile smtp up -d mongo redis mailhog
|
$(COMPOSE) --profile smtp up -d mongo redis mailhog
|
||||||
|
|
||||||
deps-down: ## 停服務(保留 volume)
|
deps-down: ## 停服務(保留 volume)
|
||||||
$(COMPOSE) --profile smtp --profile k6 down
|
$(COMPOSE) --profile smtp --profile k6 --profile ldap down
|
||||||
|
|
||||||
deps-down-v: ## 停服務並刪 volume(會清資料)
|
deps-down-v: ## 停服務並刪 volume(會清資料)
|
||||||
$(COMPOSE) --profile smtp --profile k6 down -v
|
$(COMPOSE) --profile smtp --profile k6 --profile ldap down -v
|
||||||
|
|
||||||
deps-logs: ## 看 compose log
|
deps-logs: ## 看 compose log
|
||||||
$(COMPOSE) --profile smtp --profile k6 logs -f
|
$(COMPOSE) --profile smtp --profile k6 --profile ldap logs -f
|
||||||
|
|
||||||
|
# OpenLDAP(本機 LDAP 測試,見 deploy/openldap/)
|
||||||
|
LDAP_COMPOSE := $(DOCKER_COMPOSE) -f deploy/openldap/docker-compose.yml
|
||||||
|
|
||||||
|
ldap-up: ## 只起 OpenLDAP(profile ldap;ZITADEL 用 ldap://openldap:389)
|
||||||
|
$(COMPOSE) --profile ldap up -d openldap
|
||||||
|
@echo "→ run 'make ldap-wait' to seed alice/bob"
|
||||||
|
@echo "→ 測試帳號見 deploy/openldap/README.md"
|
||||||
|
|
||||||
|
ldap-down: ## 停 OpenLDAP
|
||||||
|
$(COMPOSE) --profile ldap stop openldap
|
||||||
|
|
||||||
|
ldap-wait: ## 等 OpenLDAP ready 並 seed alice/bob
|
||||||
|
@$(MAKE) -s k6-wait-ldap
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# k6 測試(test/k6/)
|
# k6 測試(test/k6/)
|
||||||
|
|
@ -101,6 +115,7 @@ K6_GATEWAY_BIN := bin/gateway-k6
|
||||||
K6_DIR := test/k6
|
K6_DIR := test/k6
|
||||||
K6_PAT_FILE := deploy/zitadel/machinekey/zitadel-admin-sa.token
|
K6_PAT_FILE := deploy/zitadel/machinekey/zitadel-admin-sa.token
|
||||||
K6_ENV_FILE := deploy/zitadel/machinekey/k6.env
|
K6_ENV_FILE := deploy/zitadel/machinekey/k6.env
|
||||||
|
K6_BOOTSTRAP_ENV := deploy/zitadel/machinekey/dev-bootstrap.env
|
||||||
ZITADEL_HEALTH_URL := http://localhost:8080/debug/healthz
|
ZITADEL_HEALTH_URL := http://localhost:8080/debug/healthz
|
||||||
|
|
||||||
# k6 安裝指引(macOS / Linux)
|
# k6 安裝指引(macOS / Linux)
|
||||||
|
|
@ -154,7 +169,12 @@ ldap-test: ## 列出 LDAP 測試使用者 alice / bob
|
||||||
-D "cn=admin,dc=gateway,dc=local" -w admin \
|
-D "cn=admin,dc=gateway,dc=local" -w admin \
|
||||||
"(|(uid=alice)(uid=bob))" uid mail cn
|
"(|(uid=alice)(uid=bob))" uid mail cn
|
||||||
|
|
||||||
k6-wait: k6-wait-ldap ## 等 OpenLDAP + ZITADEL ready + 把 PAT 寫到 k6.env
|
k6-bootstrap-zitadel: ## 自動建立 ZITADEL LDAP IdP + OIDC App(dev / k6)
|
||||||
|
@if [ ! -s "$(K6_PAT_FILE)" ]; then echo "PAT missing — run 'make k6-wait' first"; exit 1; fi
|
||||||
|
@python3 deploy/zitadel/bootstrap_dev.py
|
||||||
|
@echo "→ OAuth / LDAP IdP 寫入 $(K6_BOOTSTRAP_ENV)"
|
||||||
|
|
||||||
|
k6-wait: k6-wait-ldap ## 等 OpenLDAP + ZITADEL ready + bootstrap IdP/OAuth + 寫 k6.env
|
||||||
@echo "waiting for ZITADEL at $(ZITADEL_HEALTH_URL)…"
|
@echo "waiting for ZITADEL at $(ZITADEL_HEALTH_URL)…"
|
||||||
@for i in $$(seq 1 120); do \
|
@for i in $$(seq 1 120); do \
|
||||||
if curl -fsS $(ZITADEL_HEALTH_URL) >/dev/null 2>&1; then \
|
if curl -fsS $(ZITADEL_HEALTH_URL) >/dev/null 2>&1; then \
|
||||||
|
|
@ -171,11 +191,14 @@ k6-wait: k6-wait-ldap ## 等 OpenLDAP + ZITADEL ready + 把 PAT 寫到 k6.env
|
||||||
echo "PAT file $(K6_PAT_FILE) missing — check 'docker logs gateway-zitadel'"; \
|
echo "PAT file $(K6_PAT_FILE) missing — check 'docker logs gateway-zitadel'"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
|
@$(MAKE) -s k6-bootstrap-zitadel
|
||||||
@PAT=$$(tr -d '\n' < $(K6_PAT_FILE)); \
|
@PAT=$$(tr -d '\n' < $(K6_PAT_FILE)); \
|
||||||
printf 'export ZITADEL_SERVICE_TOKEN=%s\nexport BASE_URL=http://localhost:8888\nexport MAILHOG_URL=http://localhost:8025\nexport REDIS_ADDR=localhost:6379\n' "$$PAT" > $(K6_ENV_FILE); \
|
printf 'export ZITADEL_SERVICE_TOKEN=%s\nexport BASE_URL=http://localhost:8888\nexport MAILHOG_URL=http://localhost:8025\nexport REDIS_ADDR=localhost:6379\n' "$$PAT" > $(K6_ENV_FILE); \
|
||||||
|
if [ -s "$(K6_BOOTSTRAP_ENV)" ]; then cat $(K6_BOOTSTRAP_ENV) >> $(K6_ENV_FILE); fi; \
|
||||||
echo "wrote $(K6_ENV_FILE)"
|
echo "wrote $(K6_ENV_FILE)"
|
||||||
@$(MAKE) -s k6-seed-fixtures
|
@$(MAKE) -s k6-seed-fixtures
|
||||||
@echo "tip: 'source $(K6_ENV_FILE)' to load into your shell"
|
@echo "tip: 'source $(K6_ENV_FILE)' to load into your shell"
|
||||||
|
@echo "tip: run 'make dev-restart-gateway' if gateway was already running"
|
||||||
|
|
||||||
# k6-seed-fixtures idempotently upserts the k6-tenant + K6INVITE invite code
|
# k6-seed-fixtures idempotently upserts the k6-tenant + K6INVITE invite code
|
||||||
# into gateway_k6 so /auth/register resolves successfully. The invite hash is
|
# into gateway_k6 so /auth/register resolves successfully. The invite hash is
|
||||||
|
|
@ -253,26 +276,7 @@ DEV_GATEWAY_LOG := $(DEV_DIR)/gateway.log
|
||||||
|
|
||||||
.PHONY: dev-up dev-down dev-status dev-restart-gateway
|
.PHONY: dev-up dev-down dev-status dev-restart-gateway
|
||||||
|
|
||||||
dev-up: k6-up k6-wait k6-build ## 一鍵起全套:mongo/redis/mailhog/zitadel + seed + Gateway 背景
|
dev-up: k6-up k6-wait k6-build dev-restart-gateway ## 一鍵起全套:mongo/redis/mailhog/zitadel + seed + Gateway 背景
|
||||||
@mkdir -p $(DEV_DIR)
|
|
||||||
@if [ -f $(DEV_GATEWAY_PID) ] && kill -0 $$(cat $(DEV_GATEWAY_PID)) 2>/dev/null; then \
|
|
||||||
echo "gateway already running (pid $$(cat $(DEV_GATEWAY_PID)))"; \
|
|
||||||
else \
|
|
||||||
set -a; . $(K6_ENV_FILE); set +a; \
|
|
||||||
nohup $(K6_GATEWAY_BIN) -f $(K6_GATEWAY_CONFIG) > $(DEV_GATEWAY_LOG) 2>&1 & \
|
|
||||||
echo $$! > $(DEV_GATEWAY_PID); \
|
|
||||||
echo "gateway starting (pid $$(cat $(DEV_GATEWAY_PID)), log $(DEV_GATEWAY_LOG))…"; \
|
|
||||||
for i in $$(seq 1 30); do \
|
|
||||||
if curl -fsS http://localhost:8888/api/v1/health >/dev/null 2>&1; then \
|
|
||||||
echo "gateway ready ($$i s)"; break; \
|
|
||||||
fi; \
|
|
||||||
sleep 1; \
|
|
||||||
if [ $$i -eq 30 ]; then \
|
|
||||||
echo "gateway did not become ready — tail $(DEV_GATEWAY_LOG)"; \
|
|
||||||
tail -20 $(DEV_GATEWAY_LOG); exit 1; \
|
|
||||||
fi; \
|
|
||||||
done; \
|
|
||||||
fi
|
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "=========================================="
|
@echo "=========================================="
|
||||||
@echo " 本機測試環境已就緒"
|
@echo " 本機測試環境已就緒"
|
||||||
|
|
@ -283,11 +287,12 @@ dev-up: k6-up k6-wait k6-build ## 一鍵起全套:mongo/redis/mailhog/zitadel
|
||||||
@echo " ZITADEL 主控台 http://localhost:8080/ui/console"
|
@echo " ZITADEL 主控台 http://localhost:8080/ui/console"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " 註冊預設:租戶 k6-tenant · 邀請碼 K6INVITE"
|
@echo " 註冊預設:租戶 k6-tenant · 邀請碼 K6INVITE"
|
||||||
@echo " Email 可填任意地址(例:you@test.com),OTP 不會進真實信箱"
|
@echo " LDAP 登入:alice / Password1!(make k6-wait 已自動設定 ZITADEL IdP)"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " 管理後台:make dev-seed-admin 後用輸出的帳密登入"
|
@echo " 管理後台:make dev-seed-admin 後用輸出的帳密登入"
|
||||||
@echo " 關閉環境:make dev-down"
|
@echo " 關閉環境:make dev-down"
|
||||||
@echo " 查看狀態:make dev-status"
|
@echo " 查看狀態:make dev-status"
|
||||||
|
@echo " OAuth/LDAP 設定變更後:make dev-restart-gateway"
|
||||||
@echo "=========================================="
|
@echo "=========================================="
|
||||||
|
|
||||||
dev-seed-admin: k6-seed-admin ## 建立具 tenant_admin 的管理員(dev-up 之後執行)
|
dev-seed-admin: k6-seed-admin ## 建立具 tenant_admin 的管理員(dev-up 之後執行)
|
||||||
|
|
@ -315,17 +320,29 @@ dev-status: ## 顯示 docker / gateway / health 狀態
|
||||||
echo "not running (run: make dev-up)"; \
|
echo "not running (run: make dev-up)"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
dev-restart-gateway: k6-build ## 只重啟 Gateway(docker 不動)
|
dev-restart-gateway: k6-build ## 重啟 Gateway(載入 k6.env 的 OAuth / LDAP 設定)
|
||||||
|
@if [ ! -s "$(K6_ENV_FILE)" ]; then echo "run 'make k6-wait' first"; exit 1; fi
|
||||||
@if [ -f $(DEV_GATEWAY_PID) ]; then \
|
@if [ -f $(DEV_GATEWAY_PID) ]; then \
|
||||||
pid=$$(cat $(DEV_GATEWAY_PID)); \
|
pid=$$(cat $(DEV_GATEWAY_PID)); \
|
||||||
kill -0 $$pid 2>/dev/null && kill $$pid || true; \
|
kill -0 $$pid 2>/dev/null && kill $$pid || true; \
|
||||||
rm -f $(DEV_GATEWAY_PID); \
|
rm -f $(DEV_GATEWAY_PID); \
|
||||||
fi
|
fi
|
||||||
|
@if command -v lsof >/dev/null 2>&1; then \
|
||||||
|
orphan=$$(lsof -ti :8888 2>/dev/null || true); \
|
||||||
|
if [ -n "$$orphan" ]; then kill $$orphan 2>/dev/null || true; sleep 1; fi; \
|
||||||
|
fi
|
||||||
@mkdir -p $(DEV_DIR)
|
@mkdir -p $(DEV_DIR)
|
||||||
@set -a; . $(K6_ENV_FILE); set +a; \
|
@set -a; . $(K6_ENV_FILE); set +a; \
|
||||||
nohup $(K6_GATEWAY_BIN) -f $(K6_GATEWAY_CONFIG) > $(DEV_GATEWAY_LOG) 2>&1 & \
|
nohup $(K6_GATEWAY_BIN) -f $(K6_GATEWAY_CONFIG) > $(DEV_GATEWAY_LOG) 2>&1 & \
|
||||||
echo $$! > $(DEV_GATEWAY_PID); \
|
echo $$! > $(DEV_GATEWAY_PID); \
|
||||||
echo "gateway restarted (pid $$(cat $(DEV_GATEWAY_PID)))"
|
echo "gateway restarted (pid $$(cat $(DEV_GATEWAY_PID)))"
|
||||||
|
@for i in $$(seq 1 30); do \
|
||||||
|
if curl -fsS http://localhost:8888/api/v1/health >/dev/null 2>&1; then \
|
||||||
|
echo "gateway ready ($$i s)"; exit 0; \
|
||||||
|
fi; \
|
||||||
|
sleep 1; \
|
||||||
|
done; \
|
||||||
|
echo "gateway did not become ready — tail $(DEV_GATEWAY_LOG)"; tail -20 $(DEV_GATEWAY_LOG); exit 1
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Frontend(使用者前台 + 管理後台)
|
# Frontend(使用者前台 + 管理後台)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ Gateway 啟用 **Notification** / **Member OTP** 需要:
|
||||||
| **MongoDB** | `notifications`、`notification_dlq` collections | 27017 |
|
| **MongoDB** | `notifications`、`notification_dlq` collections | 27017 |
|
||||||
| **Redis** | 冪等、配額、異步重試佇列、member OTP challenge | 6379 |
|
| **Redis** | 冪等、配額、異步重試佇列、member OTP challenge | 6379 |
|
||||||
| MailHog(選用) | 本機 SMTP 測試 | 1025 / 8025 |
|
| MailHog(選用) | 本機 SMTP 測試 | 1025 / 8025 |
|
||||||
| OpenLDAP(`make k6-up`) | ZITADEL LDAP IdP 本機目錄 | 389 |
|
| OpenLDAP(`make ldap-up` / `make k6-up`) | ZITADEL LDAP IdP 本機目錄 | 389 |
|
||||||
| ZITADEL(`make k6-up`) | OIDC / Social / LDAP 登入 | 8080 |
|
| ZITADEL(`make k6-up`) | OIDC / Social / LDAP 登入 | 8080 |
|
||||||
|
|
||||||
Mongo **不需要**事先手動建 collection;應用程式寫入時會自動建立。索引由 init script 或 `make mongo-index` 建立。
|
Mongo **不需要**事先手動建 collection;應用程式寫入時會自動建立。索引由 init script 或 `make mongo-index` 建立。
|
||||||
|
|
@ -42,6 +42,7 @@ make run-dev
|
||||||
```bash
|
```bash
|
||||||
make deps-up # docker compose up -d mongo redis
|
make deps-up # docker compose up -d mongo redis
|
||||||
make deps-up-smtp # 再加上 mailhog(profile smtp)
|
make deps-up-smtp # 再加上 mailhog(profile smtp)
|
||||||
|
make ldap-up # 只起 OpenLDAP(profile ldap)
|
||||||
make k6-up # 全棧含 OpenLDAP + ZITADEL(見 deploy/zitadel、deploy/openldap README)
|
make k6-up # 全棧含 OpenLDAP + ZITADEL(見 deploy/zitadel、deploy/openldap README)
|
||||||
make ldap-test # 確認 LDAP 測試帳號 alice/bob
|
make ldap-test # 確認 LDAP 測試帳號 alice/bob
|
||||||
make deps-down # 停止並移除容器(保留 volume)
|
make deps-down # 停止並移除容器(保留 volume)
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,14 @@
|
||||||
# 啟動:
|
# 啟動:
|
||||||
# make deps-up → mongo + redis(最小,吃 etc/gateway.dev.yaml)
|
# make deps-up → mongo + redis(最小,吃 etc/gateway.dev.yaml)
|
||||||
# make deps-up-smtp → + mailhog(profile smtp)
|
# make deps-up-smtp → + mailhog(profile smtp)
|
||||||
# make k6-up → mongo + redis + mailhog + postgres + zitadel(吃 etc/gateway.k6.yaml)
|
# make ldap-up → 只起 OpenLDAP(profile ldap,見 deploy/openldap/)
|
||||||
|
# make k6-up → mongo + redis + mailhog + postgres + openldap + zitadel
|
||||||
#
|
#
|
||||||
# ZITADEL admin PAT 會寫到 deploy/zitadel/machinekey/zitadel-admin-sa.token
|
# ZITADEL admin PAT 會寫到 deploy/zitadel/machinekey/zitadel-admin-sa.token
|
||||||
|
|
||||||
|
include:
|
||||||
|
- path: openldap/docker-compose.yml
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mongo:
|
mongo:
|
||||||
image: mongo:7
|
image: mongo:7
|
||||||
|
|
@ -70,47 +74,6 @@ services:
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 20
|
retries: 20
|
||||||
|
|
||||||
openldap:
|
|
||||||
profiles: ["k6"]
|
|
||||||
image: osixia/openldap:1.5.0
|
|
||||||
container_name: gateway-openldap
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
LDAP_ORGANISATION: "GatewayDev"
|
|
||||||
LDAP_DOMAIN: "gateway.local"
|
|
||||||
LDAP_ADMIN_PASSWORD: "admin"
|
|
||||||
LDAP_CONFIG_PASSWORD: "config"
|
|
||||||
LDAP_TLS: "false"
|
|
||||||
ports:
|
|
||||||
- "389:389"
|
|
||||||
volumes:
|
|
||||||
- openldap_data:/var/lib/ldap
|
|
||||||
- openldap_config:/etc/ldap/slapd.d
|
|
||||||
healthcheck:
|
|
||||||
test:
|
|
||||||
[
|
|
||||||
"CMD",
|
|
||||||
"ldapsearch",
|
|
||||||
"-x",
|
|
||||||
"-H",
|
|
||||||
"ldap://localhost",
|
|
||||||
"-b",
|
|
||||||
"dc=gateway,dc=local",
|
|
||||||
"-D",
|
|
||||||
"cn=admin,dc=gateway,dc=local",
|
|
||||||
"-w",
|
|
||||||
"admin",
|
|
||||||
"-LLL",
|
|
||||||
"-s",
|
|
||||||
"base",
|
|
||||||
"(objectClass=*)",
|
|
||||||
"dn",
|
|
||||||
]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 24
|
|
||||||
start_period: 20s
|
|
||||||
|
|
||||||
zitadel:
|
zitadel:
|
||||||
profiles: ["k6"]
|
profiles: ["k6"]
|
||||||
image: ghcr.io/zitadel/zitadel:v2.65.0
|
image: ghcr.io/zitadel/zitadel:v2.65.0
|
||||||
|
|
@ -141,5 +104,3 @@ volumes:
|
||||||
mongo_data:
|
mongo_data:
|
||||||
redis_data:
|
redis_data:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
openldap_data:
|
|
||||||
openldap_config:
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,27 @@
|
||||||
# 本機 OpenLDAP(k6 / dev)
|
# 本機 OpenLDAP(k6 / dev)
|
||||||
|
|
||||||
與 `make k6-up` 一併啟動的測試用 LDAP 目錄。ZITADEL 透過 **LDAP IdP** 連到這台伺服器;Gateway 仍只走 OIDC(`provider=ldap` + `LdapIdPID`)。
|
與 `make k6-up` 或 `make ldap-up` 一併啟動的測試用 LDAP 目錄。ZITADEL 透過 **LDAP IdP** 連到這台伺服器;Gateway 仍只走 OIDC(`provider=ldap` + `LdapIdPID`)。
|
||||||
|
|
||||||
|
Compose 定義在 [docker-compose.yml](docker-compose.yml),由主 [deploy/docker-compose.yml](../docker-compose.yml) `include` 進來。
|
||||||
|
|
||||||
## 啟動
|
## 啟動
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make k6-up # 含 openldap
|
# 方式 A:只起 LDAP(已有 ZITADEL 或其他服務時)
|
||||||
make k6-wait # 等 OpenLDAP seed + ZITADEL ready
|
make ldap-up
|
||||||
|
make ldap-wait
|
||||||
|
|
||||||
|
# 方式 B:k6 全棧(含 Postgres + ZITADEL + OpenLDAP)
|
||||||
|
make k6-up
|
||||||
|
make k6-wait # 自動:LDAP seed + ZITADEL LDAP IdP + OIDC App → k6.env
|
||||||
|
|
||||||
|
# 一鍵(含 Gateway 背景)
|
||||||
|
make dev-up
|
||||||
|
|
||||||
make ldap-test # 確認 alice / bob 可查
|
make ldap-test # 確認 alice / bob 可查
|
||||||
```
|
```
|
||||||
|
|
||||||
`make k6-wait` 會自動執行 `ldap-seed`,把 [bootstrap/10-people.ldif](bootstrap/10-people.ldif) 寫入目錄(可重複執行)。
|
`make k6-wait` 會執行 `ldap-seed`,並跑 [`deploy/zitadel/bootstrap_dev.py`](../zitadel/bootstrap_dev.py) 自動建立 ZITADEL **LDAP IdP** 與 **OIDC Web App**,把 `ZITADEL_LDAP_IDP_ID` / `ZITADEL_OAUTH_CLIENT_*` 寫入 `deploy/zitadel/machinekey/k6.env`。**不必再手動開 Console。**
|
||||||
|
|
||||||
## 連線資訊
|
## 連線資訊
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
# 本機 OpenLDAP(LDAP + ZITADEL 整合測試)
|
||||||
|
#
|
||||||
|
# 獨立啟動(只跑 LDAP):
|
||||||
|
# docker compose -f deploy/openldap/docker-compose.yml up -d
|
||||||
|
# make ldap-wait # 等 ready 並 seed alice/bob
|
||||||
|
#
|
||||||
|
# 或由主 compose 以 profile 啟動:
|
||||||
|
# make ldap-up # profile ldap
|
||||||
|
# make k6-up # profile k6(含 ZITADEL + Postgres + OpenLDAP)
|
||||||
|
#
|
||||||
|
# ZITADEL Console 建 LDAP IdP 時,Server 填 ldap://openldap:389(容器網路)
|
||||||
|
# 本機 ldapsearch 用 localhost:389
|
||||||
|
|
||||||
|
services:
|
||||||
|
openldap:
|
||||||
|
profiles: ["ldap", "k6"]
|
||||||
|
image: osixia/openldap:1.5.0
|
||||||
|
container_name: gateway-openldap
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
LDAP_ORGANISATION: "GatewayDev"
|
||||||
|
LDAP_DOMAIN: "gateway.local"
|
||||||
|
LDAP_ADMIN_PASSWORD: "admin"
|
||||||
|
LDAP_CONFIG_PASSWORD: "config"
|
||||||
|
LDAP_TLS: "false"
|
||||||
|
ports:
|
||||||
|
- "389:389"
|
||||||
|
volumes:
|
||||||
|
- openldap_data:/var/lib/ldap
|
||||||
|
- openldap_config:/etc/ldap/slapd.d
|
||||||
|
# 測試帳號由 make ldap-seed 寫入(勿 :ro 掛 bootstrap,osixia 啟動需 chown 會失敗)
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"ldapsearch",
|
||||||
|
"-x",
|
||||||
|
"-H",
|
||||||
|
"ldap://localhost",
|
||||||
|
"-b",
|
||||||
|
"dc=gateway,dc=local",
|
||||||
|
"-D",
|
||||||
|
"cn=admin,dc=gateway,dc=local",
|
||||||
|
"-w",
|
||||||
|
"admin",
|
||||||
|
"-LLL",
|
||||||
|
"-s",
|
||||||
|
"base",
|
||||||
|
"(objectClass=*)",
|
||||||
|
"dn",
|
||||||
|
]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 24
|
||||||
|
start_period: 20s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
openldap_data:
|
||||||
|
openldap_config:
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
# ZITADEL bootstrap outputs(PAT / machine key)— 不入 git
|
# ZITADEL bootstrap outputs(PAT / machine key)— 不入 git
|
||||||
machinekey/zitadel-admin-sa.token
|
machinekey/zitadel-admin-sa.token
|
||||||
machinekey/zitadel-admin-sa.json
|
machinekey/zitadel-admin-sa.json
|
||||||
|
machinekey/dev-bootstrap.env
|
||||||
|
machinekey/k6.env
|
||||||
|
machinekey/k6.env.admin
|
||||||
|
machinekey/k6.env.tmp
|
||||||
|
|
|
||||||
|
|
@ -88,24 +88,9 @@ Zitadel:
|
||||||
|
|
||||||
### 3. 設定 LDAP IdP(本機 OpenLDAP)
|
### 3. 設定 LDAP IdP(本機 OpenLDAP)
|
||||||
|
|
||||||
`make k6-up` 已含測試目錄,帳號與 ZITADEL 欄位對照見 **[deploy/openldap/README.md](../openldap/README.md)**。
|
`make k6-wait` 會**自動**建立 LDAP IdP 與 OIDC App([`bootstrap_dev.py`](bootstrap_dev.py)),並把 IdP ID 寫入 `machinekey/k6.env` 的 `ZITADEL_LDAP_IDP_ID`。
|
||||||
|
|
||||||
摘要(在 ZITADEL Console 建 Generic LDAP IdP):
|
手動設定(選用)見 **[deploy/openldap/README.md](../openldap/README.md)**。
|
||||||
|
|
||||||
| 欄位 | 值 |
|
|
||||||
|------|-----|
|
|
||||||
| Server | `ldap://openldap:389` |
|
|
||||||
| Bind DN | `cn=admin,dc=gateway,dc=local` |
|
|
||||||
| Bind password | `admin` |
|
|
||||||
| User base | `ou=people,dc=gateway,dc=local` |
|
|
||||||
| Login | uid=`alice`,密碼=`Password1!` |
|
|
||||||
|
|
||||||
建立後複製 **IdP ID** → `Zitadel.LdapIdPID`,並確認已設定 OIDC App(§1)。
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
Zitadel:
|
|
||||||
LdapIdPID: "<zitadel-ldap-idp-id>"
|
|
||||||
```
|
|
||||||
|
|
||||||
驗證目錄:`make ldap-test`
|
驗證目錄:`make ldap-test`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,315 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Idempotent dev bootstrap: OpenLDAP IdP + OIDC Web app for Gateway (k6 / dev-up).
|
||||||
|
|
||||||
|
Writes deploy/zitadel/machinekey/dev-bootstrap.env with:
|
||||||
|
ZITADEL_DEFAULT_ORG_ID, ZITADEL_OAUTH_CLIENT_ID, ZITADEL_OAUTH_CLIENT_SECRET,
|
||||||
|
ZITADEL_LDAP_IDP_ID, ZITADEL_GOOGLE_IDP_ID (empty)
|
||||||
|
|
||||||
|
Requires: ZITADEL up, PAT at deploy/zitadel/machinekey/zitadel-admin-sa.token
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
PAT_FILE = ROOT / "deploy/zitadel/machinekey/zitadel-admin-sa.token"
|
||||||
|
OUT_FILE = ROOT / "deploy/zitadel/machinekey/dev-bootstrap.env"
|
||||||
|
|
||||||
|
ZITADEL_BASE = os.environ.get("ZITADEL_BASE", "http://localhost:8080").rstrip("/")
|
||||||
|
|
||||||
|
LDAP_IDP_NAME = "GatewayDevLDAP"
|
||||||
|
PROJECT_NAME = "Gateway"
|
||||||
|
APP_NAME = "Gateway Backend"
|
||||||
|
REDIRECT_URIS = [
|
||||||
|
"http://localhost:5173/auth/callback/login",
|
||||||
|
"http://localhost:5173/auth/callback/register",
|
||||||
|
]
|
||||||
|
POST_LOGOUT_URIS = ["http://localhost:5173/"]
|
||||||
|
|
||||||
|
LDAP_BODY = {
|
||||||
|
"name": LDAP_IDP_NAME,
|
||||||
|
"servers": ["ldap://openldap:389"],
|
||||||
|
"startTls": False,
|
||||||
|
"baseDn": "dc=gateway,dc=local",
|
||||||
|
"bindDn": "cn=admin,dc=gateway,dc=local",
|
||||||
|
"bindPassword": "admin",
|
||||||
|
"userBase": "ou=people,dc=gateway,dc=local",
|
||||||
|
"userObjectClasses": ["inetOrgPerson"],
|
||||||
|
"userFilters": ["(uid=%s)"],
|
||||||
|
"attributes": {
|
||||||
|
"idAttribute": "uid",
|
||||||
|
"emailAttribute": "mail",
|
||||||
|
"firstNameAttribute": "givenName",
|
||||||
|
"lastNameAttribute": "sn",
|
||||||
|
"displayNameAttribute": "cn",
|
||||||
|
"nickNameAttribute": "uid",
|
||||||
|
},
|
||||||
|
"creationAllowed": True,
|
||||||
|
"linkingAllowed": True,
|
||||||
|
"autoCreation": True,
|
||||||
|
"autoUpdate": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BootstrapError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg: str) -> None:
|
||||||
|
print(f"[zitadel-bootstrap] {msg}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def api(method: str, path: str, body: dict | None = None) -> dict:
|
||||||
|
url = f"{ZITADEL_BASE}{path}"
|
||||||
|
data = None if body is None else json.dumps(body).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
data=data,
|
||||||
|
method=method,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {read_pat()}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||||
|
raw = resp.read().decode()
|
||||||
|
if not raw.strip():
|
||||||
|
return {}
|
||||||
|
return json.loads(raw)
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
detail = e.read().decode(errors="replace")
|
||||||
|
raise BootstrapError(f"{method} {path} -> HTTP {e.code}: {detail}") from e
|
||||||
|
|
||||||
|
|
||||||
|
def read_pat() -> str:
|
||||||
|
if not PAT_FILE.is_file():
|
||||||
|
raise BootstrapError(f"PAT missing: {PAT_FILE} (run make k6-wait)")
|
||||||
|
pat = PAT_FILE.read_text().strip()
|
||||||
|
if not pat:
|
||||||
|
raise BootstrapError(f"PAT empty: {PAT_FILE}")
|
||||||
|
return pat
|
||||||
|
|
||||||
|
|
||||||
|
def load_saved() -> dict[str, str]:
|
||||||
|
if not OUT_FILE.is_file():
|
||||||
|
return {}
|
||||||
|
out: dict[str, str] = {}
|
||||||
|
for line in OUT_FILE.read_text().splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#") or "=" not in line:
|
||||||
|
continue
|
||||||
|
k, _, v = line.partition("=")
|
||||||
|
if k.startswith("export "):
|
||||||
|
k = k[len("export ") :]
|
||||||
|
out[k.strip()] = v.strip().strip('"').strip("'")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def write_env(org_id: str, client_id: str, client_secret: str, ldap_idp_id: str) -> None:
|
||||||
|
OUT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
content = f"""# Auto-generated by deploy/zitadel/bootstrap_dev.py — do not commit
|
||||||
|
export ZITADEL_DEFAULT_ORG_ID={org_id}
|
||||||
|
export ZITADEL_OAUTH_CLIENT_ID={client_id}
|
||||||
|
export ZITADEL_OAUTH_CLIENT_SECRET={client_secret}
|
||||||
|
export ZITADEL_LDAP_IDP_ID={ldap_idp_id}
|
||||||
|
export ZITADEL_GOOGLE_IDP_ID=
|
||||||
|
"""
|
||||||
|
OUT_FILE.write_text(content)
|
||||||
|
log(f"wrote {OUT_FILE.relative_to(ROOT)}")
|
||||||
|
|
||||||
|
|
||||||
|
def org_id() -> str:
|
||||||
|
data = api("GET", "/management/v1/orgs/me")
|
||||||
|
oid = (data.get("org") or {}).get("id") or ""
|
||||||
|
if not oid:
|
||||||
|
raise BootstrapError("could not resolve org id")
|
||||||
|
return oid
|
||||||
|
|
||||||
|
|
||||||
|
def login_policy() -> tuple[dict, bool]:
|
||||||
|
data = api("GET", "/management/v1/policies/login")
|
||||||
|
return data.get("policy") or {}, bool(data.get("isDefault"))
|
||||||
|
|
||||||
|
|
||||||
|
def find_ldap_idp_in_policy(policy: dict) -> str:
|
||||||
|
for item in policy.get("idps") or []:
|
||||||
|
if item.get("idpName") == LDAP_IDP_NAME:
|
||||||
|
return item.get("idpId") or ""
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_ldap_idp() -> str:
|
||||||
|
policy, is_default = login_policy()
|
||||||
|
existing = find_ldap_idp_in_policy(policy)
|
||||||
|
if existing:
|
||||||
|
log(f"LDAP IdP already linked: {existing}")
|
||||||
|
return existing
|
||||||
|
|
||||||
|
created = api("POST", "/management/v1/idps/ldap", LDAP_BODY)
|
||||||
|
idp_id = created.get("id") or ""
|
||||||
|
if not idp_id:
|
||||||
|
raise BootstrapError(f"create LDAP IdP: unexpected response {created}")
|
||||||
|
|
||||||
|
log(f"created LDAP IdP {idp_id}")
|
||||||
|
|
||||||
|
if is_default or not policy.get("allowExternalIdp"):
|
||||||
|
api(
|
||||||
|
"POST",
|
||||||
|
"/management/v1/policies/login",
|
||||||
|
{
|
||||||
|
"allowExternalIdp": True,
|
||||||
|
"allowUsernamePassword": True,
|
||||||
|
"allowRegister": True,
|
||||||
|
"passwordlessType": "PASSWORDLESS_TYPE_ALLOWED",
|
||||||
|
"idps": [{"idpId": idp_id, "ownerType": "IDP_OWNER_TYPE_ORG"}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
log("created org login policy with LDAP IdP")
|
||||||
|
else:
|
||||||
|
api("POST", "/management/v1/policies/login/idps", {"idpId": idp_id})
|
||||||
|
log("linked LDAP IdP to existing login policy")
|
||||||
|
|
||||||
|
return idp_id
|
||||||
|
|
||||||
|
|
||||||
|
def find_project() -> str:
|
||||||
|
data = api(
|
||||||
|
"POST",
|
||||||
|
"/management/v1/projects/_search",
|
||||||
|
{
|
||||||
|
"queries": [
|
||||||
|
{
|
||||||
|
"nameQuery": {
|
||||||
|
"name": PROJECT_NAME,
|
||||||
|
"method": "TEXT_QUERY_METHOD_EQUALS",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
for item in data.get("result") or []:
|
||||||
|
if item.get("name") == PROJECT_NAME:
|
||||||
|
return item.get("id") or ""
|
||||||
|
created = api(
|
||||||
|
"POST",
|
||||||
|
"/management/v1/projects",
|
||||||
|
{
|
||||||
|
"name": PROJECT_NAME,
|
||||||
|
"projectRoleAssertion": True,
|
||||||
|
"projectRoleCheck": False,
|
||||||
|
"hasProjectCheck": False,
|
||||||
|
"privateLabelingSetting": "PRIVATE_LABELING_SETTING_UNSPECIFIED",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
pid = created.get("id") or ""
|
||||||
|
if not pid:
|
||||||
|
raise BootstrapError(f"create project: unexpected response {created}")
|
||||||
|
log(f"created project {PROJECT_NAME} ({pid})")
|
||||||
|
return pid
|
||||||
|
|
||||||
|
|
||||||
|
def find_app(project_id: str) -> tuple[str, str]:
|
||||||
|
data = api(
|
||||||
|
"POST",
|
||||||
|
f"/management/v1/projects/{project_id}/apps/_search",
|
||||||
|
{
|
||||||
|
"queries": [
|
||||||
|
{
|
||||||
|
"nameQuery": {
|
||||||
|
"name": APP_NAME,
|
||||||
|
"method": "TEXT_QUERY_METHOD_EQUALS",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
for item in data.get("result") or []:
|
||||||
|
if item.get("name") != APP_NAME:
|
||||||
|
continue
|
||||||
|
app_id = item.get("id") or ""
|
||||||
|
client_id = (item.get("oidcConfig") or {}).get("clientId") or ""
|
||||||
|
return app_id, client_id
|
||||||
|
return "", ""
|
||||||
|
|
||||||
|
|
||||||
|
def create_app(project_id: str) -> tuple[str, str, str]:
|
||||||
|
created = api(
|
||||||
|
"POST",
|
||||||
|
f"/management/v1/projects/{project_id}/apps/oidc",
|
||||||
|
{
|
||||||
|
"name": APP_NAME,
|
||||||
|
"redirectUris": REDIRECT_URIS,
|
||||||
|
"responseTypes": ["OIDC_RESPONSE_TYPE_CODE"],
|
||||||
|
"grantTypes": [
|
||||||
|
"OIDC_GRANT_TYPE_AUTHORIZATION_CODE",
|
||||||
|
"OIDC_GRANT_TYPE_REFRESH_TOKEN",
|
||||||
|
],
|
||||||
|
"appType": "OIDC_APP_TYPE_WEB",
|
||||||
|
"authMethodType": "OIDC_AUTH_METHOD_TYPE_BASIC",
|
||||||
|
"postLogoutRedirectUris": POST_LOGOUT_URIS,
|
||||||
|
"devMode": True,
|
||||||
|
"accessTokenType": "OIDC_TOKEN_TYPE_BEARER",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
app_id = created.get("appId") or ""
|
||||||
|
client_id = created.get("clientId") or ""
|
||||||
|
client_secret = created.get("clientSecret") or ""
|
||||||
|
if not app_id or not client_id or not client_secret:
|
||||||
|
raise BootstrapError(f"create OIDC app: unexpected response {created}")
|
||||||
|
log(f"created OIDC app {APP_NAME} client_id={client_id}")
|
||||||
|
return app_id, client_id, client_secret
|
||||||
|
|
||||||
|
|
||||||
|
def regenerate_secret(project_id: str, app_id: str) -> str:
|
||||||
|
data = api(
|
||||||
|
"POST",
|
||||||
|
f"/management/v1/projects/{project_id}/apps/{app_id}/oidc_config/_generate_client_secret",
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
secret = data.get("clientSecret") or ""
|
||||||
|
if not secret:
|
||||||
|
raise BootstrapError(f"regenerate client secret: unexpected response {data}")
|
||||||
|
log("regenerated OIDC client secret")
|
||||||
|
return secret
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_oidc_app(saved: dict[str, str]) -> tuple[str, str]:
|
||||||
|
project_id = find_project()
|
||||||
|
app_id, client_id = find_app(project_id)
|
||||||
|
if not app_id:
|
||||||
|
app_id, client_id, client_secret = create_app(project_id)
|
||||||
|
return client_id, client_secret
|
||||||
|
|
||||||
|
log(f"OIDC app exists client_id={client_id}")
|
||||||
|
saved_id = saved.get("ZITADEL_OAUTH_CLIENT_ID", "")
|
||||||
|
saved_secret = saved.get("ZITADEL_OAUTH_CLIENT_SECRET", "")
|
||||||
|
if saved_id == client_id and saved_secret:
|
||||||
|
return client_id, saved_secret
|
||||||
|
|
||||||
|
return client_id, regenerate_secret(project_id, app_id)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
try:
|
||||||
|
oid = org_id()
|
||||||
|
ldap_idp_id = ensure_ldap_idp()
|
||||||
|
saved = load_saved()
|
||||||
|
client_id, client_secret = ensure_oidc_app(saved)
|
||||||
|
write_env(oid, client_id, client_secret, ldap_idp_id)
|
||||||
|
print(f"LDAP IdP={ldap_idp_id} OAuth client={client_id}")
|
||||||
|
return 0
|
||||||
|
except BootstrapError as e:
|
||||||
|
log(f"error: {e}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export ZITADEL_SERVICE_TOKEN=j2uy--q988R_b4kaB0-kCsWe-NpzpejaiV66AYT5g1pRvLSJff4Mke833P0XkTJtmWt6iLM
|
|
||||||
export BASE_URL=http://localhost:8888
|
|
||||||
export MAILHOG_URL=http://localhost:8025
|
|
||||||
export REDIS_ADDR=localhost:6379
|
|
||||||
|
|
@ -35,7 +35,7 @@ DefaultInstance:
|
||||||
LoginPolicy:
|
LoginPolicy:
|
||||||
AllowRegister: true
|
AllowRegister: true
|
||||||
AllowUsernamePassword: true
|
AllowUsernamePassword: true
|
||||||
AllowExternalIDP: false
|
AllowExternalIDP: true
|
||||||
ForceMFA: false
|
ForceMFA: false
|
||||||
HidePasswordReset: false
|
HidePasswordReset: false
|
||||||
PasswordlessType: 1
|
PasswordlessType: 1
|
||||||
|
|
|
||||||
|
|
@ -107,11 +107,11 @@ Zitadel:
|
||||||
APIBase: http://localhost:8080
|
APIBase: http://localhost:8080
|
||||||
ServiceUserToken: ""
|
ServiceUserToken: ""
|
||||||
DefaultOrgID: ""
|
DefaultOrgID: ""
|
||||||
OAuthClientID: ""
|
OAuthClientID: "374875801008562439"
|
||||||
OAuthClientSecret: ""
|
OAuthClientSecret: "Z1SUCIsozer52x2DNhKHXcTZROicf2lFFLLr4pkTZjLXHfkunTzjKYmMk2EHKDch"
|
||||||
GoogleClientID: ""
|
GoogleClientID: ""
|
||||||
GoogleClientSecret: ""
|
GoogleClientSecret: ""
|
||||||
GoogleIdPID: ""
|
GoogleIdPID: ""
|
||||||
LdapIdPID: ""
|
LdapIdPID: "374875427463782663"
|
||||||
JWKSUrl: ""
|
JWKSUrl: ""
|
||||||
TimeoutSeconds: 15
|
TimeoutSeconds: 15
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ type Conf struct {
|
||||||
// ServiceUserToken is a PAT or service-account token for Management API (CreateUser, Deactivate).
|
// ServiceUserToken is a PAT or service-account token for Management API (CreateUser, Deactivate).
|
||||||
ServiceUserToken string `json:",optional,env=ZITADEL_SERVICE_TOKEN"`
|
ServiceUserToken string `json:",optional,env=ZITADEL_SERVICE_TOKEN"`
|
||||||
// DefaultOrgID is used when CreateHumanUserRequest.OrgID is empty.
|
// DefaultOrgID is used when CreateHumanUserRequest.OrgID is empty.
|
||||||
DefaultOrgID string `json:",optional"`
|
DefaultOrgID string `json:",optional,env=ZITADEL_DEFAULT_ORG_ID"`
|
||||||
// OAuthClientID and OAuthClientSecret identify the Gateway OIDC application (password grant / social).
|
// OAuthClientID and OAuthClientSecret identify the Gateway OIDC application (password grant / social).
|
||||||
OAuthClientID string `json:",optional"`
|
OAuthClientID string `json:",optional,env=ZITADEL_OAUTH_CLIENT_ID"`
|
||||||
OAuthClientSecret string `json:",optional,env=ZITADEL_OAUTH_CLIENT_SECRET"`
|
OAuthClientSecret string `json:",optional,env=ZITADEL_OAUTH_CLIENT_SECRET"`
|
||||||
// Google OAuth app credentials (register/social flow, PR 6).
|
// Google OAuth app credentials (register/social flow, PR 6).
|
||||||
GoogleClientID string `json:",optional"`
|
GoogleClientID string `json:",optional"`
|
||||||
|
|
@ -21,7 +21,7 @@ type Conf struct {
|
||||||
// GoogleIdPID is the ZITADEL external IdP id for Google (optional idp_id authorize hint).
|
// GoogleIdPID is the ZITADEL external IdP id for Google (optional idp_id authorize hint).
|
||||||
GoogleIdPID string `json:",optional"`
|
GoogleIdPID string `json:",optional"`
|
||||||
// LdapIdPID is the ZITADEL external IdP id for LDAP (optional idp_id authorize hint).
|
// LdapIdPID is the ZITADEL external IdP id for LDAP (optional idp_id authorize hint).
|
||||||
LdapIdPID string `json:",optional"`
|
LdapIdPID string `json:",optional,env=ZITADEL_LDAP_IDP_ID"`
|
||||||
// JWKSUrl overrides OIDC JWKS endpoint; defaults to {Issuer}/oauth/v2/keys.
|
// JWKSUrl overrides OIDC JWKS endpoint; defaults to {Issuer}/oauth/v2/keys.
|
||||||
JWKSUrl string `json:",optional"`
|
JWKSUrl string `json:",optional"`
|
||||||
TimeoutSeconds int `json:",optional"`
|
TimeoutSeconds int `json:",optional"`
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue