목적
MVP 2-0의 organization_memberships DB foundation이 들어간 뒤, shared permission helper가 active organization membership을 resource permission 계산의 선행 조건으로 사용하게 전환한다.
핵심 목표는 기존 team_memberships 기반 permission 계산을 그대로 유지하되, 그 앞단에 organization_memberships.membership_state='active' 확인을 추가하는 것이다. active organization member가 아니면 team permission row나 user direct permission row가 존재해도 workflow/LLM credential 접근은 fail-closed 되어야 한다.
선행 조건
Blocked by:
MBA-51: [DB][RBAC] MVP2-0 Issue 0-1: organization_memberships 모델과 migration 추가
이 이슈는 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"
문서 기준 및 정합성 확인
Source of truth:
docs/implementation-plan/mvp-2-0-organization-membership-plan.md
docs/data-model/rbac-permission-policy.md
docs/data-model/physical-data-model.md
정합성 확인:
- 계획서
Issue 0-2의 작업 범위는 permission helper 전환이다.
- 계획서
11.1 신규 helper의 helper 4개를 추가/전환한다.
- 계획서
11.2 resource permission 계산 전제에 맞춰 workflow/LLM credential permission 계산 앞단에서 active organization membership을 확인한다.
- 계획서
11.3 fail-closed 정책에 맞춰 invited/suspended/removed/no membership user는 resource permission row가 있어도 거부한다.
organization.created_by/organization.managed_by legacy fallback은 owner/manager 전용 예외로 유지한다.
- 기존 team permission 계산 join 방식과 user direct permission additive allow 정책은 유지한다.
- 이 이슈는 organization member/invitation API, team add 검증 기준 변경, user direct permission grant 대상 검증 변경, member removal cleanup, FE UI를 구현하지 않는다. 해당 작업은 Issue 0-3/0-4/0-5 범위다.
현재 코드 기준
현재 주요 변경 지점:
apps/shared/services/permissions.py
_manager_state_for_organization()은 현재 organization.created_by/managed_by만 manager로 본다.
has_organization_manager_permission()은 membership row를 보지 않는다.
get_effective_workflow_auth_state()는 active organization membership 선검증 없이 team/direct permission row를 읽는다.
get_effective_llm_credential_auth_state()도 active organization membership 선검증 없이 team/direct permission row를 읽는다.
apps/shared/tests/services/test_permissions.py
- 현재 fake DB 기반 permission helper unit test가 있다.
- legacy owner fallback, team/direct permission precedence, fail-closed 기본 동작 테스트가 이미 있으므로 MVP2-0 membership state 테스트를 추가한다.
구현 범위
1. 신규 helper 추가
파일:
apps/shared/services/permissions.py
추가 helper:
get_organization_membership(db, user_id, organization_id)
has_active_organization_membership(db, user_id, organization_id)
get_organization_auth_state(db, user_id, organization_id)
has_organization_manager_permission(db, user_id, organization_id)
권장 반환/동작:
get_organization_membership(db, user_id, organization_id)
user_id, organization_id를 coerce_uuid()로 변환한다.
- 변환 실패 시
None을 반환한다.
Organization이 존재하고 is_active=True인지 확인한다.
- active organization이 아니면
None을 반환한다.
OrganizationMembership에서 (organization_id, user_id) row를 조회한다.
- permission helper에서는 relationship lazy loading에 의존하지 않고 명시 query를 사용한다.
has_active_organization_membership(db, user_id, organization_id)
get_organization_membership() 결과의 membership_state == 'active'일 때만 True를 반환한다.
invited, suspended, removed, missing row는 모두 False다.
- deactivated user 차단이 가능한 구조라면
User.deactivated_at is None도 함께 확인한다. 어렵다면 별도 helper 또는 TODO가 아니라 테스트 가능한 쿼리로 처리한다.
get_organization_auth_state(db, user_id, organization_id)
반환 후보:
"manager"
"member"
"none"
판정 순서:
- UUID coercion 실패 시
"none" 반환
- organization이 없거나 inactive면
"none" 반환
- active
OrganizationMembership row가 있으면 organization_auth_state를 반환
- active membership이 없더라도 user가
organization.created_by 또는 organization.managed_by이면 legacy fallback으로 "manager" 반환
- 그 외에는
"none" 반환
주의:
organization_auth_state의 member/manager는 resource auth state의 viewer/operator/builder/manager와 의미가 다르다.
- 하지만 organization manager는 resource scope에서 기존 정책처럼 resource
manager override를 받는다.
has_organization_manager_permission(db, user_id, organization_id)
- 기존 함수명은 유지한다.
- 내부 구현을
get_organization_auth_state(...) == ORGANIZATION_AUTH_MANAGER 기준으로 전환한다.
- legacy fallback은
get_organization_auth_state() 내부에서만 제한적으로 처리한다.
2. Workflow permission helper 전환
파일:
apps/shared/services/permissions.py
대상 함수:
get_effective_workflow_auth_state()
has_workflow_permission()은 위 함수를 그대로 통해 동작해야 한다.
새 판정 순서:
user_id, workflow_id, organization_id scope를 기존처럼 coerce/resolve한다.
- workflow organization과 requested organization이 불일치하면
none을 반환한다.
- organization이 없거나 inactive면
none을 반환한다.
get_organization_auth_state(db, user_uuid, organization_uuid)를 호출한다.
- 결과가
manager면 resource manager를 즉시 반환한다.
- 결과가
member가 아니면 none을 반환한다.
- 기존 team workflow permission query를 수행한다.
- 기존 user workflow direct permission query를 수행한다.
- 기존
stronger_resource_auth_state 우선순위로 가장 강한 resource auth state를 반환한다.
중요:
- active membership이 없는 일반 user는 team permission row가 있어도
none이다.
- active membership이 없는 일반 user는 user direct permission row가 있어도
none이다.
- organization creator/managed_by legacy fallback은 active membership이 없어도 manager로 허용한다.
- team permission 계산 query 자체는 기존 방식과 동일해야 한다.
3. LLM credential permission helper 전환
파일:
apps/shared/services/permissions.py
대상 함수:
get_effective_llm_credential_auth_state()
has_llm_credential_permission()은 위 함수를 그대로 통해 동작해야 한다.
새 판정 순서:
user_id, llm_credential_id, organization_id scope를 기존처럼 coerce/resolve한다.
- credential organization과 requested organization이 불일치하면
none을 반환한다.
- organization-scoped credential이면 organization이 active인지 확인한다.
get_organization_auth_state(db, user_uuid, organization_uuid)를 호출한다.
- 결과가
manager면 resource manager를 즉시 반환한다.
- 결과가
member가 아니면 none을 반환한다.
- 기존 team LLM permission query를 수행한다.
- 기존 user LLM direct permission query를 수행한다.
- 기존
stronger_resource_auth_state 우선순위로 가장 강한 resource auth state를 반환한다.
기존 credential owner fallback 주의:
- 기존 코드에는 organization scope가 없는 credential에서
credential.user_id == user_id이면 manager를 반환하는 fallback이 있다.
- MVP2-0 문서의 핵심은 organization-scoped resource에 대한 membership 선검증이다.
- legacy user-scoped credential fallback을 유지할지 제거할지는 기존 MVP1 회귀 위험을 보고 판단하되, organization_id가 있는 credential에는 반드시 organization membership 선검증을 적용한다.
- 판단이 애매하면 테스트로 기존 user-scoped fallback 유지와 organization-scoped fail-closed를 분리한다.
4. Fail-closed 정책 구현
다음 조건은 workflow/LLM credential permission에서 none 또는 False가 되어야 한다.
- organization이 inactive
- active organization membership이 없음
- membership_state가
invited
- membership_state가
suspended
- membership_state가
removed
- resource organization과 requested organization이 불일치
- user가 deactivated
예외:
organization.created_by 또는 organization.managed_by인 legacy owner/manager는 active membership이 누락되어도 manager fallback을 허용한다.
- 이 예외는 owner/manager 전용이다. 일반 user의 team/direct permission row에는 적용하지 않는다.
5. 테스트 추가/수정
파일 후보:
apps/shared/tests/services/test_permissions.py
- 필요하면 helper 전용 테스트 파일 분리 가능
필수 테스트:
- active organization member + team workflow viewer -> workflow viewer
- no organization membership + team workflow viewer row만 존재 -> workflow none
- no organization membership + user direct workflow builder row만 존재 -> workflow none
- invited membership + user direct workflow builder -> workflow none
- suspended membership + team workflow manager -> workflow none
- removed membership + team workflow manager -> workflow none
- active organization member + user direct workflow builder + no team -> workflow builder
- active manager membership -> workflow manager
- organization.created_by legacy fallback + no membership -> workflow manager
- organization.managed_by legacy fallback + no membership -> workflow manager
- inactive organization -> workflow none even if membership/permission rows exist
- requested organization mismatch -> workflow none
- active organization member + team LLM operator/use -> LLM operator/use allowed
- no organization membership + user direct LLM manager row -> LLM none
- invited/suspended/removed membership + LLM permission row -> LLM none
- active manager membership -> LLM manager
- deactivated user -> workflow/LLM none
기존 테스트 유지:
- legacy auth state normalization
- workflow action matrix
- LLM action matrix
- team/direct permission precedence
- SQLAlchemy row unpacking
- audit-only auth state fail-closed for resource permissions
Fake DB 테스트를 유지할 경우:
- query 순서가 바뀌므로
FakeDb.first_values/all_values를 새 helper 호출 순서에 맞게 수정한다.
- 또는 SQLAlchemy in-memory/session fixture를 쓰는 테스트로 일부 helper 테스트를 대체한다.
- 테스트가 helper 구현의 query call order에 과도하게 묶이지 않도록, membership helper 단위 테스트와 effective auth state 테스트를 분리하는 것을 권장한다.
완료 조건
명시적 제외 범위
이 이슈에서 하지 않는다.
organization_memberships 모델/migration 추가: MBA-51 범위
get_user_primary_organization_id()를 organization membership 기준으로 바꾸는 Gateway context 전환
ensure_user_default_organization() runtime reconciliation guard
- organization member/invitation service/API
- team add 시 active organization membership 검증
- user direct permission grant 시 active organization membership 검증
- organization member removal cleanup
- FE Members UI, invite modal, picker filtering
- MVP2
user_knowledge_permissions migration 및 knowledge base use enforcement
후속 연결
이 이슈가 끝나면 다음 작업이 가능해진다.
- Issue 0-3: organization member/invitation API 추가
- Issue 0-4: team membership과 user direct permission 검증 기준 변경
- MVP2 본작업의 knowledge/RAG permission enforcement 설계 고정
특히 MVP2의 user_knowledge_permissions, knowledge base use enforcement, RAG node execution check는 organization membership 선검증 정책이 이 이슈에서 고정된 뒤 진행한다.
목적
MVP 2-0의
organization_membershipsDB foundation이 들어간 뒤, shared permission helper가 active organization membership을 resource permission 계산의 선행 조건으로 사용하게 전환한다.핵심 목표는 기존
team_memberships기반 permission 계산을 그대로 유지하되, 그 앞단에organization_memberships.membership_state='active'확인을 추가하는 것이다. active organization member가 아니면 team permission row나 user direct permission row가 존재해도 workflow/LLM credential 접근은 fail-closed 되어야 한다.선행 조건
Blocked by:
MBA-51:[DB][RBAC] MVP2-0 Issue 0-1: organization_memberships 모델과 migration 추가이 이슈는
OrganizationMembership모델과 아래 constant가 존재한다는 전제로 구현한다.문서 기준 및 정합성 확인
Source of truth:
docs/implementation-plan/mvp-2-0-organization-membership-plan.mddocs/data-model/rbac-permission-policy.mddocs/data-model/physical-data-model.md정합성 확인:
Issue 0-2의 작업 범위는 permission helper 전환이다.11.1 신규 helper의 helper 4개를 추가/전환한다.11.2 resource permission 계산 전제에 맞춰 workflow/LLM credential permission 계산 앞단에서 active organization membership을 확인한다.11.3 fail-closed 정책에 맞춰 invited/suspended/removed/no membership user는 resource permission row가 있어도 거부한다.organization.created_by/organization.managed_bylegacy fallback은 owner/manager 전용 예외로 유지한다.현재 코드 기준
현재 주요 변경 지점:
apps/shared/services/permissions.py_manager_state_for_organization()은 현재organization.created_by/managed_by만 manager로 본다.has_organization_manager_permission()은 membership row를 보지 않는다.get_effective_workflow_auth_state()는 active organization membership 선검증 없이 team/direct permission row를 읽는다.get_effective_llm_credential_auth_state()도 active organization membership 선검증 없이 team/direct permission row를 읽는다.apps/shared/tests/services/test_permissions.py구현 범위
1. 신규 helper 추가
파일:
apps/shared/services/permissions.py추가 helper:
권장 반환/동작:
get_organization_membership(db, user_id, organization_id)user_id,organization_id를coerce_uuid()로 변환한다.None을 반환한다.Organization이 존재하고is_active=True인지 확인한다.None을 반환한다.OrganizationMembership에서(organization_id, user_id)row를 조회한다.has_active_organization_membership(db, user_id, organization_id)get_organization_membership()결과의membership_state == 'active'일 때만True를 반환한다.invited,suspended,removed, missing row는 모두False다.User.deactivated_at is None도 함께 확인한다. 어렵다면 별도 helper 또는 TODO가 아니라 테스트 가능한 쿼리로 처리한다.get_organization_auth_state(db, user_id, organization_id)반환 후보:
"manager""member""none"판정 순서:
"none"반환"none"반환OrganizationMembershiprow가 있으면organization_auth_state를 반환organization.created_by또는organization.managed_by이면 legacy fallback으로"manager"반환"none"반환주의:
organization_auth_state의member/manager는 resource auth state의viewer/operator/builder/manager와 의미가 다르다.manageroverride를 받는다.has_organization_manager_permission(db, user_id, organization_id)get_organization_auth_state(...) == ORGANIZATION_AUTH_MANAGER기준으로 전환한다.get_organization_auth_state()내부에서만 제한적으로 처리한다.2. Workflow permission helper 전환
파일:
apps/shared/services/permissions.py대상 함수:
get_effective_workflow_auth_state()has_workflow_permission()은 위 함수를 그대로 통해 동작해야 한다.새 판정 순서:
user_id,workflow_id,organization_idscope를 기존처럼 coerce/resolve한다.none을 반환한다.none을 반환한다.get_organization_auth_state(db, user_uuid, organization_uuid)를 호출한다.manager면 resourcemanager를 즉시 반환한다.member가 아니면none을 반환한다.stronger_resource_auth_state우선순위로 가장 강한 resource auth state를 반환한다.중요:
none이다.none이다.3. LLM credential permission helper 전환
파일:
apps/shared/services/permissions.py대상 함수:
get_effective_llm_credential_auth_state()has_llm_credential_permission()은 위 함수를 그대로 통해 동작해야 한다.새 판정 순서:
user_id,llm_credential_id,organization_idscope를 기존처럼 coerce/resolve한다.none을 반환한다.get_organization_auth_state(db, user_uuid, organization_uuid)를 호출한다.manager면 resourcemanager를 즉시 반환한다.member가 아니면none을 반환한다.stronger_resource_auth_state우선순위로 가장 강한 resource auth state를 반환한다.기존 credential owner fallback 주의:
credential.user_id == user_id이면 manager를 반환하는 fallback이 있다.4. Fail-closed 정책 구현
다음 조건은 workflow/LLM credential permission에서
none또는False가 되어야 한다.invitedsuspendedremoved예외:
organization.created_by또는organization.managed_by인 legacy owner/manager는 active membership이 누락되어도 manager fallback을 허용한다.5. 테스트 추가/수정
파일 후보:
apps/shared/tests/services/test_permissions.py필수 테스트:
기존 테스트 유지:
Fake DB 테스트를 유지할 경우:
FakeDb.first_values/all_values를 새 helper 호출 순서에 맞게 수정한다.완료 조건
get_organization_membership()이 추가된다.has_active_organization_membership()이 추가된다.get_organization_auth_state()가 추가된다.has_organization_manager_permission()이 membership 기준 + legacy fallback 기준으로 전환된다.organization.created_by/organization.managed_bylegacy fallback은 유지된다.명시적 제외 범위
이 이슈에서 하지 않는다.
organization_memberships모델/migration 추가:MBA-51범위get_user_primary_organization_id()를 organization membership 기준으로 바꾸는 Gateway context 전환ensure_user_default_organization()runtime reconciliation guarduser_knowledge_permissionsmigration 및 knowledge base use enforcement후속 연결
이 이슈가 끝나면 다음 작업이 가능해진다.
특히 MVP2의
user_knowledge_permissions, knowledge baseuseenforcement, RAG node execution check는 organization membership 선검증 정책이 이 이슈에서 고정된 뒤 진행한다.