목적
MVP 2-0 Organization Membership / Invitation Foundation의 API 계층을 추가한다. organization_memberships를 기준으로 current user의 organization 목록, 현재 organization, organization member 목록, 기존 user 초대, 초대 수락, member 상태/auth 변경, member 제거 API를 제공한다.
이 이슈는 organization_memberships 모델/migration과 permission helper 전환 이후 진행한다. 기존 MVP1 team/resource permission API request/response shape는 깨지지 않는다.
선행 조건
Blocked by:
MBA-52: [BE][RBAC] MVP2-0 Issue 0-2: organization membership 기반 permission helper 전환
Transitive dependency:
MBA-51: [DB][RBAC] MVP2-0 Issue 0-1: organization_memberships 모델과 migration 추가
전제:
OrganizationMembership 모델과 membership/auth constant가 존재한다.
has_organization_manager_permission()은 organization_memberships.organization_auth_state='manager'와 legacy organization.created_by/managed_by fallback을 함께 반영한다.
- workflow/LLM permission helper는 active organization membership이 없으면 fail-closed 한다.
문서 기준 및 정합성 확인
Source of truth:
docs/implementation-plan/mvp-2-0-organization-membership-plan.md
docs/api/organization-rbac.md
docs/data-model/physical-data-model.md
docs/data-model/rbac-permission-policy.md
정합성 확인:
- 계획서
Issue 0-3의 범위는 organization member service, list/invite/accept/update/remove endpoint, schema, audit, self-removal/last-manager guard다.
- 계획서
13.1~13.8 API 계약의 endpoint와 state transition을 따른다.
- 계획서
14. Schema 작업의 schema 후보를 추가한다.
- 계획서
15. Audit 설계의 organization membership audit action과 cleanup metadata를 기록한다.
- 이메일만 존재하는 미가입 사용자 초대, invitation token, email 발송, signup completion flow는 MVP 2-0 범위 밖이다.
- manager-side 강제 accept는 MVP 2-0에서 제외한다. 초대 수락은 invited user 본인만 가능하다.
- 기존 Team/permission API shape는 유지한다. Team add와 user direct permission grant의 grantee 검증 기준 변경은 Issue 0-4 범위다.
현재 코드 기준
현재 확인된 코드 상태:
apps/gateway/api/api.py에는 /organizations router가 아직 등록되어 있지 않다.
apps/gateway/api/v1/endpoints/teams.py는 /teams, /permissions 중심이다.
apps/gateway/services/team_service.py에는 organization member service가 없다.
apps/shared/schemas/team.py는 team/permission schema만 가진다.
apps/shared/audit/actions.py에는 organization.invite, organization.member.accept, organization.member.update, organization.member.remove action이 없다.
User 모델에는 email, name, deactivated_at이 있어 member response와 invite validation에 사용할 수 있다.
구현 범위
1. Schema 추가
신규 파일 후보:
apps/shared/schemas/organization_membership.py
필요 schema:
OrganizationMemberInviteRequest
OrganizationMemberUpdateRequest
OrganizationMemberResponse
OrganizationSummaryResponse
OrganizationCurrentResponse
OrganizationMemberRemoveResponse
RevokedUserPermissionCounts
OrganizationMemberInviteRequest
필드:
user_id: UUID
organization_auth_state: Literal["member", "manager"] = "member"
Validation:
organization_auth_state는 member, manager만 허용한다.
- 기본값은
member다.
OrganizationMemberUpdateRequest
필드:
membership_state: Literal["active", "suspended"] | None = None
organization_auth_state: Literal["member", "manager"] | None = None
Validation:
removed는 PATCH로 받지 않는다. 제거는 DELETE endpoint만 사용한다.
- invited member를
active로 바꾸는 manager-side 강제 accept는 허용하지 않는다.
- 둘 다
None이면 400 처리한다.
OrganizationMemberResponse
필드:
id: UUID
organization_id: UUID
user_id: UUID
user_email: str
user_name: str
membership_state: str
organization_auth_state: str
invited_by: UUID | None
invited_at: datetime | None
accepted_at: datetime | None
removed_at: datetime | None = None
created_at: datetime
updated_at: datetime
주의:
- email/name은 API response에 포함한다.
- audit metadata에는 email을 저장하지 않는다.
OrganizationSummaryResponse
필드:
id: UUID
name: str
membership_state: str
organization_auth_state: str
is_active: bool
OrganizationCurrentResponse
필드는 OrganizationSummaryResponse와 동일하게 시작해도 된다.
OrganizationMemberRemoveResponse
필드:
status: Literal["removed"]
removed_team_memberships: int
revoked_user_permissions: RevokedUserPermissionCounts
RevokedUserPermissionCounts 필드:
workflow: int = 0
llm_credential: int = 0
knowledge_base: int = 0
audit: int = 0
MVP 2-0에서는 실제 cleanup 대상이 user_workflow_permissions, user_llm_permissions, team_memberships다. knowledge_base, audit은 아직 테이블이 없으면 0으로 반환한다.
2. Organization member service 추가
신규 파일 후보:
apps/gateway/services/organization_member_service.py
주요 public method:
list_organizations(db, current_user) -> list[OrganizationSummaryResponse]
get_current_organization(db, current_user) -> OrganizationCurrentResponse
list_members(db, current_user, organization_id, state=None) -> list[OrganizationMemberResponse]
invite_member(db, current_user, organization_id, request) -> OrganizationMemberResponse
accept_invitation(db, current_user, organization_id) -> OrganizationMemberResponse
update_member(db, current_user, organization_id, user_id, request) -> OrganizationMemberResponse
remove_member(db, current_user, organization_id, user_id) -> OrganizationMemberRemoveResponse
공통 helper:
_ensure_organization_manager(db, current_user, organization_id)
_get_active_organization(db, organization_id)
_get_user_or_404(db, user_id)
_get_membership(db, organization_id, user_id)
_count_active_managers(db, organization_id, exclude_user_id=None)
_ensure_not_last_manager(db, organization_id, target_user_id)
_serialize_member(membership, user)
_record_organization_membership_audit(...)
권한:
- organization list/current: authenticated user
- member list/invite/update/remove: organization manager
- accept invitation: invited user 본인
3. API endpoint 추가
신규 파일 후보:
apps/gateway/api/v1/endpoints/organizations.py
apps/gateway/api/api.py에 router 등록:
api_router.include_router(organizations.router, prefix="/organizations", tags=["organizations"])
필수 endpoint:
GET /api/v1/organizations
Permission:
동작:
- current user의
organization_memberships 기준으로 조회한다.
membership_state in ('active', 'invited')만 반환한다.
- organization이 inactive이면 제외한다.
- invited organization은 response에 포함하되 resource 화면 진입 가능 여부는 FE/permission helper가 별도로 판단한다.
Response:
[
{
"id": "uuid",
"name": "Acme",
"membership_state": "active",
"organization_auth_state": "manager",
"is_active": true
}
]
GET /api/v1/organizations/current
Permission:
동작:
- 우선 active organization membership 기준으로 current/primary organization을 반환한다.
- 정렬 기준은 가능하면
accepted_at asc nulls last, created_at asc를 따른다.
- active membership이 없으면 404 또는 기존
ensure_user_default_organization() 흐름과의 연결을 검토한다. 이 endpoint 자체에서 default organization 생성을 강제할지는 Issue 0-2/0-3 구현 시 기존 UX 회귀를 보고 결정하되, 문서상 organization membership 기준을 우선한다.
Response:
{
"id": "uuid",
"name": "Acme",
"membership_state": "active",
"organization_auth_state": "member",
"is_active": true
}
GET /api/v1/organizations/{organization_id}/members
Permission:
Query:
state=active|invited|suspended|removed optional
동작:
- manager만 조회 가능하다.
- state가 없으면 기본적으로
active, invited, suspended를 반환한다. removed 포함 여부는 query로 명시했을 때만 반환하는 것을 권장한다.
- user email/name을 join해서 response에 포함한다.
POST /api/v1/organizations/{organization_id}/members/invitations
Permission:
Request:
{
"user_id": "uuid",
"organization_auth_state": "member"
}
Rules:
- 초대 대상 user가 존재해야 한다. 없으면 404.
- deactivated user는 초대할 수 없다. 400 또는 409.
- 자기 자신 초대는 MVP2-0 권장값에 따라 400으로 막는다.
- 이미 active member이면 idempotent하게 기존 membership을 반환한다.
- 이미 invited member이면 idempotent하게 기존 invitation을 반환하되
invited_at은 갱신하지 않는다.
- removed row가 있으면 같은 row를
invited로 재활성화한다. 이때 invited_by=current_user.id, invited_at=now, accepted_at=None, removed_at=None로 갱신한다.
- suspended member이면 409를 반환한다. 재활성화는 PATCH만 사용한다.
organization_auth_state='manager' 초대는 organization manager가 요청하는 경우 허용한다.
- invitation 생성/재초대 성공 시 audit action
organization.invite를 기록한다.
Response:
{
"id": "membership_uuid",
"membership_state": "invited",
"organization_auth_state": "member"
}
POST /api/v1/organizations/{organization_id}/members/me/accept
Permission:
Rules:
- current user의 membership row를 조회한다.
- membership_state가
invited이면 active로 변경하고 accepted_at=now를 기록한다.
- 이미 active이면 idempotent하게 active membership을 반환한다.
- suspended/removed 상태는 accept할 수 없고 409를 반환한다.
- organization manager가 다른 user를 강제 activate하는 것은 MVP2-0에서 허용하지 않는다.
- accept 성공 시 audit action
organization.member.accept를 actor=current user로 기록한다.
Response:
{
"id": "membership_uuid",
"membership_state": "active"
}
선택 endpoint:
POST /api/v1/organizations/{organization_id}/members/{user_id}/accept는 구현하지 않아도 된다.
- 구현한다면
current_user.id == user_id를 강제한다.
PATCH /api/v1/organizations/{organization_id}/members/{user_id}
Permission:
Request:
{
"membership_state": "suspended",
"organization_auth_state": "manager"
}
Rules:
membership_state는 active, suspended만 직접 변경 가능하다.
removed는 DELETE endpoint만 사용한다.
- invited member를
active로 바꾸는 manager-side 강제 accept는 허용하지 않는다.
- active/suspended 전환만 처리한다.
- suspended 전환 시 team/direct permission row는 삭제하지 않는다. Issue 0-2 helper가 fail-closed 처리한다.
- suspended member를 active로 되돌리면 기존 team/direct permission row가 다시 효력을 가진다.
- 마지막 active manager를 member로 낮추거나 suspend하는 것은 금지한다.
- 자기 자신의 manager 권한을 낮추는 것은 MVP2-0에서 금지한다.
- update 성공 시 audit action
organization.member.update를 before/after metadata와 함께 기록한다.
DELETE /api/v1/organizations/{organization_id}/members/{user_id}
Permission:
Rules:
- 마지막 manager 제거 금지.
- 자기 자신 제거 금지.
- membership_state를
removed로 변경한다.
removed_at=now를 기록한다.
- invited 상태의 user도 remove할 수 있다.
- removed 상태에 대해 다시 DELETE가 오면 idempotent하게
status='removed'를 반환한다.
- 제거 transaction 안에서 해당 organization의 cleanup을 수행한다.
- cleanup 대상:
team_memberships where grantee_organization_id=organization_id and user_id=target_user_id
user_workflow_permissions where grantee_organization_id=organization_id and user_id=target_user_id
user_llm_permissions where grantee_organization_id=organization_id and user_id=target_user_id
- MVP2
user_knowledge_permissions가 아직 없으면 count 0
- MVP3
user_audit_permissions가 아직 없으면 count 0
- cleanup count를 response와 audit metadata에 남긴다.
- 중간 실패 시 제거와 cleanup이 일부만 반영되면 안 된다.
Response:
{
"status": "removed",
"removed_team_memberships": 3,
"revoked_user_permissions": {
"workflow": 2,
"llm_credential": 1,
"knowledge_base": 0,
"audit": 0
}
}
정합성 메모:
- 계획서 Issue 0-4에도
member removal cleanup이 언급되어 있지만, API 계약 13.7 Remove member가 cleanup count response를 요구한다. 따라서 이 이슈에서 remove endpoint의 최소 cleanup transaction을 구현한다.
- Issue 0-4는 team add와 user direct permission grant 생성 시점의 grantee 검증 기준 변경을 담당한다.
4. State transition 구현
허용 transition:
| 현재 상태 |
Action |
다음 상태 |
주체 |
| 없음 |
invite |
invited |
organization manager |
| invited |
accept |
active |
invited user 본인 |
| invited |
remove |
removed |
organization manager |
| active |
suspend |
suspended |
organization manager |
| active |
remove |
removed |
organization manager |
| suspended |
reactivate |
active |
organization manager |
| suspended |
remove |
removed |
organization manager |
| removed |
invite |
invited |
organization manager |
불허:
- manager가 invited member를 PATCH로 active 처리
- suspended/removed member의 accept
- self remove
- self demote from manager
- last manager demote/suspend/remove
- email-only invite
5. Last manager guard
Guard 대상:
- DELETE member
- PATCH membership_state
suspended
- PATCH organization_auth_state
member
- 두 변경이 동시에 들어와 manager 권한이 사라지는 경우
판정 기준:
- active
organization_memberships 중 organization_auth_state='manager' row count를 기준으로 한다.
- target user를 제외했을 때 active manager가 0명이 되면 400 또는 409를 반환한다.
- migration 누락을 대비한 legacy fallback은 permission helper에는 남기되, target state guard는 active manager membership row를 기준으로 삼는다. 단, 구현 중 기존 데이터 회귀가 우려되면
created_by/managed_by fallback을 보조적으로 고려한다.
6. Audit action 추가
파일:
apps/shared/audit/actions.py
추가 action:
ORGANIZATION_INVITE = "organization.invite"
ORGANIZATION_MEMBER_ACCEPT = "organization.member.accept"
ORGANIZATION_MEMBER_UPDATE = "organization.member.update"
ORGANIZATION_MEMBER_REMOVE = "organization.member.remove"
기존 action 재사용:
PERMISSION_REVOKE = "permission.revoke"는 cleanup aggregate 기록에 재사용 가능하다.
Audit metadata 원칙:
- email은 저장하지 않는다.
- user id, organization id, membership id만 저장한다.
- update/remove는 before/after state를 저장한다.
- remove는 cleanup count를 저장한다.
예시:
{
"organization_id": "uuid",
"membership_id": "uuid",
"target_user_id": "uuid",
"previous_membership_state": "active",
"next_membership_state": "removed",
"previous_organization_auth_state": "member",
"next_organization_auth_state": "member",
"reason": "organization.member.remove",
"cleanup": {
"team_memberships": 2,
"user_workflow_permissions": 1,
"user_llm_permissions": 1,
"user_knowledge_permissions": 0,
"user_audit_permissions": 0
}
}
Audit timing:
- invite/update/remove 성공 후 같은 transaction 안에서 audit row를 생성한다.
- accept는 target user 본인의 action으로 기록한다.
- cleanup으로 삭제되는 개별 permission row마다 audit row를 남기지 않아도 된다. MVP2-0 권장은 aggregate row 하나다.
7. 테스트 추가
파일 후보:
apps/gateway/tests/services/test_organization_member_service.py
apps/gateway/tests/api/test_organization_members_api.py
apps/shared/tests/test_audit_actions.py
Service test:
- manager가 기존 active user를 invited member로 초대할 수 있다.
- 초대 대상 user가 없으면 404.
- deactivated user 초대는 400/409.
- 자기 자신 초대는 400.
- active member 재초대는 idempotent하게 기존 row 반환.
- invited member 재초대는
invited_at 갱신 없이 기존 row 반환.
- removed member 재초대는 same row를 invited로 되돌리고
removed_at, accepted_at을 초기화한다.
- suspended member invite는 409.
- invited user 본인은 accept할 수 있고
accepted_at이 설정된다.
- active user accept는 idempotent하다.
- suspended/removed user accept는 409.
- manager는 active member를 suspended로 바꿀 수 있다.
- manager는 suspended member를 active로 바꿀 수 있다.
- manager는 member를 manager로 승격할 수 있다.
- 마지막 manager demote/suspend/remove는 차단된다.
- self remove와 self demote는 차단된다.
- remove member는 membership을 removed로 바꾸고 team/direct permission rows를 cleanup한다.
- remove member response에 cleanup count가 정확히 담긴다.
- remove member는 audit metadata에 cleanup count를 남긴다.
API test:
GET /api/v1/organizations는 current user의 active/invited organizations만 반환한다.
GET /api/v1/organizations/current는 active membership 기준 current organization을 반환한다.
- manager만
GET /organizations/{id}/members를 호출할 수 있다.
- manager만 invite/update/remove를 호출할 수 있다.
- invited user는
POST /members/me/accept로 직접 accept할 수 있다.
- 다른 user의 accept는 허용되지 않는다.
- invited user는 accept 전 resource 접근이 불가능하다. 이 검증은 Issue 0-2 permission helper와 통합해서 확인한다.
- remove 후 user는 workflow/LLM permission row가 남지 않거나 cleanup되어 접근할 수 없다.
Audit action test:
AuditAction.ORGANIZATION_INVITE == "organization.invite"
AuditAction.ORGANIZATION_MEMBER_ACCEPT == "organization.member.accept"
AuditAction.ORGANIZATION_MEMBER_UPDATE == "organization.member.update"
AuditAction.ORGANIZATION_MEMBER_REMOVE == "organization.member.remove"
완료 조건
명시적 제외 범위
이 이슈에서 하지 않는다.
organization_memberships model/migration 추가: MBA-51
- permission helper fail-closed 전환:
MBA-52
- team add 시 active organization membership 검증: Issue 0-4
- user direct permission grant 시 active organization membership 검증: Issue 0-4
_ensure_grantee_user_membership 제거/의미 변경: Issue 0-4
- FE Members tab, invite modal, picker filtering: Issue 0-5
- email-only invite, invitation token, email 발송, signup completion flow
- SSO/OIDC 자동 organization join
- active organization switcher/header/session 계약 확정
- MVP2
user_knowledge_permissions migration 및 knowledge base use enforcement
후속 연결
이 이슈가 끝나면 Issue 0-4에서 기존 team/direct permission mutation 경로를 active organization membership 기준으로 전환한다. 그 뒤 Issue 0-5에서 Members UI와 picker filtering을 붙인다.
목적
MVP 2-0 Organization Membership / Invitation Foundation의 API 계층을 추가한다.
organization_memberships를 기준으로 current user의 organization 목록, 현재 organization, organization member 목록, 기존 user 초대, 초대 수락, member 상태/auth 변경, member 제거 API를 제공한다.이 이슈는
organization_memberships모델/migration과 permission helper 전환 이후 진행한다. 기존 MVP1 team/resource permission API request/response shape는 깨지지 않는다.선행 조건
Blocked by:
MBA-52:[BE][RBAC] MVP2-0 Issue 0-2: organization membership 기반 permission helper 전환Transitive dependency:
MBA-51:[DB][RBAC] MVP2-0 Issue 0-1: organization_memberships 모델과 migration 추가전제:
OrganizationMembership모델과 membership/auth constant가 존재한다.has_organization_manager_permission()은organization_memberships.organization_auth_state='manager'와 legacyorganization.created_by/managed_byfallback을 함께 반영한다.문서 기준 및 정합성 확인
Source of truth:
docs/implementation-plan/mvp-2-0-organization-membership-plan.mddocs/api/organization-rbac.mddocs/data-model/physical-data-model.mddocs/data-model/rbac-permission-policy.md정합성 확인:
Issue 0-3의 범위는 organization member service, list/invite/accept/update/remove endpoint, schema, audit, self-removal/last-manager guard다.13.1~13.8 API 계약의 endpoint와 state transition을 따른다.14. Schema 작업의 schema 후보를 추가한다.15. Audit 설계의 organization membership audit action과 cleanup metadata를 기록한다.현재 코드 기준
현재 확인된 코드 상태:
apps/gateway/api/api.py에는/organizationsrouter가 아직 등록되어 있지 않다.apps/gateway/api/v1/endpoints/teams.py는/teams,/permissions중심이다.apps/gateway/services/team_service.py에는 organization member service가 없다.apps/shared/schemas/team.py는 team/permission schema만 가진다.apps/shared/audit/actions.py에는organization.invite,organization.member.accept,organization.member.update,organization.member.removeaction이 없다.User모델에는email,name,deactivated_at이 있어 member response와 invite validation에 사용할 수 있다.구현 범위
1. Schema 추가
신규 파일 후보:
apps/shared/schemas/organization_membership.py필요 schema:
OrganizationMemberInviteRequest필드:
user_id: UUIDorganization_auth_state: Literal["member", "manager"] = "member"Validation:
organization_auth_state는member,manager만 허용한다.member다.OrganizationMemberUpdateRequest필드:
membership_state: Literal["active", "suspended"] | None = Noneorganization_auth_state: Literal["member", "manager"] | None = NoneValidation:
removed는 PATCH로 받지 않는다. 제거는 DELETE endpoint만 사용한다.active로 바꾸는 manager-side 강제 accept는 허용하지 않는다.None이면 400 처리한다.OrganizationMemberResponse필드:
id: UUIDorganization_id: UUIDuser_id: UUIDuser_email: struser_name: strmembership_state: strorganization_auth_state: strinvited_by: UUID | Noneinvited_at: datetime | Noneaccepted_at: datetime | Noneremoved_at: datetime | None = Nonecreated_at: datetimeupdated_at: datetime주의:
OrganizationSummaryResponse필드:
id: UUIDname: strmembership_state: strorganization_auth_state: stris_active: boolOrganizationCurrentResponse필드는
OrganizationSummaryResponse와 동일하게 시작해도 된다.OrganizationMemberRemoveResponse필드:
status: Literal["removed"]removed_team_memberships: intrevoked_user_permissions: RevokedUserPermissionCountsRevokedUserPermissionCounts필드:workflow: int = 0llm_credential: int = 0knowledge_base: int = 0audit: int = 0MVP 2-0에서는 실제 cleanup 대상이
user_workflow_permissions,user_llm_permissions,team_memberships다.knowledge_base,audit은 아직 테이블이 없으면 0으로 반환한다.2. Organization member service 추가
신규 파일 후보:
apps/gateway/services/organization_member_service.py주요 public method:
공통 helper:
권한:
3. API endpoint 추가
신규 파일 후보:
apps/gateway/api/v1/endpoints/organizations.pyapps/gateway/api/api.py에 router 등록:필수 endpoint:
GET /api/v1/organizationsPermission:
동작:
organization_memberships기준으로 조회한다.membership_state in ('active', 'invited')만 반환한다.Response:
[ { "id": "uuid", "name": "Acme", "membership_state": "active", "organization_auth_state": "manager", "is_active": true } ]GET /api/v1/organizations/currentPermission:
동작:
accepted_at asc nulls last,created_at asc를 따른다.ensure_user_default_organization()흐름과의 연결을 검토한다. 이 endpoint 자체에서 default organization 생성을 강제할지는 Issue 0-2/0-3 구현 시 기존 UX 회귀를 보고 결정하되, 문서상 organization membership 기준을 우선한다.Response:
{ "id": "uuid", "name": "Acme", "membership_state": "active", "organization_auth_state": "member", "is_active": true }GET /api/v1/organizations/{organization_id}/membersPermission:
Query:
state=active|invited|suspended|removedoptional동작:
active,invited,suspended를 반환한다. removed 포함 여부는 query로 명시했을 때만 반환하는 것을 권장한다.POST /api/v1/organizations/{organization_id}/members/invitationsPermission:
Request:
{ "user_id": "uuid", "organization_auth_state": "member" }Rules:
invited_at은 갱신하지 않는다.invited로 재활성화한다. 이때invited_by=current_user.id,invited_at=now,accepted_at=None,removed_at=None로 갱신한다.organization_auth_state='manager'초대는 organization manager가 요청하는 경우 허용한다.organization.invite를 기록한다.Response:
{ "id": "membership_uuid", "membership_state": "invited", "organization_auth_state": "member" }POST /api/v1/organizations/{organization_id}/members/me/acceptPermission:
Rules:
invited이면active로 변경하고accepted_at=now를 기록한다.organization.member.accept를 actor=current user로 기록한다.Response:
{ "id": "membership_uuid", "membership_state": "active" }선택 endpoint:
POST /api/v1/organizations/{organization_id}/members/{user_id}/accept는 구현하지 않아도 된다.current_user.id == user_id를 강제한다.PATCH /api/v1/organizations/{organization_id}/members/{user_id}Permission:
Request:
{ "membership_state": "suspended", "organization_auth_state": "manager" }Rules:
membership_state는active,suspended만 직접 변경 가능하다.removed는 DELETE endpoint만 사용한다.active로 바꾸는 manager-side 강제 accept는 허용하지 않는다.organization.member.update를 before/after metadata와 함께 기록한다.DELETE /api/v1/organizations/{organization_id}/members/{user_id}Permission:
Rules:
removed로 변경한다.removed_at=now를 기록한다.status='removed'를 반환한다.team_membershipswheregrantee_organization_id=organization_idanduser_id=target_user_iduser_workflow_permissionswheregrantee_organization_id=organization_idanduser_id=target_user_iduser_llm_permissionswheregrantee_organization_id=organization_idanduser_id=target_user_iduser_knowledge_permissions가 아직 없으면 count 0user_audit_permissions가 아직 없으면 count 0Response:
{ "status": "removed", "removed_team_memberships": 3, "revoked_user_permissions": { "workflow": 2, "llm_credential": 1, "knowledge_base": 0, "audit": 0 } }정합성 메모:
member removal cleanup이 언급되어 있지만, API 계약13.7 Remove member가 cleanup count response를 요구한다. 따라서 이 이슈에서 remove endpoint의 최소 cleanup transaction을 구현한다.4. State transition 구현
허용 transition:
불허:
5. Last manager guard
Guard 대상:
suspendedmember판정 기준:
organization_memberships중organization_auth_state='manager'row count를 기준으로 한다.created_by/managed_byfallback을 보조적으로 고려한다.6. Audit action 추가
파일:
apps/shared/audit/actions.py추가 action:
기존 action 재사용:
PERMISSION_REVOKE = "permission.revoke"는 cleanup aggregate 기록에 재사용 가능하다.Audit metadata 원칙:
예시:
{ "organization_id": "uuid", "membership_id": "uuid", "target_user_id": "uuid", "previous_membership_state": "active", "next_membership_state": "removed", "previous_organization_auth_state": "member", "next_organization_auth_state": "member", "reason": "organization.member.remove", "cleanup": { "team_memberships": 2, "user_workflow_permissions": 1, "user_llm_permissions": 1, "user_knowledge_permissions": 0, "user_audit_permissions": 0 } }Audit timing:
7. 테스트 추가
파일 후보:
apps/gateway/tests/services/test_organization_member_service.pyapps/gateway/tests/api/test_organization_members_api.pyapps/shared/tests/test_audit_actions.pyService test:
invited_at갱신 없이 기존 row 반환.removed_at,accepted_at을 초기화한다.accepted_at이 설정된다.API test:
GET /api/v1/organizations는 current user의 active/invited organizations만 반환한다.GET /api/v1/organizations/current는 active membership 기준 current organization을 반환한다.GET /organizations/{id}/members를 호출할 수 있다.POST /members/me/accept로 직접 accept할 수 있다.Audit action test:
AuditAction.ORGANIZATION_INVITE == "organization.invite"AuditAction.ORGANIZATION_MEMBER_ACCEPT == "organization.member.accept"AuditAction.ORGANIZATION_MEMBER_UPDATE == "organization.member.update"AuditAction.ORGANIZATION_MEMBER_REMOVE == "organization.member.remove"완료 조건
apps/shared/schemas/organization_membership.py가 추가된다.apps/gateway/services/organization_member_service.py가 추가된다./api/v1/organizationsrouter가 추가되고api.py에 등록된다.GET /organizations가 active/invited organization membership 기준으로 동작한다.GET /organizations/current가 active organization membership 기준으로 동작한다.team_memberships,user_workflow_permissions,user_llm_permissionscleanup이 같은 transaction에서 수행된다./teams,/permissionsendpoint request/response shape는 변경하지 않는다.명시적 제외 범위
이 이슈에서 하지 않는다.
organization_membershipsmodel/migration 추가:MBA-51MBA-52_ensure_grantee_user_membership제거/의미 변경: Issue 0-4user_knowledge_permissionsmigration 및 knowledge base use enforcement후속 연결
이 이슈가 끝나면 Issue 0-4에서 기존 team/direct permission mutation 경로를 active organization membership 기준으로 전환한다. 그 뒤 Issue 0-5에서 Members UI와 picker filtering을 붙인다.