forked from baron/baron-sso
473 lines
31 KiB
Markdown
473 lines
31 KiB
Markdown
# 웍스모바일 Directory 연동 기술 검토
|
|
|
|
## 개요
|
|
|
|
- 대상 Epic: `orgfront`와 웍스모바일 Directory API 간 한맥가족 사용자/조직 연동
|
|
- 관련 이슈: #668 한맥가족 이메일 local-part unique 정책
|
|
- 대상 마일스톤: `한맥가족사 조직도 반영 및 웍스모바일 연동` (`id=42`)
|
|
- 기준 SoT: `hanmac-family` 테넌트 subtree 하위 Kratos identity
|
|
- 작성일: 2026-05-04
|
|
|
|
## 현재 Baron SSO 구조 요약
|
|
|
|
Baron SSO는 Ory Stack을 SSOT로 둡니다. `docs/SoT_Architecture_Policy.md`와 `docs/tenant-usergroup-policy.md` 기준으로 Identity는 Kratos, 권한/멤버십은 Keto가 원장입니다. PostgreSQL은 Ory에 저장되지 않거나 조회가 불가능한 Worksmobile/조직 표시/검색 데이터의 read model과 처리 상태 저장소로만 사용합니다.
|
|
|
|
현재 사용자 생성 흐름은 다음과 같습니다.
|
|
|
|
- `backend/internal/handler/user_handler.go`
|
|
- `CreateUser`: `companyCode`로 tenant slug를 찾아 `traits["tenant_id"]`에 tenant UUID를 저장합니다.
|
|
- `BulkCreateUsers`: CSV/import row별로 tenant slug를 tenant UUID로 변환하고 Kratos identity를 생성합니다.
|
|
- Kratos identity id는 `users.id`로 그대로 저장됩니다. 이 값이 웍스모바일 `userExternalKey` 후보입니다.
|
|
- `mapToLocalUser`는 `traits["tenant_id"]`를 `users.tenant_id`로 저장합니다.
|
|
- `backend/internal/handler/tenant_handler.go`
|
|
- `CreateTenant`: `TenantService.RegisterTenant` 호출 후 domains/config를 별도로 저장합니다.
|
|
- `UpdateTenant`: tenant 필드와 parent relation을 갱신합니다.
|
|
- `backend/internal/service/keto_relay_worker.go`
|
|
- `keto_outbox`를 polling하여 Keto relation을 비동기로 반영합니다.
|
|
|
|
한맥가족 이메일 정책은 이미 #668에서 다음 방향으로 구현되어 있습니다.
|
|
|
|
- `hanmac-family` root tenant와 descendant subtree에서 email local-part를 unique로 강제합니다.
|
|
- local-part unique 검사는 `archived`를 포함한 모든 사용자 상태를 대상으로 합니다.
|
|
- 단건 생성은 중복 시 `409 Conflict`로 차단합니다.
|
|
- bulk import는 `@domain` 입력 시 이름 기반 local-part를 제안하고, 생성 직전 재검증합니다.
|
|
- `preboarding`, `baron_guest`, `extended_leave`, `archived` 사용자는 Worksmobile 구성원 생성/갱신/backfill 대상에서 제외합니다.
|
|
- `baron_guest`, `extended_leave`, `archived` 상태로 전환된 사용자는 기존 Worksmobile 계정 delete/deprovision 대상입니다.
|
|
|
|
## 웍스모바일 Directory API 확인 사항
|
|
|
|
공식 문서 기준 Directory API는 구성원, 조직, 그룹, 직급, 직책, 사용자 유형 등을 관리합니다.
|
|
|
|
확인한 주요 엔드포인트와 제약은 다음과 같습니다.
|
|
|
|
- 인증
|
|
- API 호출에는 OAuth 2.0 Access Token이 필요합니다.
|
|
- 시스템 연동에는 서비스 계정 인증(JWT) 방식이 적합합니다.
|
|
- 필요한 scope는 최소 `directory`이며, 구성원만 다룰 경우 `user`, 조직만 다룰 경우 `orgunit`도 사용 가능합니다.
|
|
- 구성원
|
|
- `POST https://www.worksapis.com/v1.0/users`
|
|
- 필수 주요 필드: `domainId`, `email`, `userName`
|
|
- SSO 사용 시 `userExternalKey`가 필요합니다.
|
|
- `userExternalKey`는 테넌트 내 unique이며 `%`, `\`, `#`, `/`, `?`를 사용할 수 없습니다.
|
|
- `organizations[].orgUnits[].orgUnitId`는 resource id 또는 `externalKey:{orgUnitExternalKey}` 형태를 사용할 수 있습니다.
|
|
- 조직
|
|
- `POST https://www.worksapis.com/v1.0/orgunits`
|
|
- 필수 주요 필드: `domainId`, `orgUnitName`, `displayOrder`
|
|
- `orgUnitExternalKey`는 테넌트 내 unique이며 `%`, `\`, `#`, `/`, `?`를 사용할 수 없습니다.
|
|
- `parentOrgUnitId`는 resource id 또는 `externalKey:{orgUnitExternalKey}` 형태를 사용할 수 있습니다.
|
|
- External Key Mapping
|
|
- `POST /users/external-keys`
|
|
- `POST /orgunits/external-keys`
|
|
- 기존 웍스모바일 리소스에 External Key가 없는 경우 초기 bulk mapping에 사용합니다.
|
|
- 호출 제한/운영 주의
|
|
- 조직 추가/수정/부분 수정/이동 API는 도메인당 단일 스레드로 1초에 1회, 순서대로 호출해야 합니다.
|
|
- 동일 구성원에 대한 추가/수정/부분수정/전배 API는 동시에 호출하지 않아야 합니다.
|
|
- Directory API 조직 연동 배치는 직급/직책/사용자 유형 -> 조직 -> 구성원 -> 그룹 순서를 권장합니다.
|
|
- API 동시 호출은 5회 이상 하지 않도록 관리해야 하며, 특히 조직 API는 단일 스레드가 필요합니다.
|
|
|
|
## AdminFront bulk 생성과 NAVERWORKS bulk 생성 비교
|
|
|
|
구현 전에 `adminfront`의 기존 조직/사용자 bulk 생성 방식과 `adminfront/NAVERWORKS_member_add_sample_English.csv`의 구성원 bulk 필드를 비교했습니다.
|
|
|
|
### Baron/AdminFront 기존 방식
|
|
|
|
- 조직 bulk
|
|
- `adminfront/src/features/tenants/utils/tenantCsvImport.ts`
|
|
- 주요 컬럼: `tenant_id`, `name`, `type`, `parent_tenant_id`, `parent_tenant_slug`, `slug`, `memo`, `email_domain`
|
|
- Baron tenant tree를 직접 생성/갱신합니다.
|
|
- parent는 Baron tenant UUID 또는 slug 기준으로 해석합니다.
|
|
- 사용자 bulk
|
|
- `adminfront/src/features/users/components/UserBulkUploadModal.tsx`
|
|
- `parseUserCSV`가 `BulkUserItem`으로 변환한 뒤 `/api/v1/admin/users/bulk`로 전송합니다.
|
|
- 주요 컬럼: `email`, `name`, `phone`, `role`, `tenant_slug`, `department`, `position`, `jobTitle`, `employee_id`
|
|
- 사용자 import 중 없는 tenant를 미리 생성할 수 있고, #668 한맥가족 email local-part unique preview를 거칩니다.
|
|
|
|
### NAVERWORKS sample 방식
|
|
|
|
- 구성원 bulk sample
|
|
- 파일: `adminfront/NAVERWORKS_member_add_sample_English.csv`
|
|
- 주요 컬럼: `LastName`, `FirstName`, `ID`, `Personal email`, `Sub email`, `User type`, `Level`, `Organization`, `Position`, `Mobile/Country code`, `Mobile/Numbers`, `Responsibilities`, `Workplace`, `Entry Date`, `Employee number`, `Account activation time`
|
|
- `ID`는 Baron loginId 및 Worksmobile userExternalKey와는 다른 계정 local-part 성격입니다.
|
|
- `Organization`은 `org.1|org.2|org.3|myteam`처럼 path 문자열로 제공됩니다.
|
|
- `Employee number`는 Baron metadata의 `employee_id`로 보존합니다.
|
|
|
|
### 구현 반영
|
|
|
|
- `parseUserCSV`를 quoted CSV/BOM에 대응하도록 보강했습니다.
|
|
- NAVERWORKS 구성원 sample 필드를 Baron bulk user field로 흡수합니다.
|
|
- `Sub email`의 첫 이메일 -> Baron `email`
|
|
- `ID` -> Baron `loginId`, metadata `naverworks_id`
|
|
- `FirstName` + `LastName` -> Baron `name`
|
|
- `Mobile/Country code` + `Mobile/Numbers` -> Baron `phone`
|
|
- `Organization` path leaf -> Baron `department`, tenant import name
|
|
- `Position` -> Baron `position`
|
|
- `Responsibilities` -> Baron `jobTitle`
|
|
- `Employee number` -> metadata `employee_id`
|
|
- Worksmobile API payload 생성 시에는 request body의 external key/domainId를 사용하지 않고 Baron UUID와 `tenant.config.worksmobile.domainMappings` 및 `.env`의 domainId 값을 server-side 계산합니다.
|
|
|
|
## 매핑 설계
|
|
|
|
### External Key
|
|
|
|
Baron 내부 UUID는 웍스모바일 External Key 제한 문자와 충돌하지 않으므로 그대로 사용할 수 있습니다.
|
|
|
|
- 구성원 `userExternalKey`: Kratos identity UUID, 즉 `users.id`
|
|
- 조직 `orgUnitExternalKey`: Baron `tenants.id`
|
|
- 조직 지정: `externalKey:{tenant.ID}`
|
|
- 구성원 지정: `externalKey:{user.ID}` 또는 email/resource id
|
|
|
|
이 선택은 "Kratos account가 사용자 SoT"라는 정책과 맞습니다. 사용자 생성 후 Worksmobile resource id가 생기더라도 Baron의 primary mapping은 Kratos UUID를 유지하고, Worksmobile resource id는 캐시/응답 추적용으로만 보관하는 것이 좋습니다.
|
|
|
|
### 조직
|
|
|
|
Baron tenant를 Worksmobile orgunit으로 보냅니다.
|
|
|
|
- 대상 tenant: `hanmac-family` root 하위 subtree 중 `COMPANY`, `USER_GROUP`
|
|
- 제외 후보: `PERSONAL`, system/global 성격 tenant
|
|
- `orgUnitName`: `tenant.name`
|
|
- `orgUnitExternalKey`: `tenant.id`
|
|
- `parentOrgUnitId`: parent가 Worksmobile 동기화 대상이면 `externalKey:{parentTenant.ID}`
|
|
- `domainId`: tenant domain 또는 root integration config에서 email domain별로 해석
|
|
- `displayOrder`: 동일 parent 내 deterministic order 필요. 1차 구현은 `name asc`, `created_at asc`, 또는 별도 `config.worksmobile.displayOrder` 정책 중 하나를 선택해야 합니다.
|
|
|
|
주의할 점은 Worksmobile `orgunits`가 `domainId`를 필수로 요구한다는 점입니다. Baron은 한맥가족 root 아래에 여러 이메일 도메인과 법인/조직 subtree를 둘 수 있으므로, 우선 `tenant.config.worksmobile.domainMappings`를 지원하되 운영 domainId는 `.env`의 다음 값을 fallback으로 사용합니다.
|
|
|
|
- `SAMAN_DOMAIN_ID`: 삼안 계열
|
|
- `HANMAC_DOMAIN_ID`: 한맥 계열
|
|
- `GPDTDC_DOMAIN_ID`: 총괄기획&기술개발센터
|
|
- `BARONGROUP_DOMAIN_ID`: 위 세 가지에 속하지 않는 모든 한맥가족사
|
|
|
|
분류 순서는 config mapping -> 삼안 -> 한맥 -> GPDTDC -> BARONGROUP fallback입니다.
|
|
|
|
개발/스테이징에서 테스트한 값은 프로덕션에도 동일하게 반영해야 합니다. 네이버웍스 쪽 backend가 동일 환경이므로 이 mapping은 env-only 값으로 숨기지 않고 seed/config migration 등 코드 레벨에서 추적 가능한 값으로 남깁니다.
|
|
|
|
```json
|
|
{
|
|
"worksmobile": {
|
|
"enabled": true,
|
|
"tenantId": "hanmac-family",
|
|
"domainMappings": {
|
|
"hanmaceng.co.kr": 10000001,
|
|
"samaneng.com": 10000002
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 구성원
|
|
|
|
Baron Kratos identity를 Worksmobile user로 보냅니다.
|
|
|
|
- `userExternalKey`: Kratos UUID (`users.id`)
|
|
- `email`: #668 정책으로 확정된 email
|
|
- `userName.lastName`: `traits["name"]` 또는 `users.name` 전체를 우선 입력합니다. 성/이름 분리가 확정되기 전까지는 API 필수 조건 충족을 위해 전체 표시명을 `lastName`에 둡니다.
|
|
- `cellPhone`: normalized phone
|
|
- `employeeNumber`: metadata의 `employee_id` 또는 schema login ID 값이 있으면 사용
|
|
- `privateEmail`: 기본 매핑하지 않습니다. NAVERWORKS sample의 `Personal email`은 Baron metadata에는 보존하지만 Worksmobile payload에는 기본 전송하지 않습니다.
|
|
- `aliasEmails`: 한맥 tenant에 속하고 `employee_id`가 있으면 `employee_id@hanmaceng.co.kr`을 추가합니다.
|
|
- `locale`: 별도 지정이 없으면 `ko_KR`
|
|
- `passwordConfig.passwordCreationType`: `ADMIN` 값을 구성원 생성 시에만 사용합니다.
|
|
- `passwordConfig.password`: 구성원 생성 시 숫자, 영문, 기호를 모두 포함한 16자리 난수 초기 비밀번호를 생성합니다.
|
|
- `task`: Baron `jobTitle`을 우선 사용
|
|
- `organizations`
|
|
- 원직: Worksmobile 연동 제외 테넌트를 제외한 뒤 `additionalAppointments`에 가장 먼저 등록된 tenant
|
|
- 겸직: `metadata.additionalAppointments` 또는 Keto `joinedTenants` 중 Worksmobile 연동 제외가 아닌 tenant
|
|
- `orgUnits[].orgUnitId`: `externalKey:{tenant.ID}`
|
|
- `levelId`, `positionId`, `userTypeId`: 이번 scope에서는 External Key mapping을 사용하지 않고 사용자 정보 업데이트 필드로 최대한 커버
|
|
- `isManager`: `additionalAppointments[].isOwner == true` 또는 Keto owners/admins relation을 기준으로 변환
|
|
|
|
### 구성원 소속 변경 정책
|
|
|
|
Worksmobile 연동 화면의 변경 범위는 다음과 같이 분리합니다.
|
|
|
|
- 조직/그룹 탭은 Worksmobile `orgunits`/`groups` 리소스 자체의 생성, 수정, 삭제, 이동을 관리합니다.
|
|
- 구성원 탭은 Worksmobile `users` 리소스의 조직 소속(`organizations[].orgUnits[]`) 변경을 관리합니다.
|
|
- 구성원 소속 변경 중 Worksmobile에 대상 org unit이 없거나 부모 구조가 최신이 아니면, 먼저 조직/그룹 탭의 조직 sync로 구조 변경을 반영한 뒤 구성원 sync를 수행합니다.
|
|
- `worksmobileExcluded=true`인 테넌트와 그 하위 조직은 조직 sync 및 구성원 소속 sync의 대상에서 제외합니다.
|
|
|
|
개인 사용자 상세 변경은 대표 조직 1개만 수정하는 동작으로 제한하지 않습니다. Baron의 `metadata.additionalAppointments`에 들어 있는 모든 소속 조직을 기준으로 Worksmobile payload의 `organizations[].orgUnits[]`를 구성합니다. 다만 Worksmobile 연동 제외 테넌트, Worksmobile domain ID를 해석할 수 없는 테넌트, 조직 연동 설정이 켜져 있지 않은 겸직 도메인은 payload에서 제외하고 비교 결과에는 skipped reason 또는 warning을 남깁니다.
|
|
|
|
WORKS Developers 문서 확인 결과, 구성원 추가/수정 payload는 `organizations[]`와 하위 `orgUnits[]` 배열을 제공하고 `orgUnits`는 최대 30개까지 허용합니다. 또한 대표 도메인과 대표 조직은 각각 하나가 필요합니다. Directory API의 조직 연동 설명에는 구성원 회사 겸직을 설정하려면 겸직 도메인도 조직 연동 사용 설정을 켜야 한다는 제약이 있습니다.
|
|
|
|
따라서 구현 원칙은 다음과 같습니다.
|
|
|
|
- 동일 Worksmobile domain 안에서는 Worksmobile 연동 제외가 아닌 조직 소속을 모두 동기화합니다.
|
|
- 여러 Worksmobile domain을 넘는 겸직은 해당 domain의 조직 연동 설정이 켜져 있고 Baron에서 domain ID를 해석할 수 있을 때만 동기화합니다.
|
|
- Worksmobile primary는 Baron 대표 테넌트 플래그나 `additionalAppointments[].isPrimary=true`를 기준으로 삼지 않습니다. Worksmobile 연동 제외 테넌트를 제거한 뒤 `additionalAppointments`에 가장 먼저 등록된 소속을 최초 반영 소속이자 primary로 둡니다.
|
|
- `representative`, `isPrimary`, `primary` 같은 Baron 대표 소속 플래그는 Baron 내부 대표 소속 정책에만 사용하고 Worksmobile 동기화 판단 필드로 사용하지 않습니다.
|
|
- 조직장 여부(`isManager`)는 Worksmobile 동기화 대상입니다. 비교 로직은 remote와 Baron의 조직장 여부 차이를 `needs_update`로 판단해야 합니다.
|
|
- 비교 로직도 대표 조직 1개가 아니라 `organizations[].orgUnits[]` 전체 set을 기준으로 `org_unit_added`, `org_unit_removed`, `org_unit_moved`, `org_unit_primary_changed` 같은 diff reason을 산출합니다.
|
|
- Worksmobile 공식 문서 근거: https://developers.worksmobile.com/kr/docs/user-create, https://developers.worksmobile.com/kr/docs/directory
|
|
|
|
초기 비밀번호는 Worksmobile user upsert outbox payload에 `loginEmail`, `initialPassword` 형태로 함께 보관하고, adminfront의 한맥가족 Worksmobile 관리 화면에서 `email,initialPassword,status,lastError` CSV로 다운로드할 수 있게 합니다. 생성 성공/실패 판정은 outbox 작업 상태(`processed`, `failed`)와 함께 확인할 수 있으며, 운영상 평문 초기 비밀번호가 포함되므로 다운로드 권한은 `hanmac-family` tenant manage 권한으로 제한하고 보존 기간 정책을 별도 확정해야 합니다.
|
|
|
|
현재 backend `CreateUser`와 `UpdateUser`는 adminfront가 보내는 top-level `additionalAppointments` 및 `metadata.additionalAppointments`를 수용합니다. 한맥가족 단건 생성에서 대표 `tenantSlug` 없이 appointment만 오는 경우에는 first/primary appointment tenant를 대표 tenant로 해석해 Ory/Keto 관계, 허용된 Backend read model, Worksmobile enqueue가 누락되지 않게 합니다.
|
|
|
|
### 구성원 수정과 비밀번호 정책
|
|
|
|
Worksmobile 구성원 수정 API에는 PUT(`user-update-put`)과 PATCH(`user-update-patch`)가 있지만, 비밀번호 변경 경로로 사용하지 않습니다.
|
|
|
|
- Worksmobile Directory API에서 관리자 지정 초기 비밀번호 값은 별도 top-level `password`가 아니라 `passwordConfig.password`입니다.
|
|
- `passwordConfig.passwordCreationType`은 생성 방식이고, `passwordConfig.password`가 실제 초기 비밀번호 값입니다.
|
|
- 공식 문서와 개발자 포럼 확인 결과, `passwordConfig.passwordCreationType`과 `passwordConfig.password`는 모두 구성원 등록 시에만 실제 반영됩니다.
|
|
- PUT/PATCH request body에 `passwordConfig.password`를 포함해도 기존 구성원의 비밀번호 변경에는 반영되지 않습니다.
|
|
- 따라서 Baron SSO는 WORKS Mobile 구성원 생성 시에만 초기 비밀번호를 설정하고, 생성 이후 Baron 사용자 비밀번호 변경을 WORKS Mobile PUT/PATCH로 전파하지 않습니다.
|
|
- 생성 이후 WORKS Mobile 비밀번호 변경은 WORKS Mobile 관리자 페이지 또는 WORKS Mobile이 제공하는 별도 운영 절차에서 직접 처리합니다.
|
|
- PATCH/PUT payload에는 `passwordConfig`를 포함하지 않습니다.
|
|
- `privateEmail`도 기존 정책대로 기본 전송하지 않습니다.
|
|
- 기존 WORKS Mobile 구성원에 대한 일반 속성/조직/겸직 동기화는 생성 효율을 위해 먼저 `POST /v1.0/users`를 시도하고, `409 Conflict`일 때 `PATCH /v1.0/users/{email}`로 전환합니다.
|
|
- PUT은 전체 교체 성격이 강하고 누락 필드 초기화 위험이 있으므로 현 scope에서는 사용하지 않습니다. 모든 Baron -> WORKS 변경 반영은 부분 수정 PATCH를 우선합니다.
|
|
|
|
### 구성원 비밀번호 관리 링크
|
|
|
|
Baron SSO는 생성 이후 WORKS Mobile 비밀번호 값을 직접 수정하지 않습니다. 운영자가 비밀번호 수정을 요청할 때는 해당 WORKS 계정의 식별자를 이용해 WORKS Mobile 관리자 비밀번호 관리 화면을 새 창으로 엽니다.
|
|
|
|
사용 URL:
|
|
|
|
```text
|
|
https://auth.worksmobile.com/integrate/password/manage?usage=admin&targetUserTenantId={회사테넌트}&targetUserDomainId={회사도메인}&targetUserIdNo={변경대상works_USER_ID}&accessUrl=https://admin.worksmobile.com/assets/self-close.html
|
|
```
|
|
|
|
전제와 기준:
|
|
|
|
- 브라우저 사용자는 `auth.worksmobile.com`에 관리자 권한으로 로그인되어 있어야 합니다.
|
|
- `targetUserTenantId`는 Baron tenant UUID가 아니라 WORKS Mobile 회사 tenant 식별자입니다. Baron SSO backend는 `WORKS_ADMIN_TENANT_ID` 환경 변수로 이 값을 adminfront overview에 노출합니다.
|
|
- `targetUserDomainId`는 WORKS Mobile 비교 결과의 `worksmobileDomainId`를 사용합니다.
|
|
- `targetUserIdNo`는 WORKS Mobile 비교 결과의 `worksmobileId`를 사용합니다.
|
|
- adminfront는 세 값이 모두 있을 때만 비밀번호 관리 버튼을 활성화합니다.
|
|
- 이 링크는 WORKS Mobile 관리자 화면을 여는 기능이며, Baron SSO backend에서 password 또는 `passwordConfig` 변경 API를 호출하지 않습니다.
|
|
|
|
## 비동기 아키텍처 권장안
|
|
|
|
Worksmobile API를 handler에서 직접 호출하지 않고, 별도 outbox와 relay worker를 둡니다.
|
|
|
|
권장 신규 테이블:
|
|
|
|
- `worksmobile_outbox`
|
|
- `id`
|
|
- `resource_type`: `ORGUNIT`, `USER`
|
|
- `resource_id`: Baron tenant/user UUID
|
|
- `action`: `UPSERT`, `DELETE`, `EXTERNAL_KEY_MAP`
|
|
- `payload`: JSONB
|
|
- `dedupe_key`
|
|
- `status`: `pending`, `processing`, `processed`, `failed`
|
|
- `retry_count`, `last_error`
|
|
- `next_attempt_at`, `processed_at`, `created_at`, `updated_at`
|
|
- `worksmobile_resource_mappings`
|
|
- `baron_resource_type`
|
|
- `baron_resource_id`
|
|
- `external_key`
|
|
- `worksmobile_resource_id`
|
|
- `domain_id`
|
|
- `last_synced_at`
|
|
|
|
권장 신규 service/client:
|
|
|
|
- `backend/internal/service/worksmobile_client.go`
|
|
- `backend/internal/service/worksmobile_sync_service.go`
|
|
- `backend/internal/service/worksmobile_relay_worker.go`
|
|
- `backend/internal/repository/worksmobile_outbox_repository.go`
|
|
|
|
현재 구현은 `WORKS_ADMIN_*` OAuth 또는 directory token을 사용하는 `WorksmobileHTTPClient`와 `WorksmobileRelayWorker`를 통해 `worksmobile_outbox`의 pending 사용자 작업을 Directory API로 전달합니다. 사용자 생성은 `POST https://www.worksapis.com/v1.0/users`를 먼저 호출하고, 이미 존재하는 구성원으로 `409 Conflict`가 발생하면 `PATCH /v1.0/users/{email}`로 전환합니다.
|
|
|
|
SCIM은 주경로로 사용하지 않습니다. 기존 검토에서 SCIM은 다음 이유로 보류했습니다.
|
|
|
|
- Directory API의 `passwordConfig.passwordCreationType = ADMIN` 생성 정책을 그대로 표현하기 어렵습니다.
|
|
- SCIM 경로는 검증 조건 때문에 private mail 성격의 email 값을 강제해야 하는 문제가 있었습니다.
|
|
- Baron 정책은 `privateEmail` 기본 미전송이므로 SCIM을 주경로로 삼지 않습니다.
|
|
|
|
`WORKS_ADMIN_OAUTH_CLIENT_ID`, `WORKS_ADMIN_OAUTH_CLIENT_SECRET`, service account private key는 Directory API 호출용 토큰 발급에 사용합니다. OAuth redirect URI 등록이 필요한 경우 다음 경로를 사용합니다.
|
|
|
|
```text
|
|
http://localhost:5000/api/v1/admin/worksmobile/oauth/callback
|
|
```
|
|
|
|
로컬 Playwright 검증에서는 위 callback 경로가 브라우저에서 도달 가능함을 확인했습니다.
|
|
|
|
Worker 정책:
|
|
|
|
- orgunit 작업은 `domainId`별 단일 worker lane, 최소 1초 간격
|
|
- user 작업은 같은 `userExternalKey` 단위로 순차 처리
|
|
- 전체 동시성은 5 미만
|
|
- 409는 idempotent conflict로 보고 user PATCH 전환
|
|
- 404 parent orgunit은 parent job 선처리 후 retry
|
|
- 429/5xx는 exponential backoff
|
|
|
|
## AdminFront 운영 화면 배치와 권한 정책
|
|
|
|
Worksmobile 운영 화면은 `orgfront`가 아니라 `adminfront`의 tenant detail 하위에 둡니다. 데이터 성격상 tenant 관리자 도구이며, 실제 사용자 동선은 `hanmac-family` tenant detail에서 바로 확인할 수 있게 만드는 쪽이 맞습니다.
|
|
|
|
화면 노출 정책:
|
|
|
|
- 전역 메뉴에는 Worksmobile 메뉴를 추가하지 않습니다.
|
|
- `adminfront` tenant list에서 한맥가족 root tenant detail에 들어갔을 때만 Worksmobile 탭 또는 버튼을 표시합니다.
|
|
- 노출 조건은 tenant detail API 응답의 `slug == "hanmac-family"` 또는 동일한 canonical 식별자로 판단합니다.
|
|
- 별도 운영 URL을 새 탭으로 여는 방식은 허용합니다. 단, 새 탭 화면도 tenant-scoped route여야 하며 URL을 직접 입력해도 같은 권한 검사를 통과해야 합니다.
|
|
- `orgfront`는 필요 시 read-only sync badge나 adminfront deep link 정도만 담당하고, 생성/재시도/삭제 같은 운영 액션은 제공하지 않습니다.
|
|
|
|
권한 강제 정책:
|
|
|
|
- frontend의 탭 숨김은 UX 보조일 뿐이며 보안 경계로 보지 않습니다.
|
|
- backend endpoint는 `/api/v1/admin/tenants/:tenantId/worksmobile/*`처럼 tenant-scoped 형태로 둡니다.
|
|
- handler 또는 middleware에서 `tenantId`가 존재하는지, 해당 tenant가 정확히 `hanmac-family` root인지 먼저 확인합니다.
|
|
- 요청자는 `super_admin`이거나 `Tenant:{tenantId}`에 대한 관리 권한을 Keto로 통과해야 합니다. 기존 `RequireKetoPermission(..., "Tenant", "manage")` 또는 이에 준하는 relation을 사용합니다.
|
|
- user/orgunit 개별 동작은 대상 resource가 `hanmac-family` subtree 안에 있는지 추가로 확인합니다.
|
|
- Worksmobile `domainId`, `userExternalKey`, `orgUnitExternalKey`는 request body의 임의 입력을 신뢰하지 않고 server-side tenant config와 Baron UUID에서 계산합니다.
|
|
- tenant가 `hanmac-family`가 아니거나, 사용자가 해당 tenant를 관리할 수 없거나, 대상 resource가 subtree 밖이면 작업을 생성하지 않고 `403 Forbidden` 또는 존재 노출을 줄이는 `404 Not Found`로 차단합니다.
|
|
|
|
관리 화면에서 필요한 최소 기능:
|
|
|
|
- Worksmobile config/domain mapping 조회
|
|
- 조직/구성원별 최근 sync 상태와 마지막 오류 조회
|
|
- 단건 조직/구성원 sync enqueue
|
|
- 실패 작업 retry
|
|
- backfill dry-run 결과 조회 및 제한된 실행 버튼
|
|
|
|
## 삽입 지점
|
|
|
|
### 기존 계정 bulk/backfill
|
|
|
|
1. `hanmac-family` subtree tenant 전체를 읽습니다.
|
|
2. Worksmobile에 이미 존재하는 조직/구성원의 External Key Mapping을 먼저 수집하거나 Developer Console CSV mapping으로 보정합니다.
|
|
3. Baron tenant를 depth asc로 정렬해 `ORGUNIT UPSERT` outbox를 생성합니다.
|
|
4. Baron user를 `users.id` 기준으로 읽고 `USER UPSERT` outbox를 생성합니다.
|
|
5. `USER UPSERT`는 해당 사용자의 orgunit mapping이 processed인 뒤 실행합니다.
|
|
|
|
### 신규 tenant 생성
|
|
|
|
- 후보 위치: `TenantHandler.CreateTenant`에서 `replaceTenantDomains` 성공 후
|
|
- 더 좋은 위치: `TenantService.RegisterTenant`가 tenant/domain/config/outbox를 하나의 transaction으로 저장하도록 정리한 뒤 같은 transaction 안에서 `worksmobile_outbox`를 생성
|
|
- 조건: 생성된 tenant가 `hanmac-family` subtree 하위이고 type이 `COMPANY` 또는 `USER_GROUP`
|
|
|
|
### tenant 수정
|
|
|
|
- 후보 위치: `TenantHandler.UpdateTenant`에서 `h.DB.Save(&tenant)` 및 domain update 성공 후
|
|
- parent 변경 시 Worksmobile orgunit move 또는 patch가 필요합니다.
|
|
- 현재 `UpdateTenant`는 Keto parent outbox를 tenant 저장 전에 생성하므로, Worksmobile 이전에 outbox transaction 정합성 개선을 권장합니다.
|
|
|
|
### 신규 사용자 단건 생성
|
|
|
|
- 후보 위치: `UserHandler.CreateUser`에서 Kratos 생성, 허용된 Backend read model sync, login ID sync, Keto outbox enqueue 후
|
|
- payload에는 `identityID`, email, name, phone, tenantID, metadata/additionalAppointments를 포함합니다.
|
|
- `hanmac-family` scope가 아니면 enqueue하지 않습니다.
|
|
|
|
### 신규 사용자 bulk 생성
|
|
|
|
- 후보 위치: `UserHandler.BulkCreateUsers`에서 row별 허용된 Backend read model sync와 Keto outbox enqueue 후
|
|
- row별 partial success를 유지하고, Worksmobile enqueue 실패는 사용자 생성 실패와 분리하는 것이 좋습니다.
|
|
- 단, enqueue 실패는 audit/error로 남기고 운영자가 재시도할 수 있어야 합니다.
|
|
|
|
### 사용자 수정/소속 변경
|
|
|
|
- 후보 위치: `UserHandler.UpdateUser`에서 중앙 `IdentityWriteService` 기반 Kratos update와 허용된 Backend read model sync 후
|
|
- `email`, `name`, `phone`, `companyCode`, `tenant_id`, `metadata.additionalAppointments` 변경 시 `USER UPSERT` enqueue
|
|
- `suspended`는 Worksmobile suspend로 동기화합니다.
|
|
- `temporary_leave`는 Worksmobile 계정을 유지합니다.
|
|
- `preboarding`은 Worksmobile 계정을 생성하지 않습니다.
|
|
- `baron_guest`, `extended_leave`, `archived`는 Worksmobile delete/deprovision으로 동기화합니다.
|
|
- Baron user delete는 Worksmobile delete로 동기화합니다.
|
|
- 기존 `inactive` 입력은 `preboarding`, `leave_of_absence` 입력은 `temporary_leave`, `baron_only` 입력은 `baron_guest`로 호환 처리합니다.
|
|
- backend bootstrap은 위 legacy `users.status` 값이 남아 있으면 canonical 상태값으로 자동 정규화합니다.
|
|
|
|
## 테스트 전략
|
|
|
|
기능 추가이므로 테스트를 먼저 작성합니다.
|
|
|
|
### Backend unit
|
|
|
|
- Worksmobile request mapper
|
|
- tenant UUID -> `orgUnitExternalKey`
|
|
- parent tenant -> `parentOrgUnitId = externalKey:{parentID}`
|
|
- Kratos UUID -> `userExternalKey`
|
|
- `additionalAppointments` -> `organizations[].orgUnits[]`
|
|
- #668 email final value 사용
|
|
- External Key validator
|
|
- `%`, `\`, `#`, `/`, `?` 포함 시 reject
|
|
- Domain mapping resolver
|
|
- email domain -> `domainId`
|
|
- missing mapping -> blocking error 또는 skipped job
|
|
|
|
### Backend repository/service
|
|
|
|
- `worksmobile_outbox` enqueue idempotency
|
|
- orgunit depth order enqueue
|
|
- user job이 orgunit processed 전에는 보류되는지 확인
|
|
- retry/backoff/status transition
|
|
- 409 conflict 시 get/patch 전환
|
|
|
|
### Handler
|
|
|
|
- `CreateTenant`가 한맥가족 subtree tenant 생성 시 `ORGUNIT UPSERT` job을 생성
|
|
- `CreateUser`가 한맥가족 사용자 생성 시 `USER UPSERT` job을 생성
|
|
- 한맥가족 외부 tenant는 job을 만들지 않음
|
|
- `BulkCreateUsers`에서 row별 성공 결과와 Worksmobile enqueue 결과가 분리됨
|
|
|
|
### Frontend unit
|
|
|
|
- CSV alias가 Worksmobile/Baron 컬럼을 정상 매핑하는지 확인
|
|
- `additionalAppointments`를 유지한 채 사용자 생성/수정 payload가 만들어지는지 확인
|
|
- tenant detail이 `hanmac-family` root일 때만 Worksmobile 탭/버튼을 표시하는지 확인
|
|
- non-hanmac tenant detail이나 직접 URL 접근에서 Worksmobile 운영 화면이 차단되는지 확인
|
|
|
|
### E2E/manual
|
|
|
|
- adminfront에서 `hanmac-family` 하위 조직 생성 -> outbox 생성 -> mock Worksmobile orgunit create 확인
|
|
- bulk import로 기존 계정 생성 -> #668 email preview -> Worksmobile user create mock 확인
|
|
- 신규 구성원 단건 생성 -> Worksmobile user create mock 확인
|
|
- orgfront 조직도 표시가 기존 `joinedTenants`/`additionalAppointments` 기준과 충돌하지 않는지 확인
|
|
- adminfront tenant list -> 한맥가족 detail -> Worksmobile 운영 화면 새 탭 -> 단건 sync 결과 확인
|
|
- non-hanmac tenant detail에서는 Worksmobile UI가 보이지 않고 직접 URL 접근도 backend에서 차단되는지 확인
|
|
|
|
## 주요 리스크와 선행 결정
|
|
|
|
1. `domainId` mapping 관리
|
|
- Worksmobile API는 `domainId`가 필수입니다.
|
|
- mapping 위치는 `tenant.config.worksmobile.domainMappings`로 결정했습니다.
|
|
- 개발/스테이징에서 검증한 값이 프로덕션에도 적용되어야 하므로 코드 레벨 seed/config로 추적 가능해야 합니다.
|
|
|
|
2. `additionalAppointments` backend 정규화
|
|
- 현재 frontend는 값을 보내지만 backend 단건 생성 path는 구조적으로 처리하지 않습니다.
|
|
- Worksmobile 연동 전 Baron 내부 membership과 metadata 정합성을 먼저 맞춰야 합니다.
|
|
|
|
3. transaction 정합성
|
|
- 현재 tenant create/update와 Keto outbox는 완전한 transactional outbox가 아닙니다.
|
|
- Worksmobile outbox는 신규 구현 시 DB 변경과 같은 transaction으로 enqueue되도록 설계하는 것이 좋습니다.
|
|
|
|
4. Worksmobile orgunit rate limit
|
|
- 조직 API는 도메인당 단일 스레드/1초 1회 제약이 있으므로 worker lane 설계가 필수입니다.
|
|
|
|
5. 기존 Worksmobile 데이터 backfill
|
|
- Developer Console External Key Mapping이 비어 있는 기존 조직/구성원은 CSV mapping 또는 API external-key update가 선행되어야 합니다.
|
|
|
|
6. 상태 정책
|
|
- Baron `active`, `temporary_leave`, `suspended`는 Worksmobile 구성원 비교 및 backfill scope에 포함합니다.
|
|
- Baron `suspended`는 Worksmobile suspend로 동기화합니다.
|
|
- Baron `preboarding`은 Worksmobile 계정을 생성하지 않습니다.
|
|
- Baron `baron_guest`, `extended_leave`, `archived`는 Worksmobile delete/deprovision으로 동기화합니다.
|
|
- Baron delete는 Worksmobile delete로 동기화합니다.
|
|
- 직급/직책/사용자 유형 External Key sync는 이번 scope에서 제외합니다.
|
|
|
|
7. adminfront 권한 경계
|
|
- Worksmobile 운영 화면은 `hanmac-family` root tenant detail에서만 보이게 합니다.
|
|
- 별도 URL/새 탭은 허용하지만 backend는 tenant slug, Keto 관리 권한, subtree membership을 모두 확인해야 합니다.
|
|
- UI hidden state만으로 접근 제어를 대신하지 않습니다.
|
|
|
|
## 권장 구현 순서
|
|
|
|
1. Worksmobile integration config와 `tenant.config.worksmobile.domainMappings` seed/config 정책 확정
|
|
2. `additionalAppointments` backend DTO/parser/Keto membership sync 정리
|
|
3. Worksmobile mapper unit test 작성 후 RED 확인
|
|
4. `worksmobile_outbox` repository/service/worker 구현
|
|
5. tenant orgunit enqueue 및 mock client GREEN
|
|
6. user enqueue 및 mock client GREEN
|
|
7. backfill command 또는 admin API dry-run 구현
|
|
8. adminfront 상태/재시도 UI 또는 최소 운영 조회 API 추가
|
|
9. E2E/mock integration 검증
|
|
|
|
## 문서 업데이트 후보
|
|
|
|
- `README.md`: 관리 데이터 import 정책에 Worksmobile sync 상태와 External Key 원칙 추가
|
|
- `docs/organization-chart-policy.md`: Worksmobile orgunit mapping 정책 추가
|
|
- `docs/tenant-usergroup-policy.md`: 외부 Directory sync outbox 정책 추가
|
|
- `docs/worksmobile-directory-sync-technical-review.md`: 본 문서를 위키 반영 전 검토본으로 유지
|