다크모드
BE handoff — Organization·Workspace IAM 계약
정본
Leyve의 고객 테넌트는 Organization, 실제 업무·장부 범위는 Workspace다. Supabase 프로토타입도 이 명칭을 물리 스키마, RPC, Edge 응답, Auth metadata까지 동일하게 사용한다.
- baseline:
supabase/migrations/20260822000000_organization_workspace_canonical_baseline.sql - SignIn Edge Function:
supabase/functions/iam-sign-in/index.ts - 문맥 선택 Edge Function:
supabase/functions/iam-context/index.ts - 검증:
supabase/tests/organization_workspace_canonical_core_smoke.sql,supabase/tests/organization_workspace_catalog_zero_smoke.sql
기존 명칭을 위한 table, view, RPC, payload alias는 제공하지 않는다. canonical baseline 자체가 최종 Prototype DB blueprint이며, 전환용 forward migration 없이 이 baseline에서 DB를 새로 만든다.
범위 모델
organizations: 계약·결제 주체이자 고객 테넌트의 단일 본체다. 표시명, slug, 사업자등록번호, 상태, authorization epoch를 소유한다.workspaces: Organization에 속한 업무·장부 범위다. 코드, 불변 순번, edition, 주소, 상태, authorization epoch를 소유한다.organization_affiliations: 조직별 프로필·좌석 수명주기이며 접근권을 만들지 않는다.organization_memberships,workspace_memberships: 실제 접근 관계다. Workspace-only 사용자는 Organization Affiliation을 가지되 Organization Membership이 없어도 된다.organization_role_assignments,workspace_role_assignments: 역할을 Principal이 아니라 특정 Membership 세대에 바인딩한다.- 모든 Workspace 종속 데이터는
organization_id,workspace_id를 사용한다. 서로 다른 Organization의 Workspace를 연결하지 못하도록 복합 FK를 둔다.
organizations/workspaces와 별도의 표시용 scope adapter는 두지 않는다. 고객 계정과 IAM scope는 동일 UUID와 동일 행을 사용한다.
Canonical Role 계약
| Scope | hierarchy |
|---|---|
| Organization | organization.owner > organization.admin > organization.manager > organization.viewer |
| Workspace | workspace.owner > workspace.admin > workspace.manager > workspace.viewer |
- 두 scope 모두 Owner는 Admin, Admin은 Manager, Manager는 Viewer Permission을 포함하는 누적 계층이다.
- Owner는 소유권·Owner 부여·회수·이전·종료, Admin은 설정·Membership·Owner 아래 Role, Manager는 일상 업무 데이터·비민감 운영 상태, Viewer는 조회만 담당한다. Organization의 계약·결제는 Organization Owner에게만 허용한다.
- Viewer 조회는 명시된 일반 Workspace 데이터로 한정한다. 타인의 근태 이벤트·스케줄·예외는 Manager 이상, 법적 고용계약은 Owner/Admin 또는 exact-session 자기 주체 계약으로만 읽는다.
- Organization Role은 산하 모든 Workspace에 같은 등급으로 투영한다.
owner→owner,admin→admin,manager→manager,viewer→viewer다. 별도 opt-in flag나 Workspace Membership을 요구하지 않는다. - Workspace 직접 Role은 Organization 투영 Role보다 엄격히 높을 때만 해당 Workspace에 추가한다. 같거나 낮은 부여는
workspace_direct_role_must_elevate로 거부하며 Workspace Role은 Organization으로 역상속하지 않는다. 효과 Role 계산은 방어적으로 더 높은 등급을 사용한다. Organization Role 승격으로 기존 직접 Role이 같거나 낮아지면 같은 잠금과 transaction에서 Assignment와 불필요해진 Workspaceusername을 만료하며 자동 복원하지 않는다. operator | auditor | contract_admin | accountant는 초기 카탈로그에 두지 않는다. 고위험 승인·반출·마감과 직무별 차이는 도메인 helper와 contextual policy로 제한한다.- 기본 access Role은 테넌트 업무 권한의 상한선이다. 초기 모델에는 사용자별 Functional Permission grant를 두지 않는다. subject-owned self-service와 trusted worker는 별도 인가 계약으로 판정한다.
organization-member와workspace-member는 로그인 응답의 접근 문맥 분기값이지 Role이 아니다. 권한은 Membership에 바인딩된 dotted Role key로만 판정한다.
로그인 API
| 문맥 | 식별자 | 입력 | 권리 확인 |
|---|---|---|---|
| Organization Owner | EMAIL | 이메일 + 패스워드 | Affiliation + Organization Membership + organization.owner |
| Organization 최고관리자/중간관리자/조회자 | ORGANIZATION_USERNAME | Organization Code + organizationUsername + 패스워드 | Affiliation + Organization Membership + 해당 Role |
| Workspace | WORKSPACE_USERNAME | Workspace Code + workspaceUsername + 패스워드 | Affiliation + Workspace Membership |
- Code는 각각
org-{10 lower Crockford Base32},ws-{10 lower Crockford Base32}다. - Organization Owner를 non-owner Role로 변경할 때 active Organization
username이 없으면organization_username_required_before_owner_demotion으로 transaction 전체를 거부한다. Owner 승격은 Organizationusername을 만료하고 verified EMAIL identifier를 요구한다. - 서버가
{username}@{namespace_code}를 조립하며 클라이언트 조립 문자열을 신뢰하지 않는다. - 요청은
organizationCode+organizationUsername또는workspaceCode+workspaceUsername으로 분리하며 범용code·signInId를 받지 않는다. - 인증 실패는 Code·Identifier·Principal 존재 여부를 응답 코드, 본문, 지연에서 구분하지 않는다.
iam-sign-in응답은organizationId,organizationSlug,organization,workspaceIds,workspaces,workspaceNumber만 사용한다.- Organization principal kind는
organization-member, Workspace principal kind는workspace-member다. - 이메일은 Role key가 아닌 인증 방법이지만, 제품 정책상 active
organization.owner에게만 Organization 이메일 로그인을 허용한다. 인증 성공만으로 Owner Role을 파생하지 않는다. - Leysys 본사 staff는 고객 Role 카탈로그와 분리된
ops-centersurface·issuer· 승인 흐름을 사용한다.platform/superadmin은 고객 Role key가 아니다.
SignUp·세션 metadata
create_account_kind: organization_ownerorganization_namesales_provisioned_organization_owner- 상태:
choose-organization,no-workspaces
클라이언트가 보낸 Organization ID나 Auth metadata만으로 기존 Organization 권한을 만들지 않는다. 가입·초대·프로비저닝 RPC가 서버에서 Membership과 역할을 생성하고 다시 검증한다.
권한과 보안
- 공개 IAM table은 모두 RLS를 활성화하고
anontable privilege를 부여하지 않는다. iam-sign-in은 pre-authentication endpoint라verify_jwt=false이지만, secret key로 필요한 table의 최소service_role권한만 사용한다.- 로그인 서버에는 Organization/Workspace와 Membership 조회, session 원장 쓰기, authorization audit append 권한만 부여한다.
- 프로비저닝 RPC는
PUBLIC,anon,authenticated실행 권한을 회수하고service_role만 실행한다. - terminal Membership은 되살리지 않고 새 generation을 만든다.
- 권한 변경은 관련 identity·membership·role·policy·product epoch를 같은 트랜잭션에서 증가시킨다.
- 브라우저 저장소의 principal은 표시 캐시다. access token 변경 시 서버에서 session과 Membership을 다시 읽는다.
- Viewer Role 자체에 mutation Permission을 넣지 않고, 모든 mutation endpoint는 효과 Role과 도메인별 Permission·hard deny를 함께 검증한다.
Workspace 0 불변식
- 새 Organization에는 같은 트랜잭션에서 정확히 하나의
workspace_number = 0Workspace를 만든다. (organization_id, workspace_number)는 unique이고 순번은 0 이상이며 변경·재사용하지 않는다.- Workspace 코드와 소속 Organization도 생성 후 변경할 수 없다.
- Workspace 0 직접 삭제는 거부하고 Organization 삭제에 따른 cascade만 허용한다.
- 새 Workspace는 Organization별 transaction lock 안에서
max(workspace_number) + 1을 배정한다.
검증 게이트
로컬에서 supabase db reset 후 SQL smoke를 실행한다. catalog gate는 public/private의 relation, column, constraint, index, function name·argument·body, view output, policy, trigger, comment에서 폐기된 scope 식별자가 남지 않았는지 확인한다. 또한 아래를 검사한다.
- Organization-only / Workspace-only RLS 분리
- cross-Organization Workspace FK 거부
- terminal Membership 재활성화 거부
workspace_module_entitlements접근 경계- canonical named argument RPC 호출
- 가입·Sales 프로비저닝 결과의 Organization/Workspace payload
- 두 번째 reset 뒤 schema diff 0