Files
baron_qa_write/docs/sso-tenant-organization-review.md
T
root ccf48c8224
Deploy EG-BIM QA Gateway / deploy (push) Successful in 1m24s
fix: use supported redirect mode for Workers organization requests
2026-10-30 12:49:15 +09:00

13 KiB

SSO 사용자 테넌트와 회사·부서 매핑 확인

확인일: 2026-10-30

확인 결과

사용자 소속 목록과 조직 계층을 조회하는 경로가 있다. 다만 로그인 claim, 본인 가입 테넌트, Integration Credential로 조회하는 조직 subtree는 서로 다른 범위다. tenants scope를 요청했다고 사용자의 모든 소속과 모든 상위 조직이 자동으로 전달된다고 가정하면 안 된다.

요청자가 확인한 표시 기준은 다음과 같다. 이 이름은 기대 결과이며 실제 SSO 응답과 UUID를 조회해 검증한 결과는 아니다.

항목 기대 값
회사 총괄기획실&기술개발센터
부서 IS3

사용자 추가 확인: STAGING_ORG_CONTEXT_TENANT_IDS에는 회사나 IS3가 아닌 한맥가족 등 최상위 분류 조직의 UUID를 등록했다. UUID 원문과 실제 조직 응답은 아직 확인하지 않았다.

실제 세션 응답으로 확인된 상태

사용자가 전달한 로그인 후 /auth/session 응답의 관련 필드는 다음과 같다. 개인 연락처와 사용자 UUID는 이 문서에 복사하지 않는다.

{
  "authenticated": true,
  "user": {
    "tenantName": "IS3",
    "company": "IS3",
    "companySource": "sso",
    "department": "",
    "orgContextStatus": "unavailable"
  }
}

이 응답으로 현재 사용자 조직 조회·응답 처리에 실패하여 기존 SSO 값이 유지되고 있음은 확인됐다. 조직 조회에 성공한 뒤 회사 노드를 잘못 선택했다고 확정할 수 있는 상태는 아니다. 코드상 unavailable은 구성 검사, HTTP 호출, 응답 해석·계층 검증 등의 예외를 함께 나타낸다. 사용자 UUID를 찾지 못한 경우의 not_found, 복수 소속을 선택하지 못한 경우의 ambiguous_membership과도 다르다.

최근 스테이징 배포 로그에서는 org:verify가 rootCount=7, tenantCount=560, userCount=4607, membershipCount=5370으로 성공했다. 배포 당시 CI에서 키·조회 범위·응답 처리 전체가 성공했다는 증거이며 현재 Worker에서도 성공한다는 증거는 아니다.

CI 검증은 요청당 15초, 실제 Worker 조회는 요청당 5초 제한이다. Worker의 org_context_timeout, org_context_http_401, org_context_http_403, org_context_request_failed 등 정확한 로그 코드를 확인해야 통신·시간 제한·runtime credential 문제를 구분할 수 있다. 세션 응답에 있는 빈 scope나 중복 tenantIds만으로 조직 API 실패 원인을 단정하지 않는다.

사용자가 추가로 전달한 로그는 /auth/session의 HTTP 200 요청 기록이며 outcome=ok, wall_time_ms=3이다. 코드가 조직 예외를 처리한 뒤 정상 HTTP 응답을 반환하므로 이 요청 기록에는 내부 오류가 직접 표시되지 않을 수 있다. 이 시간만으로 타임아웃을 확정할 수 없다.

로그 조회만으로 원인을 구분하지 못한 문제를 보완하기 위해 user.orgContextError에 허용된 오류 코드만 추가했다. 임의의 upstream 메시지는 숨기고, 성공 시 이전 오류 값은 제거한다. 배포 후 /auth/session의 이 필드로 원인을 확인할 수 있으며 실제 사용자 조직 조회 실패 자체를 해결한 변경은 아니다.

후속 확인: Workers 런타임에서 실패하는 코드 수정

진단 보완 배포 후 사용자가 제공한 오류는 org_context_request_failed였다. 조직 API 호출에 지정한 redirect: "error"가 Workers에서 지원되지 않아 요청 전 TypeError를 발생시키는 결함을 확인했다. Node.js는 이 옵션을 허용하므로 CI의 API 검증만 성공할 수 있었다. Cloudflare Request 문서에도 이 제한이 명시되어 있다.

동일한 배포 compatibility date를 사용한 실제 workerd 테스트에서 수정 전 /auth/session이 약 3ms 만에 org_context_request_failed와 unavailable을 반환하는 것을 재현했다. redirect: "manual"로 변경한 뒤에는 모의 조직 응답을 사용한 동일한 실제 런타임 경로에서 회사 총괄기획실&기술개발센터, 부서 IS3, 상태 resolved를 확인했다. 302 응답은 따라가지 않고 org_context_http_302로 거부하는 검사도 통과했다.

수정 후 전체 테스트는 77개 통과했다. 키·테넌트 설정 변경 없이 요청 옵션을 수정했으며, 실제 사용자에게 발급된 키로 원본 조직 데이터를 조회하거나 운영 배포를 수행한 것은 아니다. 사용자가 push하고 스테이징 배포가 성공한 뒤 실제 /auth/session에서 resolved와 회사·부서를 확인해야 한다. 과거 잘못 저장된 글의 정정은 여전히 별도 작업이다.

조회 경로

경로 확인한 필드·범위 Q&A 적용 시 주의점
OIDC /oidc/userinfo 및 ID token RP scope·claim 정의와 동의에 따라 전달되는 사용자 claim 회사·부서·상위 계층과 전체 소속이 항상 있다는 계약은 가이드에서 확인되지 않음
GET /api/v1/user/me 공개 OpenAPI의 UserProfileResponse에 id, department, tenant, joinedTenants[]가 있음 본인 프로필·가입 테넌트 목록 확인에 사용. TenantResponse에는 id, name, type, parentId가 있지만 ancestors는 정의되어 있지 않음
GET /api/v1/integrations/org-context 허용된 subtree의 scope, tree, tenants[], 직접 소속된 members[] 여러 허용 subtree를 조회하고 동일 사용자 UUID의 소속을 모으면 해당 조회 범위 안의 복수 소속을 찾을 수 있음

joinedTenants[]는 가입 테넌트 목록을 받는 필드다. 모든 상위 조직까지 자동으로 포함하는 배열인지, 실제 로그인 사용자 응답에서 어떤 목록을 반환하는지는 별도 확인이 필요하다. 현재 Q&A는 /user/me를 호출하지 않고, 서버와 브라우저의 정규화 코드도 joinedTenants를 읽지 않는다.

/user/me는 사용자 인증 문맥으로 호출하는 API다. Integration Key를 이 경로의 인증 수단으로 바꾸지 않는다. 기존 AUTH_USERINFO_URL을 무조건 /user/me로 교체해서도 안 된다. 기존 OIDC sub와 프로필의 canonical id를 대조하며 보충 정보를 결합해야 한다.

조직 API 호출은 다음 계약을 사용한다.

GET https://sadmin.hmac.kr/api/v1/integrations/org-context?tenantId=<허용된-조회기준-UUID>&exposure=identifiers
X-Baron-Key-ID: <서버에 등록된 키 ID>
X-Baron-Key-Secret: <서버에 등록된 시크릿>
Accept: application/json
  • OperationID: baron.org-context.read.
  • tenantId, tenantSlug 중 정확히 하나를 지정한다. 조회 기준을 생략한 전체 조직 조회 계약은 없다.
  • credential이 허용한 subtree 및 exposure 범위만 조회할 수 있다. 여러 조직에 겸직 중이어도 조회 범위 밖의 소속까지 확인된 것으로 처리하지 않는다.
  • schemaVersion 예시는 baron.org-context.v2다.
  • members[].id는 canonical 사용자 UUID이며 members[]는 해당 테넌트의 직접 소속 사용자 목록이다.
  • isPrimary는 대표 소속 선택의 근거다. 복수 소속을 수집한 뒤 현재 테넌트 문맥과 대표 소속을 함께 판단한다.
  • members exposure에도 canonical UUID가 포함된다. identifiers에는 추가로 전화번호가 포함되므로, 전화번호도 사용하는 현재 구현에서 identifiers 요청 자체가 잘못된 것은 아니다.
  • 구성원의 회사 이메일이나 이름으로 사용자 ID를 대신 맞추지 않는다.

현재 코드에서 재현된 매핑 오류

src/org-context.js의 buildOrgDirectory()는 모든 조회 기준 UUID를 분류 루트로 취급하고, 그 바로 아래 노드를 회사로 선택한다. src/index.js의 로그인 claim 정규화에도 같은 가정이 있다.

조회 범위를 회사 자체로 설정하는 것은 API 계약상 가능한 요청이다. 하지만 아래처럼 회사에서 시작하는 응답을 현재 코드에 전달하면 회사가 한 단계 밀린다.

조회 기준: 총괄기획실&기술개발센터
└─ IS3
   └─ 해당 사용자 UUID의 membership

기대: 회사=총괄기획실&기술개발센터, 부서=IS3
현재 코드의 재현 결과: 회사=IS3, 부서="", 상태=resolved

이 결과는 합성 UUID를 사용한 로컬 재현이며, 회사 기준 조회에 존재하는 코드 결함이다. 사용자 확인에 따르면 실제 조회 기준은 최상위 분류 조직이므로 이 사례를 현재 스테이징 문제의 원인으로 확정할 수 없다.

반대로 한맥가족 → 총괄기획실&기술개발센터 → IS3 → 사용자 membership 계층을 현재 코드에 전달하면 회사와 부서가 기대대로 매핑된다. 이 경우도 로컬 합성 데이터로 확인했다. 실제 화면의 원인을 구분하려면 다음을 대조해야 한다.

확인 지점 대조할 값 구분할 문제
Q&A 로그인 후 /auth/session user.orgContextStatus, company, department, companyTenantId, departmentTenantId, departmentPath 조직 조회 실패·사용자 연결 실패와 잘못된 조직 선택을 구분
조직 API의 해당 사용자 소속 조회 기준부터 membership 노드까지의 id, name, parentId, type, orgUnitType, isPrimary 실제 계층과 대표 소속을 확인. 회사가 IS3의 상위 노드로 제공되는지 확인
해당 글의 API 응답 requester_affiliation, requester_department, tenant_name 현재 프로필은 정상이나 과거 저장 값 또는 표시 fallback이 잘못됐는지 구분

전체 디렉터리, 키, 시크릿, 쿠키, access token을 공유할 필요는 없다. 서버 측 진단은 해당 사용자 UUID에 연결된 소속 경로와 정규화 결과만 대상으로 한다.

추가로 확인한 코드 경로:

  • normalizeClaims()는 tenants와 tenantIds를 읽지만 joinedTenants는 읽지 않는다. 첫 번째 테넌트 선택도 대표 소속 보장을 대신할 수 없다.
  • 로그인 claim의 primaryTenant.ancestors를 사용하지만 /user/me의 TenantResponse 계약에는 해당 필드가 없다. 전체 계층이 자동으로 들어온다고 기대할 수 없다.
  • egbim/app.js의 normalizeRemotePost()는 회사 필드가 없으면 tenant_name까지 회사 후보로 사용한다. 그 값이 부서 이름이라면 IS3가 회사로 표시될 수 있다.
  • 신규 글 저장 시 requester_affiliation과 requester_department에 서버가 계산한 값을 저장한다. 이미 잘못 저장된 글은 로그인 사용자 프로필만 고쳐도 자동으로 바로잡히지 않는다.

필요한 후속 작업

  1. 사용자가 확인한 최상위 분류 조직 기준을 유지한다. 확인된 Workers 미지원 요청 옵션의 수정을 배포하고 실제 /auth/session의 상태·회사·부서를 확인한다. 이후에도 잘못된 값이 있으면 해당 사용자의 canonical UUID 및 회사·IS3 UUID를 원본 조직 응답에서 대조한다.
  2. 조회 범위와 회사 판별 기준을 구분한다. API의 type, orgUnitType, parentId와 실제 계층을 확인하고, 의미를 구분할 수 없는 동일 타입 계층은 별도의 명시적 회사 기준을 사용한다. 조회 기준이라는 이유만으로 그 노드를 회사 후보에서 제외하지 않는다.
  3. 모든 허용 subtree의 membership을 사용자 UUID로 모으고, 현재 인증 테넌트 문맥과 isPrimary로 소속을 선택한다. /user/me의 joinedTenants를 도입할 경우 기존 OIDC 식별자를 유지하면서 보충 정보로 결합한다.
  4. 신규 저장에서 회사 총괄기획실&기술개발센터, 부서 IS3가 각각 올바른 필드로 나가는지 확인한다. 회사 기준 조회·분류 루트 조회·부서 기준 조회·겸직을 구분해 검증한다.
  5. 기존 글 중 잘못 저장된 항목의 정정 범위를 별도로 확인한다. 현재 조회자의 조직을 다른 작성자의 글에 덮어쓰거나 과거 전보 이력을 현재 소속으로 일괄 대체하지 않는다.

초기 확인에서는 조사만 진행했고, 후속 확인에서 안전한 오류 코드 진단과 Workers 요청 옵션 수정을 진행했다. 조직 소속과 기존 글 데이터는 변경하지 않았다. 실제 사용자 응답은 사용자가 제공한 /auth/session 결과만 확인했고, 사용자 token이나 Integration Credential로 직접 원본 조직 API를 조회하지 않았다.

확인 자료