목적
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:
필수 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__.py에 OrganizationMembership과 관련 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 순서:
organization_memberships table 생성
- unique/check/FK/index 생성
- 기존
team_memberships에서 distinct (grantee_organization_id, user_id)를 active member로 backfill
organization.created_by를 active manager로 backfill
organization.managed_by가 있으면 active manager로 backfill
(organization_id, user_id) 충돌 시 manager row가 이기도록 merge/upsert
- team membership 기반 row는
organization_auth_state='member'
- creator/manager 기반 row는
organization_auth_state='manager'
- creator/manager 기반 row는
accepted_at=organization.created_at 또는 migration 실행 시각으로 설정
- 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 테스트를 우선 작성한다.
필수 시나리오:
- organization 생성
- team 생성
- 서로 다른 user 2명의 team membership 생성
- creator는 team membership이 없는 상태로 둔다.
- migration upgrade 실행
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 생성 테스트 중 기존 프로젝트 패턴에 맞는 방식을 따른다.
완료 조건
후속 이슈로 넘길 것
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 전제가 고정된 뒤 진행한다.
목적
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.mddocs/data-model/physical-data-model.mddocs/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하지 않는다.구현 범위
1. SQLAlchemy 모델 추가
후보 파일:
apps/shared/db/models/organization.pyapps/shared/db/models/organization_membership.py추가할 class:
OrganizationMembership필수 constant:
필수 column:
id: UUID primary key, defaultuuid.uuid4, not nullorganization_id: UUID, FKorganization.id, not null, indexeduser_id: UUID, FKusers.id, not null, indexedmembership_state: varchar(50), not null, defaultinvitedorganization_auth_state: varchar(50), not null, defaultmemberinvited_by: UUID, FKusers.id, nullable, indexedinvited_at: timezone-aware datetime, nullableaccepted_at: timezone-aware datetime, nullableremoved_at: timezone-aware datetime, nullablecreated_at: timezone-aware datetime, not null, default nowupdated_at: timezone-aware datetime, not null, default now, onupdate nowoptions: JSONB, not null, default{}flags: BigInteger, not null, default0필수 constraint:
id(organization_id, user_id)membership_state in ('invited', 'active', 'suspended', 'removed')organization_auth_state in ('member', 'manager')flags >= 0권장 index:
ix_organization_memberships_organization_idix_organization_memberships_user_idix_organization_memberships_membership_state(organization_id, membership_state)(user_id, membership_state)Relationship:
Organization.membershipsUser.organization_membershipsOrganizationMembership.organizationOrganizationMembership.userOrganizationMembership.inviter주의:
2. 모델 import 경로 정리
apps/shared/db/models/__init__.py에OrganizationMembership과 관련 constant export를 추가한다.TYPE_CHECKING순환 참조가 생기지 않도록User,Organizationrelationship import를 정리한다.3. Alembic migration 추가
권장 revision name:
파일 위치:
apps/shared/alembic/versions/<revision>_mvp2_0_add_organization_memberships.pyUpgrade 순서:
organization_membershipstable 생성team_memberships에서 distinct(grantee_organization_id, user_id)를 active member로 backfillorganization.created_by를 active manager로 backfillorganization.managed_by가 있으면 active manager로 backfill(organization_id, user_id)충돌 시 manager row가 이기도록 merge/upsertorganization_auth_state='member'organization_auth_state='manager'accepted_at=organization.created_at또는 migration 실행 시각으로 설정Downgrade:
organization_membershipsindex/constraint/table을 drop한다.team_memberships가 유지되므로 MVP1 수준 rollback이 가능해야 한다.4. Backfill 세부 규칙
Team membership 기반:
생성/merge 결과:
organization_id = team_memberships.grantee_organization_iduser_id = team_memberships.user_idmembership_state = 'active'organization_auth_state = 'member'invited_by = 가장 이른 assigned_by, 없으면organization.created_byinvited_at = 가장 이른 assigned_ataccepted_at = 가장 이른 assigned_atOrganization creator 기반:
organization_id = organization.iduser_id = organization.created_bymembership_state = 'active'organization_auth_state = 'manager'invited_by = organization.created_byinvited_at = organization.created_ataccepted_at = organization.created_atOrganization manager 기반:
organization_id = organization.iduser_id = organization.managed_bymembership_state = 'active'organization_auth_state = 'manager'invited_by = organization.created_byinvited_at = organization.created_ataccepted_at = organization.created_atmanaged_by is null이면 row를 만들지 않는다.충돌 처리:
organization_memberships기존 row가 없다는 전제지만, idempotent하게 작성 가능하면 더 좋다.5. Migration 검증 query
Upgrade 마지막에 최소한 아래 조건을 검증한다.
모든 team membership은 active organization membership을 가져야 한다.
모든 organization creator는 active manager membership을 가져야 한다.
추가 검증:
위 query들이 row를 반환하면 MVP1 regression 위험이 있으므로 migration을 실패 처리한다.
6. 테스트 범위
DB/model/migration 테스트를 우선 작성한다.
필수 시나리오:
organization_memberships확인기대 결과:
organization.managed_by가 있으면 active manager다.(organization_id, user_id)중복 row가 없다.가능하면 Alembic migration 단위 테스트 또는 SQLAlchemy metadata/table 생성 테스트 중 기존 프로젝트 패턴에 맞는 방식을 따른다.
완료 조건
OrganizationMembershipORM 모델이 추가된다.apps/shared/db/models/__init__.pyexport/import가 정리된다.mvp2_0_add_organization_memberships가 추가된다.organization_membershipstable, constraints, indexes를 만든다.(organization_id, user_id)중복이 생기지 않는다.organization_membershipstable drop까지 수행한다.organization,teams,team_memberships,team_*_permissions,user_*_permissionsschema는 제거/rename하지 않는다.후속 이슈로 넘길 것
has_active_organization_membership,get_organization_auth_statepermission helper 추가get_user_primary_organization_id()organization membership 기준 전환ensure_user_default_organization()runtime reconciliation guard참고
이 이슈 완료 후
Issue 0-2 permission helper 전환과Issue 0-4 team/direct permission 검증 기준 변경을 시작할 수 있다. MVP 2 본작업의user_knowledge_permissions, knowledge baseuseenforcement, RAG node execution check는 organization membership 전제가 고정된 뒤 진행한다.