목적
MVP2-0 Organization Membership / Invitation Foundation의 핵심 백엔드 전환 작업이다. 기존에는 특정 user에게 direct workflow/LLM permission을 부여하려면 그 user가 target organization의 어떤 team에든 먼저 속해 있어야 했다. 이 기준을 organization_memberships.membership_state='active'인 organization member 기준으로 바꾼다.
변경 후 정책은 다음과 같다.
- active organization member만 team에 추가할 수 있다.
- active organization member만 workflow/LLM user direct permission grant 대상이 될 수 있다.
- team membership은 더 이상 user direct permission의 전제 조건이 아니다.
- organization member가 remove되면 해당 organization의 team membership과 user direct permission row를 hard delete로 cleanup한다.
- 기존 MVP1 team/resource permission API request/response shape는 유지한다.
문서 정합성
기준 문서: docs/implementation-plan/mvp-2-0-organization-membership-plan.md
이 이슈는 문서의 다음 항목과 직접 대응한다.
Issue 0-4. [BE][RBAC] team membership과 user direct permission 검증 기준 변경
- team add 시 active organization membership 확인
- user direct workflow/LLM permission grant 시 organization membership 확인
_ensure_grantee_user_membership 제거 또는 의미 변경
- member removal cleanup 구현
7.2 상태 정의
active만 resource 접근 가능성, team 추가 가능, direct permission 대상 가능
invited, suspended, removed는 모두 resource 접근/team 추가/direct permission 대상 불가
- removed 시 team membership과 user direct permission은 hard delete
8.1 team_memberships
- migration 이후 모든 team membership row는 active organization membership row를 전제로 한다.
- 신규 team membership 생성 시 active organization membership이 없으면 400을 반환한다.
8.2 user_*_permissions
- 모든 user direct permission은 active organization membership을 전제로 한다.
- team membership은 더 이상 user direct permission의 전제 조건이 아니다.
- organization에서 user가 제거되면 해당 organization의 user direct permission을 revoke한다.
12.3 Member removal cleanup
- organization member removal은 team membership delete, user_* permission delete, organization membership removed update를 하나의 transaction에서 처리한다.
12.4 permission grant/revoke
- team grant는 기존대로 team 기준
- user direct grant는 active organization member 기준
13.7 Remove member, 13.8 상태 전이표, 13.9 MVP 1 API 호환성
- remove는 cleanup 포함
- suspend는 cleanup하지 않고 helper가 fail-closed 처리
- 기존 MVP1 endpoint는 유지하고 내부 검증 기준만 변경
22. 완료 기준
- Team, User direct permission, Cleanup 항목과 직접 연결된다.
현재 코드 기준 변경 후보
현재 구현 전 상태에 가까운 지점:
apps/gateway/services/team_service.py
_ensure_grantee_user_membership()가 TeamMembership join으로 user direct permission grant 대상을 검증한다.
TeamService.add_membership()가 target user의 active organization membership을 확인하지 않고 team membership row를 생성한다.
TeamService.grant_resource_permission()가 grantee_type='user'일 때 _ensure_grantee_user_membership()을 호출한다.
TeamService.revoke_resource_permission()는 기존 shape를 유지해야 하며, cleanup/manual revoke를 위해 target user가 active가 아니어도 revoke 가능해야 한다.
apps/shared/services/permissions.py
- Issue 0-2에서 추가되는
has_active_organization_membership, get_organization_auth_state를 이 이슈의 service validation에서 재사용해야 한다.
- 이 이슈에서 permission 계산 로직을 새로 중복 구현하지 않는다.
선행 조건 / 의존성
Blocked by:
MBA-53: Issue 0-3 organization member/invitation API
Transitive dependency:
MBA-51: Issue 0-1 organization_memberships DB/model/migration
MBA-52: Issue 0-2 organization membership 기반 permission helper 전환
Blocks:
MBA-54: Issue 0-5 Organization Members UI
MBA-55: Issue 0-6 regression 및 acceptance smoke
구현 범위
1. active organization membership 검증 helper 사용
목표: Gateway service layer에서 target user가 target organization의 active member인지 일관되게 검사한다.
작업:
- 기존
_ensure_grantee_user_membership()를 제거하거나 의미를 변경한다.
- 권장 이름:
_ensure_grantee_organization_membership() 또는 _ensure_active_organization_member().
- Issue 0-2에서 만든 shared helper를 사용한다.
has_active_organization_membership(db, user_id, organization_id)
- 또는
get_organization_auth_state(db, user_id, organization_id)
- 직접 query를 새로 작성해야 한다면 0-2 helper와 policy가 어긋나지 않게 shared helper로 끌어올린다.
검증 기준:
- target user가 존재해야 한다.
- target user가 deactivated 상태면 실패한다.
- membership row가 없어도 실패한다.
membership_state='active'가 아니면 실패한다.
organization_auth_state는 member 또는 manager만 허용한다.
- target organization mismatch는 실패한다.
권장 실패 응답:
- status code: 400
- detail: 기존 client 호환을 고려해 큰 이유 노출 없이
Grantee user is not an active organization member 또는 기존 메시지와 호환되는 문구 사용
- caller가 manager/manage 권한이 없는 경우는 기존대로 403
2. team add 검증 기준 변경
대상:
TeamService.add_membership()
POST /api/v1/teams/{team_id}/members
- legacy alias endpoint가 있으면 동일 적용
작업:
- team 조회 후 team이 target organization에 속하고 active인지 확인한다.
- current user가 organization manager인지 확인하는 기존 정책은 유지한다.
- target user가 active organization member인지 확인한 뒤 team membership을 생성한다.
- active organization membership이 없으면 team membership을 생성하지 않는다.
- 기존 team membership row가 이미 있더라도 target user가 더 이상 active organization member가 아니라면 성공으로 반환하지 않는다.
- team membership 생성이 organization membership을 암묵적으로 생성하면 안 된다.
정책:
active member: team add 가능
invited member: team add 불가
suspended member: team add 불가
removed member: team add 불가
- no membership: team add 불가
- deactivated user: team add 불가
3. team remove semantics 유지
대상:
TeamService.remove_membership()
DELETE /api/v1/teams/{team_id}/members/{user_id}
작업:
- team membership 제거는 hard delete 유지
- organization membership은 제거하지 않는다.
- organization member lifecycle과 team membership lifecycle을 분리한다.
- existing endpoint response shape는 유지한다.
검증:
- team에서 제거해도
organization_memberships row는 그대로 남는다.
- team에서 제거된 user가 active organization member라면 이후 user direct permission grant 대상이 될 수 있다.
4. user direct workflow permission grant 기준 변경
대상:
TeamService.grant_resource_permission() 중 grantee_type='user', resource_type='workflow'
/api/v1/permissions/workflows/{workflow_id}/users/{user_id} 형태의 legacy endpoint가 있으면 동일 적용
/api/v1/teams/permissions generic endpoint가 있으면 동일 적용
작업:
- resource organization mismatch 검증은 기존대로 유지한다.
- current user가 resource manager 또는 organization manager인지 확인하는 기존 정책은 유지한다.
- grantee user 검증 기준을 team membership에서 active organization membership으로 변경한다.
- target user가 team에 없어도 active organization member면 grant를 허용한다.
- target user가 active organization member가 아니면 grant를 차단한다.
- 기존
UserWorkflowPermission create/update semantics와 unique constraint 사용 방식은 유지한다.
정책:
- active organization member + no team membership: grant 가능
- active organization member + team membership 있음: grant 가능
- invited/suspended/removed/no-membership/deactivated user: grant 불가
5. user direct LLM credential permission grant 기준 변경
대상:
TeamService.grant_resource_permission() 중 grantee_type='user', resource_type='llm_credential'
/api/v1/permissions/llm-credentials/{credential_id}/users/{user_id} 형태의 legacy endpoint가 있으면 동일 적용
작업:
- workflow user direct grant와 같은 active organization membership 기준을 적용한다.
- LLM credential organization mismatch 검증은 기존대로 유지한다.
- existing request/response shape는 유지한다.
- 기존
UserLLMPermission create/update semantics와 unique constraint 사용 방식은 유지한다.
정책:
- active organization member + no team membership: grant 가능
- invited/suspended/removed/no-membership/deactivated user: grant 불가
6. user direct permission revoke는 active membership을 요구하지 않음
대상:
TeamService.revoke_resource_permission()
- workflow/LLM user revoke endpoint
작업:
- revoke는 cleanup/manual recovery 목적이 있으므로 grantee가 active organization member인지 검사하지 않는다.
- caller의 manager/manage 권한 검증과 resource organization mismatch 검증은 유지한다.
- row가 없을 때 기존 idempotent behavior가 있으면 유지한다.
이유:
- removed/suspended/deactivated user의 stale direct permission row를 관리자가 revoke할 수 있어야 한다.
- member removal cleanup 실패나 과거 데이터 잔존을 수동으로 정리할 수 있어야 한다.
7. organization member removal cleanup 구현 또는 정합성 보강
대상:
- Issue 0-3에서 추가되는 organization member service의 remove method
- 예상 endpoint:
DELETE /api/v1/organizations/{organization_id}/members/{user_id}
작업:
- remove는 하나의 transaction 안에서 처리한다.
- 마지막 manager 제거 금지.
- 자기 자신 제거 금지.
- affected row count를 계산한다.
team_memberships
user_workflow_permissions
user_llm_permissions
- MVP2 knowledge permission table이 이미 존재하면
user_knowledge_permissions
- 해당 organization의 target user team membership을 hard delete한다.
- 해당 organization의 target user direct permission을 hard delete한다.
organization_memberships.membership_state를 removed로 변경한다.
removed_at을 기록한다.
- audit log에 cleanup count metadata를 남긴다.
- 중간 실패 시 일부 cleanup만 commit되면 안 된다.
주의:
suspended 전환 시에는 team/direct permission row를 삭제하지 않는다.
reactivate 시에는 suspend 중 유지된 row가 다시 효력을 가진다.
removed 후 재초대/accept해도 과거 team/direct permission은 자동 복구되지 않는다.
8. MVP1 API compatibility 유지
유지해야 하는 것:
POST /api/v1/teams/{team_id}/members endpoint shape
DELETE /api/v1/teams/{team_id}/members/{user_id} endpoint shape
- workflow team permission grant/revoke shape
- workflow user permission grant/revoke shape
- LLM credential team permission grant/revoke shape
- LLM credential user permission grant/revoke shape
- permission mutation audit action의 기본 category/action 구조
변경되는 것:
- team add target validation 기준
- user direct grant target validation 기준
- organization member removal cleanup side effect
변경하지 말아야 하는 것:
- team permission 계산 방식 자체
- team permission grant/revoke 대상 검증
- resource auth_state enum
- team membership table schema
- 기존 endpoint URL/HTTP method/request body/response model
테스트 범위
이 이슈에서 최소한 service/API regression test를 추가한다. 0-6에서 더 넓은 regression을 수행하되, 0-4의 정책은 여기서 먼저 고정한다.
권장 테스트 위치:
apps/gateway/tests/services/test_team_service_permissions.py
apps/gateway/tests/api/test_team_memberships.py
apps/gateway/tests/api/test_resource_permissions.py
- organization member removal service/API test는 0-3에서 만든 파일에 추가
필수 테스트:
Team add
- active organization member를 team에 추가할 수 있다.
- active organization member가 이미 team member면 기존 idempotent behavior를 유지한다.
- invited member를 team에 추가할 수 없다.
- suspended member를 team에 추가할 수 없다.
- removed member를 team에 추가할 수 없다.
- no membership user를 team에 추가할 수 없다.
- deactivated user를 team에 추가할 수 없다.
- inactive team에는 member를 추가할 수 없다.
- team organization과 membership organization이 다르면 추가할 수 없다.
Team remove
- team remove는 team membership만 삭제한다.
- team remove는 organization membership을 삭제하거나 removed 상태로 바꾸지 않는다.
- team remove 후에도 active organization member에게 user direct permission을 부여할 수 있다.
Workflow user direct grant
- team에 속하지 않은 active organization member에게 workflow direct builder/viewer/operator/manager permission을 줄 수 있다.
- active organization member + 기존 team membership user에게 grant가 기존처럼 동작한다.
- invited/suspended/removed/no-membership/deactivated user에게 workflow direct permission을 줄 수 없다.
- resource organization mismatch는 기존대로 실패한다.
- caller가 manage 권한이 없으면 기존대로 실패한다.
LLM user direct grant
- team에 속하지 않은 active organization member에게 LLM direct permission을 줄 수 있다.
- invited/suspended/removed/no-membership/deactivated user에게 LLM direct permission을 줄 수 없다.
- credential organization mismatch는 기존대로 실패한다.
- caller가 manage 권한이 없으면 기존대로 실패한다.
Revoke
- removed/suspended/non-active user의 stale workflow direct permission row도 manager가 revoke할 수 있다.
- removed/suspended/non-active user의 stale LLM direct permission row도 manager가 revoke할 수 있다.
- 없는 row revoke는 기존 idempotent behavior를 유지한다.
Organization member remove cleanup
- active member remove 시 해당 organization의 team memberships가 삭제된다.
- active member remove 시 해당 organization의 user workflow permissions가 삭제된다.
- active member remove 시 해당 organization의 user LLM permissions가 삭제된다.
- 다른 organization의 team/direct permission row는 삭제하지 않는다.
- invited member remove도 removed 상태로 전환된다.
- suspended member remove도 cleanup 후 removed 상태가 된다.
- 마지막 manager 제거는 차단된다.
- 자기 자신 제거는 차단된다.
- cleanup count가 response/audit metadata에 반영된다.
- transaction 실패 시 partial cleanup이 남지 않는다.
Acceptance criteria
- team에 속하지 않은 active organization member에게 workflow/LLM direct permission을 줄 수 있다.
- organization member가 아닌 user에게 workflow/LLM direct permission을 줄 수 없다.
- invited/suspended/removed/no-membership/deactivated user에게 workflow/LLM direct permission을 줄 수 없다.
- organization member가 아닌 user를 team에 추가할 수 없다.
- invited/suspended/removed/no-membership/deactivated user를 team에 추가할 수 없다.
- team membership 생성은 organization membership을 암묵적으로 만들지 않는다.
- team membership 제거는 organization membership을 제거하지 않는다.
- organization member 제거 시 해당 organization의 team membership과 user direct permission이 cleanup된다.
- cleanup은 하나의 transaction으로 처리된다.
- suspend/reactivate는 team/direct permission row를 삭제하지 않는다.
- user direct revoke는 target user의 active membership을 요구하지 않는다.
- 기존 MVP1 permission endpoint URL/request/response shape가 유지된다.
- 0-2 permission helper와 0-4 service validation 기준이 서로 어긋나지 않는다.
권장 검증 명령
PYTHONPATH=$PWD apps/gateway/.venv/bin/python -m pytest apps/gateway/tests/services/test_team_service_permissions.py
PYTHONPATH=$PWD apps/gateway/.venv/bin/python -m pytest apps/gateway/tests/api/test_team_memberships.py
PYTHONPATH=$PWD apps/gateway/.venv/bin/python -m pytest apps/gateway/tests/api/test_resource_permissions.py
PYTHONPATH=$PWD apps/gateway/.venv/bin/python -m pytest apps/shared/tests/services/test_permissions.py
통합 확인:
Non-goals
organization_memberships 모델/migration/backfill 추가는 0-1 범위다.
- permission 계산 앞단 fail-closed helper 구현은 0-2 범위다. 이 이슈는 그 helper를 service validation에 재사용한다.
- organization member/invitation endpoint 전체 구현은 0-3 범위다. 이 이슈는 remove cleanup 정책을 정합성 있게 완성/보강한다.
- Members UI와 picker filtering은 0-5 범위다.
- 전체 regression gate와 MVP1 demo smoke는 0-6 범위다.
- MVP2 knowledge/RAG/data-source permission enforcement 본작업은 이 이슈 범위가 아니다. 단,
user_knowledge_permissions table이 이미 존재하는 경우 remove cleanup 대상에 포함할 수 있다.
리스크 / 주의점
- stale team membership row가 남아 있을 때 add_membership이 성공처럼 반환하면 removed/suspended user가 team permission을 되살릴 수 있다. active organization membership을 먼저 확인한다.
- direct permission row cleanup이 누락되면 removed user가 재초대 후 과거 권한을 자동 복구받을 수 있다. MVP2-0 정책은 과거 권한 복구 없음이다.
- suspend와 remove를 혼동하면 reactivate 동작이 문서와 달라진다. suspend는 row 유지, remove는 cleanup이다.
- revoke까지 active membership을 요구하면 stale permission cleanup이 어려워진다. grant와 revoke 검증 기준을 분리한다.
- 기존 MVP1 client compatibility를 위해 endpoint shape 변경이나 auth_state enum 변경은 피한다.
목적
MVP2-0 Organization Membership / Invitation Foundation의 핵심 백엔드 전환 작업이다. 기존에는 특정 user에게 direct workflow/LLM permission을 부여하려면 그 user가 target organization의 어떤 team에든 먼저 속해 있어야 했다. 이 기준을
organization_memberships.membership_state='active'인 organization member 기준으로 바꾼다.변경 후 정책은 다음과 같다.
문서 정합성
기준 문서:
docs/implementation-plan/mvp-2-0-organization-membership-plan.md이 이슈는 문서의 다음 항목과 직접 대응한다.
Issue 0-4. [BE][RBAC] team membership과 user direct permission 검증 기준 변경_ensure_grantee_user_membership제거 또는 의미 변경7.2 상태 정의active만 resource 접근 가능성, team 추가 가능, direct permission 대상 가능invited,suspended,removed는 모두 resource 접근/team 추가/direct permission 대상 불가8.1 team_memberships8.2 user_*_permissions12.3 Member removal cleanup12.4 permission grant/revoke13.7 Remove member,13.8 상태 전이표,13.9 MVP 1 API 호환성22. 완료 기준현재 코드 기준 변경 후보
현재 구현 전 상태에 가까운 지점:
apps/gateway/services/team_service.py_ensure_grantee_user_membership()가TeamMembershipjoin으로 user direct permission grant 대상을 검증한다.TeamService.add_membership()가 target user의 active organization membership을 확인하지 않고 team membership row를 생성한다.TeamService.grant_resource_permission()가grantee_type='user'일 때_ensure_grantee_user_membership()을 호출한다.TeamService.revoke_resource_permission()는 기존 shape를 유지해야 하며, cleanup/manual revoke를 위해 target user가 active가 아니어도 revoke 가능해야 한다.apps/shared/services/permissions.pyhas_active_organization_membership,get_organization_auth_state를 이 이슈의 service validation에서 재사용해야 한다.선행 조건 / 의존성
Blocked by:
MBA-53: Issue 0-3 organization member/invitation APITransitive dependency:
MBA-51: Issue 0-1 organization_memberships DB/model/migrationMBA-52: Issue 0-2 organization membership 기반 permission helper 전환Blocks:
MBA-54: Issue 0-5 Organization Members UIMBA-55: Issue 0-6 regression 및 acceptance smoke구현 범위
1. active organization membership 검증 helper 사용
목표: Gateway service layer에서 target user가 target organization의 active member인지 일관되게 검사한다.
작업:
_ensure_grantee_user_membership()를 제거하거나 의미를 변경한다._ensure_grantee_organization_membership()또는_ensure_active_organization_member().has_active_organization_membership(db, user_id, organization_id)get_organization_auth_state(db, user_id, organization_id)검증 기준:
membership_state='active'가 아니면 실패한다.organization_auth_state는member또는manager만 허용한다.권장 실패 응답:
Grantee user is not an active organization member또는 기존 메시지와 호환되는 문구 사용2. team add 검증 기준 변경
대상:
TeamService.add_membership()POST /api/v1/teams/{team_id}/members작업:
정책:
activemember: team add 가능invitedmember: team add 불가suspendedmember: team add 불가removedmember: team add 불가3. team remove semantics 유지
대상:
TeamService.remove_membership()DELETE /api/v1/teams/{team_id}/members/{user_id}작업:
검증:
organization_membershipsrow는 그대로 남는다.4. user direct workflow permission grant 기준 변경
대상:
TeamService.grant_resource_permission()중grantee_type='user',resource_type='workflow'/api/v1/permissions/workflows/{workflow_id}/users/{user_id}형태의 legacy endpoint가 있으면 동일 적용/api/v1/teams/permissionsgeneric endpoint가 있으면 동일 적용작업:
UserWorkflowPermissioncreate/update semantics와 unique constraint 사용 방식은 유지한다.정책:
5. user direct LLM credential permission grant 기준 변경
대상:
TeamService.grant_resource_permission()중grantee_type='user',resource_type='llm_credential'/api/v1/permissions/llm-credentials/{credential_id}/users/{user_id}형태의 legacy endpoint가 있으면 동일 적용작업:
UserLLMPermissioncreate/update semantics와 unique constraint 사용 방식은 유지한다.정책:
6. user direct permission revoke는 active membership을 요구하지 않음
대상:
TeamService.revoke_resource_permission()작업:
이유:
7. organization member removal cleanup 구현 또는 정합성 보강
대상:
DELETE /api/v1/organizations/{organization_id}/members/{user_id}작업:
team_membershipsuser_workflow_permissionsuser_llm_permissionsuser_knowledge_permissionsorganization_memberships.membership_state를removed로 변경한다.removed_at을 기록한다.주의:
suspended전환 시에는 team/direct permission row를 삭제하지 않는다.reactivate시에는 suspend 중 유지된 row가 다시 효력을 가진다.removed후 재초대/accept해도 과거 team/direct permission은 자동 복구되지 않는다.8. MVP1 API compatibility 유지
유지해야 하는 것:
POST /api/v1/teams/{team_id}/membersendpoint shapeDELETE /api/v1/teams/{team_id}/members/{user_id}endpoint shape변경되는 것:
변경하지 말아야 하는 것:
테스트 범위
이 이슈에서 최소한 service/API regression test를 추가한다. 0-6에서 더 넓은 regression을 수행하되, 0-4의 정책은 여기서 먼저 고정한다.
권장 테스트 위치:
apps/gateway/tests/services/test_team_service_permissions.pyapps/gateway/tests/api/test_team_memberships.pyapps/gateway/tests/api/test_resource_permissions.py필수 테스트:
Team add
Team remove
Workflow user direct grant
LLM user direct grant
Revoke
Organization member remove cleanup
Acceptance criteria
권장 검증 명령
통합 확인:
Non-goals
organization_memberships모델/migration/backfill 추가는 0-1 범위다.user_knowledge_permissionstable이 이미 존재하는 경우 remove cleanup 대상에 포함할 수 있다.리스크 / 주의점