Meta Account (플랫폼 계정) 설계 — v1.1 (리뷰 확정)
2026-07-23 작성, 같은 날 리뷰 확정. branch
feat/system-account코드 정찰 + 2026-07-23 E2E 검증(트랙 A) 기반. 결정 반영: impersonation 포함 / 동의는 계약·약관 레벨(시스템 강제 없음) / AuditLog 본체는 다음 범위 / 오픈 퀘스천 4건 전부 확정(§7).
1. 배경과 목표
Account(전역 신원) ↔ Employee(조직 멤버십) 분리로 무소속 회원이 정상 상태가 되었다. 다음 단계는 조직에 소속되지 않은 계정이 조직 안에서 조회·업무수행을 할 수 있는 경로다. 이 하나의 개념("조직 안에서 행동할 권리")으로 아래 세 요구를 통일한다.
- Owner without Employee — 생성자(owner)가 조직의 직원이 아니어도 자기 조직에 진입해 기능을 조회·테스트할 수 있어야 한다. (owner는 특별 권한이 아니라 "생성자 표식")
- 사람 assistant (support) — 플랫폼 스태프가 고객 조직에 들어가 IT에 익숙하지 않은 사용자를 직접 도와준다. 특정 사용자의 화면 그대로 보는 impersonation 포함.
- AI assistant — AI가 조직 컨텍스트에서 사용가이드·업무수행을 돕는다. 사람 assistant가 AI의 미비점을 이어받는 핸드오프 구조.
2. 리서치 요약 (타 시스템)
| 시스템 | 모델 | 우리가 가져올 것 |
|---|---|---|
| Slack / Notion / GitHub | 전역 계정 1 : N 워크스페이스 멤버십 | 이미 동형 구조 (Account/Employee) |
| Salesforce "Login As" | 지원 접근을 고객이 허용 → 지원자가 해당 유저로 로그인, 전 행동 기록 | impersonation UX + 기록 원칙 |
| AWS STS AssumeRole | 시간제한·스코프제한 임시 자격 증명 | grant의 expiresAt + 스코프 |
| Google 도메인 위임 | 서비스(봇)가 사용자를 대행하되 스코프 초과 불가 | AI의 "호출자 ctx 실행" 불변식 유지 |
공통 불변식은 동의·기록·만료. 이번 범위 결정에 따라 동의는 계약/약관으로 갈음하고, 기록(AuditLog)은 다음 범위로 미루되 스키마가 이미 수용하도록 설계한다.
3. 현재 구조 진단 (정찰 확정 사실)
- 인가는 단일 관문
resolveResourceScope(packages/api/src/trpc.ts:201)를 지난다. org 프로시저와 AI tool 레이어 모두 여기서 스코프를 해석한다.isSystemAdmin단락(:207)이 이미 존재한다. - 신원 해석은
context.ts에서ctx.user(org 세션,userIdJWT) XORctx.account(account 세션,accountIdJWT). - 조직 진입은 ACTIVE Employee row 하드 요구 —
account.selectWorkspace(account.router.ts:178)가 유일한 관문. Employee 없이 org 세션을 얻는 경로는 없다. isSystemAdmin은 Employee 컬럼(조직 단위). Account 레벨 플랫폼 롤 없음. impersonation/on-behalf/service account 개념 0건.- AI tool은
(db, user, organizationId)의 순수 함수 — 합성 User만 주면 무변경 동작. tools/system-admin은 tRPC 신원 체계 밖(단일 계정 + raw pg). 이번 설계는 in-app tRPC 신원 체계 확장이며 sysadmin 콘솔은 그대로 둔다.
4. 설계
4.1 개념 모델 — 조직 안에서 행동할 수 있는 세 경로
Account ──(1) Employee 멤버십───────────→ 조직 (영구, 기존 그대로)
──(2) OrganizationGrant────────→ 조직 (시간제한·스코프제한, 신규)
──(3) Ownership (암묵 grant)───→ 자기 조직 (ownerAccountId, 신규 해석)권한 레이어는 "어느 경로로 왔는가"를 몰라도 되도록, 진입 시점에 합성 User로 정규화한다.
4.2 스키마
accounts.platformRole (varchar, null) — support | operator. 플랫폼 스태프 표식. Employee.isSystemAdmin(조직 단위)과 무관한 Account 레벨 속성.
organization_grants (신규 테이블)
| 컬럼 | 타입 | 설명 |
|---|---|---|
| id | uuid PK | |
| accountId | uuid FK→accounts | 행동 주체 |
| organizationId | uuid FK→organizations | 대상 조직 |
| kind | varchar | owner(암묵, 행 없이 해석) 제외 — support | custom |
| roleCode | varchar null | 부여할 조직 역할 코드 (null이면 grantedPermissions 사용) |
| grantedPermissions | jsonb null | "resource:action.scope" 문자열 배열 (roleCode와 택1) |
| impersonateEmployeeId | uuid null | impersonation 대상 (null = 대행 모드) |
| reason | text | 사유 (지원 티켓 번호 등) |
| grantedByAccountId | uuid | 발급자 (플랫폼 operator) |
| expiresAt | timestamp null | 만료. null = 영구 grant (장기·영구 허용 — 리뷰 확정) |
| revokedAt | timestamp null | 조기 회수 |
| BaseEntity | createdAt/updatedAt/deletedAt |
- 동의 컬럼은 두지 않는다(계약·약관으로 갈음, 결정 ②). 단,
reason은 필수로 남겨 누가 왜는 항상 기록되게 한다. - AuditLog는 다음 범위지만, grant 행 자체가 "접근 존재 사실"의 1차 기록이 된다. 이후 AuditLog 도입 시
grantId를 FK로 참조하면 되므로 스키마 변경 없이 확장 가능.
4.3 세션과 신원 해석
JWT 확장 — org-scope 토큰에 위임 마커 추가:
{ "accountId": "...", "organizationId": "...",
"grantId": "...", // grant 기반 진입
"impersonateEmployeeId": "..." // impersonation 시
// userId 없음 — Employee가 아니므로
}context.ts 확장 — 제3의 해석 경로:
userId있으면 기존resolveUser(불변).accountId+grantId있으면resolveGrantUser: grant 유효성(만료·회수) 검증 → 합성 User 구성:- 대행 모드:
id = "grant:" + grantId(실 Employee 아님을 표기),permissions= roleCode 해석 또는 grantedPermissions,organizationId= grant 대상. - impersonation 모드: 대상 Employee를 로드해 그 신원·권한을 그대로 사용하되,
ctx.realActor = { accountId, grantId }를 병기.
- 대행 모드:
- owner 암묵 grant:
accountId만으로organizations.ownerAccountId일치 시, grant 행 없이 SUPER_ADMIN 상당 전권의 합성 User 발급(리뷰 확정 — 전용 프로시저로만, 4.4).
User 타입에 actingVia?: { grantId, realAccountId, impersonating: boolean }을 추가한다. 기존 코드는 이 필드를 몰라도 동작한다.
4.4 진입 API (account.router 확장)
account.enterOwnedWorkspace({ organizationId })— owner 암묵 grant.ownerAccountId === ctx.account.id확인 후 org 세션 발급. 허브 목록에 owner 조직은 Employee 없이도 노출("owner로 입장" 라벨).account.enterGrantedWorkspace({ grantId })— 유효 grant로 org 세션 발급.impersonateEmployeeId세팅 시 impersonation 세션.platform.grants.*(operator 전용 신규 라우터) — grant 발급/조회/회수.platformRole = operator만 발급 가능,support는 사용만.
4.5 인가 관문 처리
resolveResourceScope(trpc.ts:201)는 변경 최소화: 합성 User의 permissions가 이미 스코프 문자열이므로 기존 로직이 그대로 동작한다. impersonation은 대상 직원의 권한을 그대로 쓰므로 역시 무변경. 즉 관문 코드는 건드리지 않고 신원 구성 단계에서 해결한다 — 이것이 이 설계의 핵심 이점.
주의 가드 2개만 추가:
- grant 세션은
superAdminProcedure접근 불가(플랫폼 스태프가 조직 삭제 등 파괴 행위 금지 — grant 스코프에system:manage부여 금지 검증). - 자격증명·비밀번호 변경 등 신원 계열 프로시저는 impersonation 세션에서 차단(
ctx.user.actingVia?.impersonating검사).
4.6 UI
- 허브: owner 조직(Employee 없음) 항목 노출 + "owner로 입장". support 계정은 "지원 중인 조직" 섹션(유효 grant 목록).
- 조직 화면: grant/impersonation 세션이면 상단 배너 고정 — "지원 모드: ○○ 조직 (만료 D-2)" / "○○○ 님으로 보는 중 — [내 세션으로 복귀]". Salesforce Login-As와 동일한 상시 표시 원칙.
- admin 접근 제어와의 관계: E2E 발견 #5(일반 직원 admin 앱 막다른 골목)와 함께, 허브·grant 진입은 앱별 접근 제어보다 앞단에서 동작해야 한다. 후속 이슈로 연결.
4.7 AI / 사람 assistant 연결
- AI tool은 합성 User를 그대로 받는다 — support가 grant로 진입해 AI를 부르면 AI 스코프 = grant 스코프(초과 불가). 코드 변경 없음.
- 핸드오프: "AI가 못 푼 문제를 사람이 이어받는다" = 같은 조직 grant 스코프에서 사람 세션이 이어받는 것. Phase C에서 대화 컨텍스트 공유(ai_conversations 참조 전달)만 추가하면 된다.
5. E2E 검증에서 발견된 연관 갭 (2026-07-23, 트랙 A)
이 설계와 직접 맞물리는 기존 갭 — Phase A 전후로 함께 처리 권장:
- #6 기존 계정 이메일로 직원 생성 → 합성 계정 분열 (겸직 시나리오 차단). 제안: 미연결 생성 + 초대 자동 발송.
- #5 일반 직원/비관리 조직의 admin 앱 막다른 골목 — 허브·수락 화면과 앱 접근 제어 분리.
- #1 무소속 계정 프로필 수정 API/UI 부재 (
account.updateProfile,account.changePassword). - #3 팀/역할 없는 직원 권한 0 — 기본 EMPLOYEE 역할 자동 부여 검토.
- #4 초대 수락 직후 stale
mustChangePassword리다이렉트 (경미).
6. 단계 계획 (진행 현황)
- Phase A (기반) — ✅ 완료 (2026-07-23):
platformRole+organization_grants마이그레이션(1863),resolveOwnerGrantUser, owner 암묵 grant +enterOwnedWorkspace, 허브 owner 입장 UI. - Phase B (지원) — ✅ 완료 (2026-07-23):
platform.grants.*라우터(OPERATOR 전용),enterGrantedWorkspace(대행+impersonation),resolveGrantUser, changePassword 위임 차단 가드, 지원 모드/impersonation 배너, 허브 "지원 중인 조직" 섹션, sysadmin Platform Staff 탭(platformRole 부여/해제 — 해제 시 유효 grant 자동 회수). - Phase C (기록 + AI) — ✅ 핵심 완료 (2026-07-23):
- AuditLog 도입 (마이그레이션 1864,
audit_logs): 위임 세션(OWNER/GRANT/IMPERSONATION)의 전 tRPC 호출(성공·실패 포함) + operator의 grant 발급·회수를 append-only 기록. 실행위자(actorAccountId) + 유효 신원(effectiveUserId) 이중 기록, grantId FK, 민감 키 레닥션(password/token류) + 4KB 절단. 기록은 fire-and-forget(원 요청 무영향). 조회는platform.auditLogs(OPERATOR 전용). 일반 직원 세션은 기록하지 않음(별도 범위). - AI grant-스코프 상속 확인: grant에
ai:execute없으면 거부, 있으면 권한 게이트 통과(조직settings.ai.enabled게이트는 그대로) — AI tool 레이어 코드 무변경으로 상속 동작 검증 완료. - 남은 항목: 핸드오프 컨텍스트 공유(ai_conversations 참조 전달 — 사람↔AI 이어받기 UX)는 AI 기능 제품 범위와 묶어 별도 진행.
- AuditLog 도입 (마이그레이션 1864,
7. 확정 결정 (2026-07-23 리뷰)
- owner 암묵 grant = SUPER_ADMIN 상당 전권. (단, §4.5의 가드대로
superAdminProcedure계열 파괴 행위는 grant 세션 공통 정책을 따름 — owner의 조직 삭제 등은 기존 owner 정책 그대로) - grant는 장기 허용, permanent(만료 없음) 가능.
expiresAtnullable — null = 영구. 발급 UI 기본값은 무기한, 필요 시 만료 지정. - impersonation 알림 없음. 대상 직원에게 통지하지 않는다 (Salesforce Login-As와 동일). 상시 배너는 지원자 화면에만 표시.
platformRole부여는 sysadmin 콘솔 수동 부여로 충분. 별도 셀프서비스/승인 플로우 없음 — sysadmin 콘솔에 Account 검색 + platformRole 지정 UI 추가(Phase B).