Skip to content

[DB][RBAC] MVP2-0 Issue 0-1: organization_memberships 모델과 migration 추가 #72

Description

@yoonki1207

목적

MVP 2-0 Organization Membership / Invitation Foundation의 첫 구현 단위다. 기존 team_memberships를 조직 소속의 source of truth처럼 사용하던 구조를 바로 제거하지 않고, additive하게 organization_memberships 테이블과 ORM 모델을 추가한다. 이후 permission helper, member API, team/direct permission 검증 기준 변경이 이 테이블을 전제로 진행될 수 있어야 한다.

이 이슈의 범위는 DB/model/migration/backfill/import 정리까지다. Permission helper 전환, Gateway service 변경, organization member API, FE UI는 후속 이슈에서 처리한다.

문서 기준 및 정합성 확인

Source of truth:

  • docs/implementation-plan/mvp-2-0-organization-membership-plan.md
  • docs/data-model/physical-data-model.md
  • docs/data-model/rbac-permission-policy.md

정합성 확인:

  • 계획서 Issue 0-1의 작업 범위는 SQLAlchemy model, Alembic migration, backfill SQL, downgrade, model import 경로 정리다.
  • 계획서 7.1 신규 table과 물리 데이터 모델의 organization_memberships 정의가 일치한다.
  • users.organization_id, role catalog, 통합 resource_permissions 같은 새 구조는 만들지 않는다.
  • 기존 organization, teams, team_memberships, team_*_permissions, user_workflow_permissions, user_llm_permissions는 제거하거나 rename하지 않는다.
  • 이 이슈는 active organization 전달 방식, invitation API, permission helper fail-closed 로직을 구현하지 않는다. 단, 후속 작업이 사용할 수 있도록 table/model/relationship/constant를 준비한다.

구현 범위

1. SQLAlchemy 모델 추가

후보 파일:

  • 우선 권장: apps/shared/db/models/organization.py
  • 모델이 커지면 대안: apps/shared/db/models/organization_membership.py

추가할 class:

  • OrganizationMembership

필수 constant:

ORGANIZATION_MEMBERSHIP_INVITED = "invited"
ORGANIZATION_MEMBERSHIP_ACTIVE = "active"
ORGANIZATION_MEMBERSHIP_SUSPENDED = "suspended"
ORGANIZATION_MEMBERSHIP_REMOVED = "removed"

ORGANIZATION_AUTH_MEMBER = "member"
ORGANIZATION_AUTH_MANAGER = "manager"

필수 column:

  • id: UUID primary key, default uuid.uuid4, not null
  • organization_id: UUID, FK organization.id, not null, indexed
  • user_id: UUID, FK users.id, not null, indexed
  • membership_state: varchar(50), not null, default invited
  • organization_auth_state: varchar(50), not null, default member
  • invited_by: UUID, FK users.id, nullable, indexed
  • invited_at: timezone-aware datetime, nullable
  • accepted_at: timezone-aware datetime, nullable
  • removed_at: timezone-aware datetime, nullable
  • created_at: timezone-aware datetime, not null, default now
  • updated_at: timezone-aware datetime, not null, default now, onupdate now
  • options: JSONB, not null, default {}
  • flags: BigInteger, not null, default 0

필수 constraint:

  • primary key: id
  • unique: (organization_id, user_id)
  • check: membership_state in ('invited', 'active', 'suspended', 'removed')
  • check: organization_auth_state in ('member', 'manager')
  • check: flags >= 0

권장 index:

  • ix_organization_memberships_organization_id
  • ix_organization_memberships_user_id
  • ix_organization_memberships_membership_state
  • composite index 후보: (organization_id, membership_state)
  • composite index 후보: (user_id, membership_state)

Relationship:

  • Organization.memberships
  • User.organization_memberships
  • OrganizationMembership.organization
  • OrganizationMembership.user
  • OrganizationMembership.inviter

주의:

  • relationship은 API response/test 편의용이다.
  • permission helper는 후속 이슈에서 명시 query를 우선 사용한다.

2. 모델 import 경로 정리

  • apps/shared/db/models/__init__.pyOrganizationMembership과 관련 constant export를 추가한다.
  • Alembic metadata가 새 모델을 발견할 수 있도록 import 순서를 확인한다.
  • TYPE_CHECKING 순환 참조가 생기지 않도록 User, Organization relationship import를 정리한다.

3. Alembic migration 추가

권장 revision name:

mvp2_0_add_organization_memberships

파일 위치:

  • apps/shared/alembic/versions/<revision>_mvp2_0_add_organization_memberships.py

Upgrade 순서:

  1. organization_memberships table 생성
  2. unique/check/FK/index 생성
  3. 기존 team_memberships에서 distinct (grantee_organization_id, user_id)를 active member로 backfill
  4. organization.created_by를 active manager로 backfill
  5. organization.managed_by가 있으면 active manager로 backfill
  6. (organization_id, user_id) 충돌 시 manager row가 이기도록 merge/upsert
  7. team membership 기반 row는 organization_auth_state='member'
  8. creator/manager 기반 row는 organization_auth_state='manager'
  9. creator/manager 기반 row는 accepted_at=organization.created_at 또는 migration 실행 시각으로 설정
  10. backfill 검증 query를 실행하고 누락이 있으면 migration을 실패시킨다.

Downgrade:

  • organization_memberships index/constraint/table을 drop한다.
  • downgrade 시 invitation/membership 이력 유실은 허용한다.
  • 기존 team_memberships가 유지되므로 MVP1 수준 rollback이 가능해야 한다.

4. Backfill 세부 규칙

Team membership 기반:

SELECT DISTINCT grantee_organization_id, user_id
FROM team_memberships

생성/merge 결과:

  • organization_id = team_memberships.grantee_organization_id
  • user_id = team_memberships.user_id
  • membership_state = 'active'
  • organization_auth_state = 'member'
  • invited_by = 가장 이른 assigned_by, 없으면 organization.created_by
  • invited_at = 가장 이른 assigned_at
  • accepted_at = 가장 이른 assigned_at

Organization creator 기반:

  • organization_id = organization.id
  • user_id = organization.created_by
  • membership_state = 'active'
  • organization_auth_state = 'manager'
  • invited_by = organization.created_by
  • invited_at = organization.created_at
  • accepted_at = organization.created_at

Organization manager 기반:

  • organization_id = organization.id
  • user_id = organization.managed_by
  • membership_state = 'active'
  • organization_auth_state = 'manager'
  • invited_by = organization.created_by
  • invited_at = organization.created_at
  • accepted_at = organization.created_at
  • managed_by is null이면 row를 만들지 않는다.

충돌 처리:

  • 기존 member active + creator/manager backfill이면 manager로 승격한다.
  • 기존 manager active + team member backfill이면 manager를 유지한다.
  • migration 시점에는 organization_memberships 기존 row가 없다는 전제지만, idempotent하게 작성 가능하면 더 좋다.

5. Migration 검증 query

Upgrade 마지막에 최소한 아래 조건을 검증한다.

모든 team membership은 active organization membership을 가져야 한다.

SELECT tm.id
FROM team_memberships tm
LEFT JOIN organization_memberships om
  ON om.organization_id = tm.grantee_organization_id
 AND om.user_id = tm.user_id
 AND om.membership_state = 'active'
WHERE om.id IS NULL;

모든 organization creator는 active manager membership을 가져야 한다.

SELECT o.id
FROM organization o
LEFT JOIN organization_memberships om
  ON om.organization_id = o.id
 AND om.user_id = o.created_by
 AND om.membership_state = 'active'
 AND om.organization_auth_state = 'manager'
WHERE om.id IS NULL;

추가 검증:

SELECT organization_id, user_id, count(*)
FROM organization_memberships
GROUP BY organization_id, user_id
HAVING count(*) > 1;

위 query들이 row를 반환하면 MVP1 regression 위험이 있으므로 migration을 실패 처리한다.

6. 테스트 범위

DB/model/migration 테스트를 우선 작성한다.

필수 시나리오:

  1. organization 생성
  2. team 생성
  3. 서로 다른 user 2명의 team membership 생성
  4. creator는 team membership이 없는 상태로 둔다.
  5. migration upgrade 실행
  6. organization_memberships 확인

기대 결과:

  • team member 2명은 active member다.
  • organization creator는 active manager다.
  • organization.managed_by가 있으면 active manager다.
  • (organization_id, user_id) 중복 row가 없다.
  • creator가 이미 team member였으면 row는 1개이고 manager로 승격된다.
  • downgrade는 table drop까지 수행한다.

가능하면 Alembic migration 단위 테스트 또는 SQLAlchemy metadata/table 생성 테스트 중 기존 프로젝트 패턴에 맞는 방식을 따른다.

완료 조건

  • OrganizationMembership ORM 모델이 추가된다.
  • membership/auth state constant가 추가된다.
  • apps/shared/db/models/__init__.py export/import가 정리된다.
  • Alembic migration mvp2_0_add_organization_memberships가 추가된다.
  • migration upgrade가 organization_memberships table, constraints, indexes를 만든다.
  • migration upgrade가 기존 team members를 active organization members로 backfill한다.
  • migration upgrade가 organization creator/manager를 active manager membership으로 backfill한다.
  • manager/member 충돌 시 manager가 유지된다.
  • (organization_id, user_id) 중복이 생기지 않는다.
  • migration 검증 query가 누락 backfill을 감지한다.
  • downgrade가 organization_memberships table drop까지 수행한다.
  • 기존 organization, teams, team_memberships, team_*_permissions, user_*_permissions schema는 제거/rename하지 않는다.
  • 관련 migration/model 테스트가 통과한다.

후속 이슈로 넘길 것

  • has_active_organization_membership, get_organization_auth_state permission helper 추가
  • workflow/LLM permission helper fail-closed 전환
  • get_user_primary_organization_id() organization membership 기준 전환
  • ensure_user_default_organization() runtime reconciliation guard
  • organization member/invitation API
  • team add와 user direct permission grant 검증 기준 변경
  • member removal cleanup
  • FE Members UI와 picker filtering

참고

이 이슈 완료 후 Issue 0-2 permission helper 전환Issue 0-4 team/direct permission 검증 기준 변경을 시작할 수 있다. MVP 2 본작업의 user_knowledge_permissions, knowledge base use enforcement, RAG node execution check는 organization membership 전제가 고정된 뒤 진행한다.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions