Skip to content

feat: MVP1 RBAC 권한 모델 적용 - #65

Merged
HyungminYoon1 merged 9 commits into
nodease:devfrom
yoonki1207:feature/mba-41
Jun 27, 2026
Merged

feat: MVP1 RBAC 권한 모델 적용#65
HyungminYoon1 merged 9 commits into
nodease:devfrom
yoonki1207:feature/mba-41

Conversation

@yoonki1207

@yoonki1207 yoonki1207 commented Jun 27, 2026

Copy link
Copy Markdown
Member

변경 사항

  • MVP1 RBAC foundation을 추가했습니다.
    • 공통 permission vocabulary와 PermissionService를 추가했습니다.
    • 조직 역할, 사용자 직접 권한, 팀 권한, deny/effective permission 평가 흐름을 정리했습니다.
    • 권한 변경 감사 로그와 tracing 접근 제어를 연결했습니다.
  • 팀 권한 관리 API를 추가했습니다.
    • 팀 생성/수정/멤버 추가/제거는 organization owner/manager 기준으로 제한했습니다.
    • 사용자/팀 grant 대상의 active membership/team 검증을 추가했습니다.
  • App, Workflow, Deployment 접근 제어를 owner 기반에서 권한 기반으로 전환했습니다.
  • LLM credential 접근 제어를 문서 기준으로 정렬했습니다.
    • credential 목록/preview/model 목록 조회는 read 권한 기준으로 제한했습니다.
    • credential 생성/수정/삭제와 workflow 실행 시 use 권한 평가를 분리했습니다.
    • workflow engine 런타임에서도 credential permission 관계를 검증하도록 보강했습니다.
  • SQLAlchemy 2.0 단일 컬럼 Row 반환을 권한 grant 강도 비교 전에 unpack하도록 수정했습니다.
    • workflow와 LLM credential의 team/direct grant가 Row 객체 때문에 none으로 정규화되어 무시되는 문제를 막았습니다.
  • workflow stats endpoint의 권한 오류 응답을 보존하도록 수정했습니다.
    • GET /workflows/{workflow_id}/stats에서 read 권한이 없을 때 HTTPException(403)이 catch-all에 잡혀 500으로 바뀌지 않게 했습니다.

문서 변경 사항:

  • docs/api/organization-rbac.md
    • 조직 역할, resource permission, user/team grant, deny precedence, active membership/team 조건을 최신 구현 기준으로 정리했습니다.
    • App/Workflow/Deployment/LLM Credential/Tracing 리소스별 권한 규칙과 API 기대 동작을 보강했습니다.
  • docs/api/llm-credentials.md
    • credential 생성/조회/수정/삭제 권한과 model list/preview/use 권한 기준을 구현과 맞췄습니다.
    • 공유 credential의 read/use 분리와 workflow 실행 시 권한 평가 흐름을 명시했습니다.
  • docs/api/deployments.md
    • deployment 접근 조건을 creator/owner 중심 설명에서 permission 기반 설명으로 조정했습니다.
  • docs/implementation-plan/mvp-1-development-issue-plan.md

관련 이슈

Closes #56

변경 유형

  • 버그 수정
  • 새로운 기능
  • 리팩토링
  • 문서 수정
  • 기타

테스트

  • 로컬에서 테스트 완료
  • 기존 테스트 통과 확인

검증한 명령:

  • apps/gateway/.venv/bin/python -m compileall apps/shared apps/gateway apps/workflow_engine
  • apps/gateway/.venv/bin/python -m compileall apps/gateway/api/v1/endpoints/workflow.py apps/gateway/tests/api/test_workflow_stats_permissions.py
  • apps/gateway/.venv/bin/python -m pytest apps/shared/tests/services/test_permissions.py apps/gateway/tests/services/test_llm_service_permissions.py apps/gateway/tests/services/test_team_service_permissions.py apps/gateway/tests/api/test_permission_helpers.py apps/gateway/tests/api/test_permission_denied_audit.py -q
  • apps/gateway/.venv/bin/python -m pytest apps/gateway/tests/api/test_workflow_stats_permissions.py -q
  • apps/workflow_engine/.venv/bin/python -m pytest apps/workflow_engine/tests/nodes/test_llm_node_runtime.py apps/shared/tests/services/test_tracing_access.py -q

스크린샷 (UI 변경 시)

UI 변경 없음

@yoonki1207
yoonki1207 marked this pull request as ready for review June 27, 2026 08:39
@yoonki1207

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 38bd6cb3e9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/shared/services/permissions.py Outdated
Comment on lines +136 to +137
auth_state = row[0] if isinstance(row, tuple) else row
result = stronger_resource_auth_state(result, auth_state)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Unpack SQLAlchemy rows before ranking grants

The project depends on SQLAlchemy >=2.0, where single-column ORM queries like db.query(TeamWorkflowPermission.auth_state).all() return Row objects rather than plain tuples. Because this only indexes tuples, each row is passed wholesale into stronger_resource_auth_state, normalizes to none, and every team/direct grant is ignored; non-organization-manager users remain denied for workflows and LLM credentials even after being granted access.

Useful? React with 👍 / 👎.

Comment on lines +180 to +184
.filter(
UserWorkflowPermission.user_id == user_uuid,
UserWorkflowPermission.workflow_id == workflow.id,
UserWorkflowPermission.grantee_organization_id == organization_uuid,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Require active membership for direct user grants

Direct workflow grants are evaluated only by user/resource/org, unlike team grants which require an active team membership. If an org manager grants a user direct access and later removes that user's last team membership, TeamService.remove_membership deletes only the membership row, so this stale UserWorkflowPermission row still qualifies and the removed user keeps access; the UserLLMPermission path below has the same issue. Join/check current active membership here or delete direct grants on removal.

Useful? React with 👍 / 👎.

workflow = db.query(Workflow).filter(Workflow.id == workflow_id).first()
if not workflow:
raise HTTPException(status_code=404, detail="Workflow not found")
ensure_workflow_permission(db, current_user, workflow_id, "read")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve permission errors from the stats endpoint

When a caller lacks read access to this workflow, ensure_workflow_permission raises an HTTPException(403), but it is inside the broad try and the except Exception below converts it into a 500 response. This makes authorization failures on GET /workflows/{workflow_id}/stats look like server errors while the other workflow read endpoints correctly return 403/404; add an except HTTPException: raise before the catch-all or move the permission check outside the try.

Useful? React with 👍 / 👎.

@yoonki1207
yoonki1207 marked this pull request as draft June 27, 2026 08:50
@yoonki1207

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7f9b64a221

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +65 to +67
target_workflow_id = workflow_id
if app_id and not target_workflow_id:
app = db.query(App).filter(App.id == app_id).first()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Authorize the app_id that is actually listed

When both app_id and workflow_id are supplied, this check authorizes only the supplied workflow_id because the app lookup is skipped, while DeploymentService.list_deployments still returns deployments for app_id when it is present. A caller with read access to any workflow can therefore pass that workflow_id together with another app's id and read that app's deployment graph snapshots; resolve the app's workflow and reject mismatches or ignore app_id in this case.

Useful? React with 👍 / 👎.

Comment on lines +565 to +566
if not has_organization_manager_permission(db, user_id, organization_id):
raise PermissionError("Credential creation requires organization manager")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Flush the default organization before the manager check

For a user without an existing default membership, the fallback just above creates a default org via ensure_user_default_organization, but gateway sessions are configured with autoflush=False, so the new org/team/membership are still pending when this permission query runs. The manager lookup will not see the newly-created org and first credential registration through the fallback incorrectly returns 403; flush in the helper before returning or before this check.

Useful? React with 👍 / 👎.

@HyungminYoon1 HyungminYoon1 changed the title [codex] MVP1 RBAC 권한 모델 적용 feat: MVP1 RBAC 권한 모델 적용 Jun 27, 2026

@HyungminYoon1 HyungminYoon1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

검토 결과, RBAC foundation 방향은 전반적으로 맞습니다. dev 기준 merge result도 conflict 없이 생성되고, team/user direct permission, workflow/deployment/LLM credential 권한 적용은 #56 요구와 대체로 일치합니다.

다만 병합 전 수정이 필요한 항목이 있습니다.

  1. High: LLM pricing 관리 endpoint가 인증/권한 없이 열려 있습니다.

apps/gateway/api/v1/endpoints/llm.pyPOST /api/v1/llm/models/sync-pricing, PUT /api/v1/llm/models/{model_id}/pricingget_current_user나 system admin 권한 check 없이 DB state를 변경합니다. 그런데 docs/api/llm-credentials.md는 두 endpoint를 system admin 권한으로 명시하고 있어 문서와 구현도 충돌합니다.

병합 전 admin/system permission dependency와 deny test를 추가해 주세요.

  1. Medium: /api/v1/llm/providers 문서와 구현이 불일치합니다.

docs/api/llm-credentials.mdGET /api/v1/llm/providersauthenticated로 명시하지만, route 구현에는 current_user dependency가 없습니다. provider/model catalog를 공개로 둘 정책이면 문서를 바꾸고, authenticated가 맞으면 route에 dependency를 추가해야 합니다.

  1. Low/follow-up: permission denied audit metadata가 최소 수준입니다.

apps/gateway/auth/permissions.pyrecord_permission_deniedpermission_action, effective_auth_state만 남깁니다. #56 acceptance에는 충분할 수 있지만, 이후 #59 trace/audit 요구와 합칠 때는 request path/method/request_id/policy reason 같은 정보를 공통 helper에 포함하는 편이 좋겠습니다.

참고: PR branch tree만 보면 audit enum/filter가 빠진 것처럼 보일 수 있지만, origin/dev merge result 기준으로는 audit_log.pyvalues_callable=enum_values와 audit log server-side filter가 보존됩니다. 이 둘은 blocker로 보지 않았습니다.

검토 방식: gh pr view, git fetch, git merge-tree origin/dev origin/pr/65, merge result tree의 주요 파일 diff/line inspection. 전체 테스트는 실행하지 않았습니다.

@yoonki1207
yoonki1207 marked this pull request as ready for review June 27, 2026 11:22

@HyungminYoon1 HyungminYoon1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

재검토 결과, 이전에 요청한 blocking 항목은 해결된 것으로 확인했습니다.

  • LLM pricing 관리 endpoint에 인증과 system_admin check가 추가되었습니다.
  • /api/v1/llm/providers도 문서와 맞게 authenticated route가 되었습니다.
  • 관련 deny test와 deployment list 권한 보강 test가 추가되었습니다.
  • merge result 기준 audit enum mapping과 audit log server-side filter도 보존됩니다.

남은 사항은 TraceAccessService.is_system_admin() provider wiring이 기본 deny-all이라는 운영상 follow-up입니다. 보안상 열린 상태는 아니므로 이 PR의 merge blocker로 보지 않습니다.

@HyungminYoon1
HyungminYoon1 merged commit 521e229 into nodease:dev Jun 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BE][Infra][FIX] MVP1 RBAC/Organization Foundation 정렬

2 participants