From 1341f07ef92df485c82c1b02ea6ae729c335ac1f Mon Sep 17 00:00:00 2001 From: Lectom Date: Tue, 9 Jun 2026 21:03:05 +0900 Subject: [PATCH] chore: consolidate local integration changes --- .gitea/workflows/build_RC.yml | 3 +- .gitea/workflows/code_check.yml | 1 + .gitea/workflows/staging_code_pull.yml | 31 +- .gitea/workflows/staging_release.yml | 6 + .../workflows/userfront_e2e_full_nightly.yml | 1 + README.md | 53 + adminfront/scripts/runtime-mode.sh | 11 +- adminfront/seed-tenant.csv | 1 + adminfront/src/app/routes.test.tsx | 16 +- adminfront/src/app/routes.tsx | 4 +- .../components/common/LanguageSelector.tsx | 2 + .../src/components/layout/AppLayout.test.tsx | 4 +- .../src/components/layout/AppLayout.tsx | 6 +- .../src/components/ui/checkbox.test.tsx | 19 + adminfront/src/components/ui/checkbox.tsx | 7 +- adminfront/src/components/ui/input.test.tsx | 14 + adminfront/src/components/ui/input.tsx | 7 +- .../src/components/ui/textarea.test.tsx | 19 + adminfront/src/components/ui/textarea.tsx | 7 +- .../src/features/audit/AuditLogsPage.tsx | 2 + .../auth/components/PermissionChecker.tsx | 2 + .../coverage/adminTenantTabs.test.tsx | 36 +- .../integrity/DataIntegrityPage.test.tsx | 66 +- .../features/integrity/DataIntegrityPage.tsx | 3 +- .../projections/UserProjectionPage.test.tsx | 84 +- .../projections/UserProjectionPage.tsx | 260 +-- .../components/ParentTenantSelector.tsx | 2 + .../tenants/routes/TenantListPage.test.ts | 18 + .../tenants/routes/TenantListPage.tsx | 23 + .../tenants/routes/TenantProfilePage.tsx | 4 + .../tenants/routes/TenantSchemaPage.tsx | 7 + .../routes/TenantUsersPage.export.test.tsx | 148 ++ .../tenants/routes/TenantUsersPage.tsx | 289 ++- .../routes/TenantWorksmobilePage.test.ts | 37 + .../tenants/routes/TenantWorksmobilePage.tsx | 651 +++---- .../features/tenants/routes/tenantListView.ts | 13 +- .../tenants/routes/worksmobileComparison.ts | 32 + .../routes/TenantUserGroupsTab.tsx | 33 + .../features/users/GlobalCustomClaimsPage.tsx | 317 +++ .../src/features/users/UserCreatePage.tsx | 2 + .../UserDetailPage.employeeNumber.test.tsx | 54 + .../src/features/users/UserDetailPage.tsx | 279 ++- .../users/UserListPage.render.test.tsx | 31 +- .../src/features/users/UserListPage.tsx | 74 +- .../components/UserBulkMoveGroupModal.tsx | 4 + .../users/components/UserBulkUploadModal.tsx | 7 + adminfront/src/lib/adminApi.contract.test.ts | 14 +- adminfront/src/lib/adminApi.ts | 91 +- adminfront/src/lib/tenantTree.test.ts | 44 + adminfront/src/lib/tenantTree.ts | 7 +- adminfront/src/locales/en.toml | 64 +- adminfront/src/locales/ko.toml | 61 +- adminfront/src/locales/template.toml | 39 +- .../src/test/formFieldDiagnostics.test.ts | 40 + adminfront/src/test/formFieldDiagnostics.ts | 26 + adminfront/src/test/i18nMock.ts | 100 +- adminfront/tests/security_roles.spec.ts | 4 +- adminfront/tests/tenants.spec.ts | 158 +- adminfront/tests/users.spec.ts | 179 ++ adminfront/tests/worksmobile.spec.ts | 296 ++- backend/cmd/adminctl/main.go | 6 + backend/cmd/adminctl/main_test.go | 70 +- backend/cmd/adminctl/worksmobile_sync.go | 1709 +++++++++++++++++ backend/cmd/adminctl/worksmobile_sync_test.go | 38 + backend/cmd/fix_kratos_roles.go | 22 +- backend/cmd/server/main.go | 18 +- backend/internal/bootstrap/bootstrap.go | 1 + backend/internal/domain/identity_cache.go | 19 + backend/internal/domain/system_setting.go | 11 + backend/internal/domain/user.go | 48 +- backend/internal/domain/user_validate_test.go | 23 + backend/internal/handler/admin_handler.go | 230 ++- .../internal/handler/admin_handler_test.go | 95 +- backend/internal/handler/auth_handler.go | 107 +- .../handler/auth_handler_client_test.go | 15 + .../auth_handler_dynamic_claims_test.go | 25 + .../auth_handler_profile_cache_test.go | 32 + backend/internal/handler/common_test.go | 12 +- backend/internal/handler/dev_handler.go | 270 ++- .../handler/dev_handler_rp_metadata_test.go | 188 +- backend/internal/handler/dev_handler_test.go | 151 +- backend/internal/handler/tenant_handler.go | 297 ++- .../internal/handler/tenant_handler_test.go | 171 ++ backend/internal/handler/user_handler.go | 217 ++- backend/internal/handler/user_handler_test.go | 266 ++- .../internal/handler/worksmobile_handler.go | 29 +- .../handler/worksmobile_handler_test.go | 7 +- .../repository/user_projection_repository.go | 62 +- .../user_projection_repository_test.go | 95 +- .../internal/repository/user_repository.go | 61 +- .../repository/user_repository_test.go | 54 + .../internal/service/kratos_admin_service.go | 88 +- .../service/kratos_admin_service_test.go | 63 + backend/internal/service/redis_service.go | 146 ++ .../internal/service/redis_service_test.go | 150 ++ .../internal/service/user_group_service.go | 43 +- .../service/user_group_service_test.go | 36 +- .../service/user_projection_sync_service.go | 2 +- .../user_projection_sync_service_test.go | 31 + .../internal/service/worksmobile_client.go | 190 +- .../service/worksmobile_client_test.go | 197 ++ .../service/worksmobile_live_flow_test.go | 2 +- .../internal/service/worksmobile_mapper.go | 129 +- .../service/worksmobile_mapper_test.go | 211 +- .../service/worksmobile_relay_leader_lock.go | 79 + .../service/worksmobile_relay_worker.go | 50 +- .../service/worksmobile_sync_service.go | 55 +- .../service/worksmobile_sync_service_test.go | 140 +- deploy/templates/docker-compose.yaml | 5 +- devfront/scripts/runtime-mode.sh | 11 +- .../features/clients/ClientConsentsPage.tsx | 508 ++++- .../clients/ClientDetailTabs.test.tsx | 29 + .../src/features/clients/ClientDetailTabs.tsx | 12 +- .../features/clients/ClientGeneralPage.tsx | 241 ++- .../components/AllowedTenantBadge.test.tsx | 82 + .../clients/components/AllowedTenantBadge.tsx | 59 + .../src/features/coverage/pageSmoke.test.tsx | 24 + devfront/src/lib/devApi.test.ts | 11 + devfront/src/lib/devApi.ts | 26 + devfront/src/locales/en.toml | 6 +- devfront/src/locales/ko.toml | 6 +- .../devfront-client-tenant-access.spec.ts | 124 ++ .../tests/devfront-clients-lifecycle.spec.ts | 73 +- devfront/tests/devfront-consents.spec.ts | 59 +- devfront/tests/helpers/devfront-fixtures.ts | 63 +- docker-compose.yaml | 4 + docker/docker-compose.staging.template.yaml | 1 + docker/staging_pull_compose.template.yaml | 2 + ...identity-redis-mirror-policy-2026-06-09.md | 245 +++ ...-502-restart-policy-incident-2026-06-08.md | 80 + ...-projection-visibility-audit-2026-06-08.md | 123 ++ docs/works-only-user-recovery-2026-06-09.md | 112 ++ ...mobile-api-rate-limit-policy-2026-06-09.md | 64 + orgfront/scripts/runtime-mode.sh | 11 +- .../features/orgchart/rankPriority.test.ts | 14 + .../src/features/orgchart/rankPriority.ts | 3 + .../orgchart/routes/OrgChartPage.test.tsx | 44 +- .../features/orgchart/routes/OrgChartPage.tsx | 123 +- orgfront/src/lib/adminApi.test.ts | 4 + orgfront/src/lib/adminApi.ts | 21 + orgfront/tests/orgchart-pan-zoom.spec.ts | 118 +- orgfront/tests/orgchart-vector-render.spec.ts | 73 +- scripts/backup/lib/postgres.sh | 4 + scripts/backup/lib/report.sh | 6 + scripts/clear_orphan_tenant_memberships.sh | 12 + scripts/test_staging_workflow_env.sh | 4 + test/backup_scripts_policy_test.sh | 14 + test/frontend_dev_bind_mount_policy_test.sh | 23 +- .../kratos_identity_write_path_policy_test.sh | 83 + test/orgfront_integration_policy_test.sh | 2 + test/staging_frontend_deploy_policy_test.sh | 19 +- test/staging_pull_restart_policy_test.sh | 37 + userfront/Dockerfile | 2 +- .../presentation/dashboard_screen.dart | 2 +- userfront/lib/i18n_data.dart | 2 +- userfront/scripts/dev-server.sh | 97 +- .../test/dashboard_timeline_dedup_test.dart | 19 + userfront/test/toml_asset_loader_test.dart | 4 + 158 files changed, 10995 insertions(+), 1490 deletions(-) create mode 100644 adminfront/src/components/ui/checkbox.test.tsx create mode 100644 adminfront/src/components/ui/textarea.test.tsx create mode 100644 adminfront/src/features/tenants/routes/TenantUsersPage.export.test.tsx create mode 100644 adminfront/src/features/users/GlobalCustomClaimsPage.tsx create mode 100644 adminfront/src/test/formFieldDiagnostics.test.ts create mode 100644 adminfront/src/test/formFieldDiagnostics.ts create mode 100644 backend/cmd/adminctl/worksmobile_sync.go create mode 100644 backend/cmd/adminctl/worksmobile_sync_test.go create mode 100644 backend/internal/domain/identity_cache.go create mode 100644 backend/internal/domain/system_setting.go create mode 100644 backend/internal/service/kratos_admin_service_test.go create mode 100644 backend/internal/service/redis_service_test.go create mode 100644 backend/internal/service/worksmobile_relay_leader_lock.go create mode 100644 devfront/src/features/clients/ClientDetailTabs.test.tsx create mode 100644 devfront/src/features/clients/components/AllowedTenantBadge.test.tsx create mode 100644 devfront/src/features/clients/components/AllowedTenantBadge.tsx create mode 100644 devfront/tests/devfront-client-tenant-access.spec.ts create mode 100644 docs/identity-redis-mirror-policy-2026-06-09.md create mode 100644 docs/staging-502-restart-policy-incident-2026-06-08.md create mode 100644 docs/user-projection-visibility-audit-2026-06-08.md create mode 100644 docs/works-only-user-recovery-2026-06-09.md create mode 100644 docs/worksmobile-api-rate-limit-policy-2026-06-09.md create mode 100755 test/kratos_identity_write_path_policy_test.sh diff --git a/.gitea/workflows/build_RC.yml b/.gitea/workflows/build_RC.yml index 2704fa24..678d59f1 100644 --- a/.gitea/workflows/build_RC.yml +++ b/.gitea/workflows/build_RC.yml @@ -156,8 +156,9 @@ jobs: - name: Build and push userfront RC image uses: docker/build-push-action@v5 with: - context: ./userfront + context: . file: ./userfront/Dockerfile + target: production push: true tags: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/userfront:${{ steps.rc_calculator.outputs.new_rc_tag }} provenance: false diff --git a/.gitea/workflows/code_check.yml b/.gitea/workflows/code_check.yml index 1b031315..362dbd26 100644 --- a/.gitea/workflows/code_check.yml +++ b/.gitea/workflows/code_check.yml @@ -695,6 +695,7 @@ jobs: mkdir -p reports set +e cd userfront + rm -rf build/web flutter build web --wasm --release 2>&1 | tee ../reports/userfront-e2e-build.log build_exit_code=${PIPESTATUS[0]} cd .. diff --git a/.gitea/workflows/staging_code_pull.yml b/.gitea/workflows/staging_code_pull.yml index 3fe6e06e..a5386331 100644 --- a/.gitea/workflows/staging_code_pull.yml +++ b/.gitea/workflows/staging_code_pull.yml @@ -80,6 +80,7 @@ jobs: AUDIT_WORKER_COUNT=5 AUDIT_QUEUE_SIZE=2000 PROFILE_CACHE_TTL=${{ vars.PROFILE_CACHE_TTL }} + ORGFRONT_ORGCHART_CACHE_TTL_SECONDS=${{ vars.ORGFRONT_ORGCHART_CACHE_TTL_SECONDS }} NAVER_CLOUD_ACCESS_KEY=${{ vars.NAVER_CLOUD_ACCESS_KEY }} NAVER_CLOUD_SECRET_KEY=${{ secrets.NAVER_CLOUD_SECRET_KEY }} NAVER_CLOUD_SERVICE_ID=${{ vars.NAVER_CLOUD_SERVICE_ID }} @@ -133,12 +134,14 @@ jobs: ORGFRONT_CALLBACK_URLS=${{ vars.ORGFRONT_CALLBACK_URLS }} KRATOS_ALLOWED_RETURN_URLS_JSON=${{ vars.KRATOS_ALLOWED_RETURN_URLS_JSON }} KRATOS_ALLOWED_RETURN_URLS_EXTRA=${{ vars.KRATOS_ALLOWED_RETURN_URLS_EXTRA }} - STAGING_PUBLIC_HEALTH_URL=${{ vars.STAGING_PUBLIC_HEALTH_URL }} - STAGING_PUBLIC_HEALTH_MAX_ATTEMPTS=${{ vars.STAGING_PUBLIC_HEALTH_MAX_ATTEMPTS }} # OATHKEEPER_INTROSPECT_CLIENT_ID=${{ vars.OATHKEEPER_INTROSPECT_CLIENT_ID }} # OATHKEEPER_INTROSPECT_CLIENT_SECRET=${{ secrets.STG_OATHKEEPER_INTROSPECT_CLIENT_SECRET }} EOF + if ! grep -Eq "^ORGFRONT_ORGCHART_CACHE_TTL_SECONDS=.+" .env; then + sed -i "s/^ORGFRONT_ORGCHART_CACHE_TTL_SECONDS=.*/ORGFRONT_ORGCHART_CACHE_TTL_SECONDS=3600/" .env + fi + # 코드 업데이트 (Git) ssh "${STAGE_USER}@${STAGE_HOST}" "mkdir -p '${DEPLOY_PATH}' && cd '${DEPLOY_PATH}' && \ if [ ! -d .git ]; then @@ -224,36 +227,12 @@ jobs: return 1 } - check_public_http() { - url="$1" - if [ -z "${url}" ]; then - echo "ERROR: STAGING_PUBLIC_HEALTH_URL is required." >&2 - return 1 - fi - max="${STAGING_PUBLIC_HEALTH_MAX_ATTEMPTS:-30}" - i=1 - while [ "${i}" -le "${max}" ]; do - if curl -fsS --max-time 10 "${url}" >/dev/null; then - echo "Public staging URL ready: ${url}" - return 0 - fi - echo "Waiting for public staging URL: ${url} (${i}/${max})" - i=$((i + 1)) - sleep 2 - done - echo "ERROR: public staging URL not ready: ${url}" >&2 - docker compose -f staging_pull_compose.yaml ps >&2 || true - docker logs baron_gateway --tail 200 >&2 || true - return 1 - } - check_container_url baron_backend http://127.0.0.1:3000/health check_container_http baron_userfront 5000 check_container_http baron_gateway 5000 check_container_http baron_adminfront 5173 check_container_http baron_devfront 5173 check_container_http baron_orgfront 5175 - check_public_http "${STAGING_PUBLIC_HEALTH_URL}" echo "===== INIT-RP LOGS =====" docker compose -f staging_pull_compose.yaml logs init-rp || true diff --git a/.gitea/workflows/staging_release.yml b/.gitea/workflows/staging_release.yml index fa1c9eba..d15d1338 100644 --- a/.gitea/workflows/staging_release.yml +++ b/.gitea/workflows/staging_release.yml @@ -90,6 +90,7 @@ jobs: AUDIT_WORKER_COUNT=5 AUDIT_QUEUE_SIZE=2000 PROFILE_CACHE_TTL=${{ vars.PROFILE_CACHE_TTL }} + ORGFRONT_ORGCHART_CACHE_TTL_SECONDS=${{ vars.ORGFRONT_ORGCHART_CACHE_TTL_SECONDS }} NAVER_CLOUD_ACCESS_KEY=${{ vars.NAVER_CLOUD_ACCESS_KEY }} NAVER_CLOUD_SECRET_KEY=${{ secrets.NAVER_CLOUD_SECRET_KEY }} NAVER_CLOUD_SERVICE_ID=${{ vars.NAVER_CLOUD_SERVICE_ID }} @@ -142,11 +143,16 @@ jobs: # OATHKEEPER_INTROSPECT_CLIENT_SECRET=${{ secrets.STG_OATHKEEPER_INTROSPECT_CLIENT_SECRET }} EOF + if ! grep -Eq "^ORGFRONT_ORGCHART_CACHE_TTL_SECONDS=.+" .env; then + sed -i "s/^ORGFRONT_ORGCHART_CACHE_TTL_SECONDS=.*/ORGFRONT_ORGCHART_CACHE_TTL_SECONDS=3600/" .env + fi + required_dotenv_keys=" APP_ENV BACKEND_LOG_LEVEL CLIENT_LOG_DEBUG WORKS_ADMIN_API_BASE_URL WORKS_ADMIN_OAUTH_TOKEN_URL TZ IDP_PROVIDER DB_PORT CLICKHOUSE_PORT_HTTP CLICKHOUSE_PORT_NATIVE CLICKHOUSE_HOST CLICKHOUSE_USER CLICKHOUSE_PASSWORD BACKEND_PORT ADMINFRONT_PORT DEVFRONT_PORT ORGFRONT_PORT USERFRONT_PORT OATHKEEPER_API_URL DB_USER DB_PASSWORD DB_NAME COOKIE_SECRET JWT_SECRET REDIS_ADDR CORS_ALLOWED_ORIGINS PROFILE_CACHE_TTL + ORGFRONT_ORGCHART_CACHE_TTL_SECONDS NAVER_CLOUD_ACCESS_KEY NAVER_CLOUD_SECRET_KEY NAVER_CLOUD_SERVICE_ID NAVER_SENDER_PHONE_NUMBER AWS_REGION AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SES_SENDER ADMIN_EMAIL ADMIN_PASSWORD USERFRONT_URL ORGFRONT_URL BACKEND_PUBLIC_URL BACKEND_URL OATHKEEPER_PUBLIC_URL diff --git a/.gitea/workflows/userfront_e2e_full_nightly.yml b/.gitea/workflows/userfront_e2e_full_nightly.yml index 83aa3d57..01cda41c 100644 --- a/.gitea/workflows/userfront_e2e_full_nightly.yml +++ b/.gitea/workflows/userfront_e2e_full_nightly.yml @@ -121,6 +121,7 @@ jobs: - name: Build userfront WASM run: | cd userfront + rm -rf build/web flutter build web --wasm --release cd .. node userfront/scripts/optimize-web-build.mjs userfront/build/web diff --git a/README.md b/README.md index 6a41a4a1..50e459ed 100644 --- a/README.md +++ b/README.md @@ -378,6 +378,59 @@ flowchart TD Kratos가 사용자 SoT이며 Hydra는 순수 OIDC 토큰 엔진입니다. 비지니스로직은 Backend를 통해서, 기본 인증 로직은 Ory Stack을 통해 진행됩니다. +### SSOT 및 Redis Cache 전략 + +Baron SSO는 “하나의 DB가 모든 데이터의 원본”인 구조가 아닙니다. 데이터 성격별로 원장이 다르며, Backend는 원장 쓰기 경로와 감사 로그를 중앙화하는 Control Plane입니다. Redis와 PostgreSQL projection은 성능과 운영 편의를 위한 read model/cache로만 사용하고, 원장과 불일치할 수 있다는 전제를 명시합니다. + +#### 데이터별 원본 위치 + +| 데이터 | SSOT | 보조 저장소/캐시 | 비고 | +| --- | --- | --- | --- | +| Identity subject, credentials, recovery/verification address | Ory Kratos `identities` | Redis identity mirror, PostgreSQL `users.id` 참조 | Kratos identity ID가 사용자 subject이며 WORKS `externalKey` 기준입니다. | +| 로그인 식별자 | Kratos traits, `user_login_ids` | Redis identity mirror | Kratos는 인증 식별자, PostgreSQL은 중복/정책 검증용 index입니다. | +| 사용자 이름, 이메일, 전화번호, role 기본값 | Kratos traits | PostgreSQL `users`, Redis mirror | 인증/profile 계산에 필요한 최소 identity 값만 Kratos에 유지합니다. | +| Baron 사용자 상태, soft delete, 운영 메타데이터 | PostgreSQL `users`, `users.metadata` | Redis mirror 조합 응답 | `users.deleted_at`은 Baron 운영 상태이며 Kratos identity 삭제와 같은 의미가 아닙니다. | +| 테넌트 tree, slug, 조직/부서/직무/직책 | PostgreSQL `tenants`, `users`, membership metadata | Redis/API response cache 가능 | 관계형 조직 데이터는 Kratos traits가 아니라 Backend DB가 원장입니다. | +| 권한/관계 | Ory Keto relation tuple | PostgreSQL outbox/status | Backend를 통해 relation command를 보내고 처리 상태를 추적합니다. | +| OAuth2/OIDC client, consent, token state | Ory Hydra | PostgreSQL `client_consents`, audit/read model | Hydra가 프로토콜 원장이며 로컬 테이블은 운영 조회/감사용입니다. | +| RP별 사용자 custom claim 값 | PostgreSQL `rp_user_metadata` | ID token/userinfo projection | RP 관리자 범위 데이터이며 전역 claim과 분리합니다. | +| 전역 사용자 custom claim 값 | PostgreSQL `users.metadata.global_custom_claims` | ID token projection | 전체 사용자 대상 claim으로 adminfront 사용자 상세에서만 관리합니다. | +| WORKS Mobile mapping/outbox/job 상태 | PostgreSQL `worksmobile_*` | WORKS API 비교 응답 cache 가능 | 외부 SaaS 연동 상태이며 identity 원장이 아닙니다. | +| 감사 로그/사용량 | ClickHouse, Oathkeeper/Ory 로그 | 화면별 summary cache 가능 | command와 보안 이벤트의 감사 원장입니다. | +| Headless JWKS 검증 상태 | Redis `headless:jwks:*` cache | DevFront 상태 카드 | RP public key 문서 자체는 외부 `jwksUri`가 원본입니다. | +| 로그인 코드, pending login, verification token | Redis short-lived key | 없음 | 만료 가능한 휘발성 상태입니다. 백업/복구 대상이 아닙니다. | + +#### SSOT 보장 원칙 + +1. Kratos/Hydra/Keto/WORKS로 향하는 쓰기 command는 Backend를 통과합니다. +2. Backend는 원장 write 성공 후 원장 ID를 기준으로 재조회하고, PostgreSQL read model 또는 Redis mirror를 write-through 갱신합니다. +3. write-through 갱신 실패 시 원장 write를 되돌린 것으로 간주하지 않습니다. 대신 mirror/cache 상태를 `stale` 또는 `failed`로 표시하고 drift report와 refresh 대상으로 둡니다. +4. Kratos Admin API 또는 Kratos DB를 Backend 밖에서 직접 수정하는 경로는 운영 정책상 금지합니다. 정비/DR처럼 예외가 필요한 경우에는 Redis mirror를 stale로 표시하고, full refresh와 drift report를 완료하기 전까지 cache 결과를 신뢰하지 않습니다. +5. PostgreSQL projection은 Kratos partial list를 full snapshot처럼 취급하지 않습니다. Kratos 목록 조회가 partial이면 로컬 사용자를 삭제/숨김 처리하지 않습니다. +6. frontend 대량 조회는 cursor 기반을 원칙으로 합니다. `limit=5000&offset=0` 같은 단일 대량 offset 조회는 사용자 수가 늘면 partial data를 전체처럼 보이게 만들 수 있으므로 신규 구현에서 금지합니다. +7. Redis cache miss가 발생한 단건 조회는 가능한 경우 SSOT로 fallback하고, fallback 성공 시 Redis를 갱신합니다. 목록 조회는 mirror 상태가 `ready`가 아니면 화면/API에 경고 상태를 함께 전달해야 합니다. + +#### Redis 사용 원칙 + +Redis는 원장이 아니라 cache/mirror 계층입니다. Redis 데이터 유실은 장애지만 데이터 유실 사고로 보지 않고, 원장 재조회와 refresh로 재수렴해야 합니다. + +| Redis 데이터 | 역할 | TTL/보존 정책 | 장애 시 처리 | +| --- | --- | --- | --- | +| `identity:mirror:{identityID}` | Kratos identity summary 단건 cache | 장기 mirror. refresh 상태와 함께 운영 | Kratos `GetIdentity` fallback 후 write-through | +| `identity:index:*` | identity 목록/검색 cursor index | mirror refresh 주기로 재작성 | `stale` 표시 후 full refresh | +| `identity:mirror:state` | mirror 상태, count, last error | 영구 상태 key | adminfront에서 경고 표시 | +| `headless:jwks:*` | RP headless login JWKS cache | JWKS TTL과 prefetch 정책 | kid miss/검증 실패/TTL 만료 시 재조회 | +| login/verification/pending 계열 key | 인증 흐름의 단기 상태 | 짧은 TTL 필수 | 만료 또는 유실 시 사용자가 흐름 재시작 | +| 일반 API response cache | 선택적 성능 cache | 짧은 TTL, invalidation 우선 | miss 시 Backend DB 또는 Ory 원장 조회 | + +운영 Redis 설정은 `maxmemory`와 `maxmemory_policy`가 명시되어야 합니다. identity mirror처럼 재수렴 가능한 데이터와 pending login처럼 사용자 흐름에 영향을 주는 단기 key가 같은 Redis를 공유하므로, eviction 발생 여부와 TTL 없는 key 증가를 운영 화면에서 볼 수 있어야 합니다. + +#### Redis 모니터링 계획 + +Redis 적정 설정 판단에 필요한 운영 지표를 adminfront에 노출하는 후속 작업은 이슈 [#1046](https://gitea.hmac.kr/baron/baron-sso/issues/1046)으로 분리했습니다. + +표시 대상은 Redis 연결/버전/uptime, `used_memory`, `maxmemory`, `maxmemory_policy`, keyspace hit/miss, expired/evicted keys, prefix별 key count, TTL 분포, `identity:mirror:state`, headless JWKS cache failure 요약입니다. 이 화면은 `super_admin` 전용으로 두고, Redis key value 자체는 노출하지 않습니다. + --- ## 🚀 시작하기 (Getting Started) diff --git a/adminfront/scripts/runtime-mode.sh b/adminfront/scripts/runtime-mode.sh index b2709aa1..feffa2b1 100644 --- a/adminfront/scripts/runtime-mode.sh +++ b/adminfront/scripts/runtime-mode.sh @@ -51,14 +51,17 @@ ensure_frontend_dependencies() { if [ -n "$WORKSPACE_ROOT" ]; then WORKSPACE_DIR="$WORKSPACE_ROOT" LOCK_FILE="$WORKSPACE_ROOT/pnpm-lock.yaml" + COMMON_PACKAGE_FILE="$WORKSPACE_ROOT/common/package.json" INSTALL_CMD="cd $WORKSPACE_ROOT && CI=true pnpm install --filter ${APP_PACKAGE_NAME}... --frozen-lockfile --ignore-scripts" elif [ -f "pnpm-lock.yaml" ]; then WORKSPACE_DIR="." LOCK_FILE="pnpm-lock.yaml" + COMMON_PACKAGE_FILE="/workspace/common/package.json" INSTALL_CMD="CI=true pnpm install --frozen-lockfile --ignore-scripts" else WORKSPACE_DIR="." LOCK_FILE="package-lock.json" + COMMON_PACKAGE_FILE="/workspace/common/package.json" INSTALL_CMD="npm ci" fi @@ -100,9 +103,9 @@ ensure_frontend_dependencies() { } if command -v sha256sum >/dev/null 2>&1; then - deps_hash="$(sha256sum "$WORKSPACE_DIR/package.json" "$LOCK_FILE" package.json 2>/dev/null | sha256sum | awk '{print $1}')" + deps_hash="$(sha256sum "$WORKSPACE_DIR/package.json" "$LOCK_FILE" "$COMMON_PACKAGE_FILE" package.json 2>/dev/null | sha256sum | awk '{print $1}')" else - deps_hash="$(cksum "$WORKSPACE_DIR/package.json" "$LOCK_FILE" package.json 2>/dev/null | cksum | awk '{print $1}')" + deps_hash="$(cksum "$WORKSPACE_DIR/package.json" "$LOCK_FILE" "$COMMON_PACKAGE_FILE" package.json 2>/dev/null | cksum | awk '{print $1}')" fi deps_stamp="node_modules/.baron-deps-hash" installed_hash="$(cat "$deps_stamp" 2>/dev/null || true)" @@ -111,9 +114,9 @@ ensure_frontend_dependencies() { echo "Installing frontend dependencies..." acquire_install_lock if command -v sha256sum >/dev/null 2>&1; then - deps_hash="$(sha256sum "$WORKSPACE_DIR/package.json" "$LOCK_FILE" package.json 2>/dev/null | sha256sum | awk '{print $1}')" + deps_hash="$(sha256sum "$WORKSPACE_DIR/package.json" "$LOCK_FILE" "$COMMON_PACKAGE_FILE" package.json 2>/dev/null | sha256sum | awk '{print $1}')" else - deps_hash="$(cksum "$WORKSPACE_DIR/package.json" "$LOCK_FILE" package.json 2>/dev/null | cksum | awk '{print $1}')" + deps_hash="$(cksum "$WORKSPACE_DIR/package.json" "$LOCK_FILE" "$COMMON_PACKAGE_FILE" package.json 2>/dev/null | cksum | awk '{print $1}')" fi installed_hash="$(cat "$deps_stamp" 2>/dev/null || true)" if [ "$installed_hash" = "$deps_hash" ]; then diff --git a/adminfront/seed-tenant.csv b/adminfront/seed-tenant.csv index 5ff8391f..5eabc9bd 100644 --- a/adminfront/seed-tenant.csv +++ b/adminfront/seed-tenant.csv @@ -9,4 +9,5 @@ c18a8284-0008-48aa-9cdf-9f47ab79a2a9,(주)장헌,COMPANY,baron-group,jangheon,,j b2fcf17f-7085-4bfe-9663-d8a2f2f4b2d6,장헌산업,COMPANY,baron-group,jangheon-sanup,,jangheon.co.kr,,, e57cb22c-383e-4489-8c2f-0c5431917e86,(주)피티씨,COMPANY,baron-group,ptc,,pre-cast.co.kr,,, 4d0f26b9-702c-4bc6-8996-46e9eedfdeb7,MH_manager,USER_GROUP,hanmac-family,mhd,맨아워 대시보드 권한 보유자그룹,,private,,no +e41adf79-3d15-4807-8303-afbdb0f2bab7,SW_uploader,USER_GROUP,hanmac-family,sw-uploader,소프트웨어 배포 권한 그룹,,private,,no 9607eb7b-04d2-42ab-80fe-780fe21c7e8f,Personal,PERSONAL,,personal,개인 사용자 기본 루트 테넌트,,,, diff --git a/adminfront/src/app/routes.test.tsx b/adminfront/src/app/routes.test.tsx index ec98de66..64ff5323 100644 --- a/adminfront/src/app/routes.test.tsx +++ b/adminfront/src/app/routes.test.tsx @@ -16,10 +16,10 @@ describe("admin routes", () => { expect(matches?.at(-1)?.route.path).toBe("/auth/callback"); }); - it("registers the super-admin user projection management route", () => { - const matches = matchRoutes(adminRoutes, "/system/projections/users"); + it("registers the super-admin Ory SSOT system route", () => { + const matches = matchRoutes(adminRoutes, "/system/ory-ssot"); - expect(matches?.at(-1)?.route.path).toBe("system/projections/users"); + expect(matches?.at(-1)?.route.path).toBe("system/ory-ssot"); }); it("registers the super-admin data integrity management route", () => { @@ -28,6 +28,16 @@ describe("admin routes", () => { expect(matches?.at(-1)?.route.path).toBe("system/data-integrity"); }); + it("routes global custom claim settings before user detail id matching", () => { + const matches = matchRoutes(adminRoutes, "/users/custom-claims"); + const leafRoute = matches?.at(-1)?.route; + + expect(leafRoute?.path).toBe("users/custom-claims"); + expect(getRouteElementName(leafRoute?.element)).toBe( + "GlobalCustomClaimsPage", + ); + }); + it("keeps protected admin pages behind an auth guard before mounting the layout", () => { const rootRoute = adminRoutes.find((route) => route.path === "/"); const protectedShellRoute = rootRoute?.children?.[0]; diff --git a/adminfront/src/app/routes.tsx b/adminfront/src/app/routes.tsx index 0d44cda1..339a14d7 100644 --- a/adminfront/src/app/routes.tsx +++ b/adminfront/src/app/routes.tsx @@ -19,6 +19,7 @@ import { TenantProfilePage } from "../features/tenants/routes/TenantProfilePage" import { TenantSchemaPage } from "../features/tenants/routes/TenantSchemaPage"; import { TenantWorksmobilePage } from "../features/tenants/routes/TenantWorksmobilePage"; import TenantUserGroupsTab from "../features/user-groups/routes/TenantUserGroupsTab"; +import GlobalCustomClaimsPage from "../features/users/GlobalCustomClaimsPage"; import UserCreatePage from "../features/users/UserCreatePage"; import UserDetailPage from "../features/users/UserDetailPage"; import UserListPage from "../features/users/UserListPage"; @@ -44,6 +45,7 @@ export const adminRoutes: RouteObject[] = [ { path: "audit-logs", element: }, { path: "auth", element: }, { path: "users", element: }, + { path: "users/custom-claims", element: }, { path: "users/new", element: }, { path: "users/:id", element: }, { path: "tenants", element: }, @@ -65,7 +67,7 @@ export const adminRoutes: RouteObject[] = [ }, { path: "api-keys", element: }, { path: "api-keys/new", element: }, - { path: "system/projections/users", element: }, + { path: "system/ory-ssot", element: }, { path: "system/data-integrity", element: }, ], }, diff --git a/adminfront/src/components/common/LanguageSelector.tsx b/adminfront/src/components/common/LanguageSelector.tsx index c1716ef4..94d35369 100644 --- a/adminfront/src/components/common/LanguageSelector.tsx +++ b/adminfront/src/components/common/LanguageSelector.tsx @@ -53,6 +53,8 @@ function LanguageSelector() { return ( { expect(screen.getByPlaceholderText("Enter text")).toBeInTheDocument(); }); + it("adds a fallback id for browser autofill diagnostics", () => { + render(); + + expect(screen.getByPlaceholderText("Enter text")).toHaveAttribute("id"); + }); + + it("keeps explicit id and name values", () => { + render(); + const input = screen.getByRole("textbox"); + + expect(input).toHaveAttribute("id", "explicit-id"); + expect(input).toHaveAttribute("name", "explicit-name"); + }); + it("handles value changes", async () => { const onChange = vi.fn(); const user = userEvent.setup(); diff --git a/adminfront/src/components/ui/input.tsx b/adminfront/src/components/ui/input.tsx index 1322da14..39df5f97 100644 --- a/adminfront/src/components/ui/input.tsx +++ b/adminfront/src/components/ui/input.tsx @@ -6,9 +6,14 @@ export interface InputProps extends React.InputHTMLAttributes {} const Input = React.forwardRef( - ({ className, type, ...props }, ref) => { + ({ className, type, id, name, ...props }, ref) => { + const fallbackId = React.useId(); + const fieldId = id ?? (name ? undefined : fallbackId); + return ( { + it("adds a fallback id for browser autofill diagnostics", () => { + render(