Files
dify/api/controllers/openapi/auth/composition.py
GareArc 8ce4477408 refactor(api): consolidate workspace role check into prepare/verify split
Fetch the caller's tenant role once in a new load_workspace_role prepare
step (stashed on AuthData.tenant_role) instead of querying TenantAccountJoin
twice — once for membership, once for role. check_workspace_role becomes a
pure assertion over the stashed role; non-members get 404 (no cross-tenant
ID leak), out-of-set roles 403.

Other cleanups:
- All prepare steps are now skip-if-already-loaded for safe re-entry.
- Move the app.enable_api policy check out of load_app into a dedicated
  check_app_api_enabled verify step (data-load vs policy-assert separation).
- Validate workspace_id is a UUID in load_tenant_from_request (malformed
  id -> 404, not 500).
- Collapse guard/guard_workspace duplication into a shared _make_decorator.
- Delete the now-unused role_gate.require_workspace_role decorator and its
  tests; enforcement lives entirely in the auth pipeline.
2026-06-02 01:57:18 -07:00

75 lines
2.5 KiB
Python

from __future__ import annotations
from controllers.openapi.auth.conditions import (
EDITION_EE,
HAS_ALLOWED_ROLES,
LOADED_APP_IS_PRIVATE,
PATH_HAS_APP_ID,
WEBAPP_AUTH_ENABLED,
WORKSPACE_MEMBERSHIP_REQUIRED,
)
from controllers.openapi.auth.data import Edition
from controllers.openapi.auth.flow import When
from controllers.openapi.auth.pipeline import AuthPipeline, PipelineRoute, PipelineRouter
from controllers.openapi.auth.prepare import (
load_account,
load_app,
load_app_access_mode,
load_tenant,
load_tenant_from_request,
load_workspace_role,
resolve_external_user,
)
from controllers.openapi.auth.verify import (
check_acl,
check_app_api_enabled,
check_membership,
check_private_app_permission,
check_scope,
check_workspace_mismatch,
check_workspace_role,
)
from libs.oauth_bearer import TokenType
account_pipeline = AuthPipeline(
prepare=[
When(PATH_HAS_APP_ID, then=load_app),
When(PATH_HAS_APP_ID, then=load_tenant),
When(WORKSPACE_MEMBERSHIP_REQUIRED & ~PATH_HAS_APP_ID, then=load_tenant_from_request),
load_account,
When(HAS_ALLOWED_ROLES, then=load_workspace_role),
When(PATH_HAS_APP_ID & EDITION_EE, then=load_app_access_mode),
],
auth=[
When(PATH_HAS_APP_ID, then=check_app_api_enabled),
check_scope,
When((PATH_HAS_APP_ID | WORKSPACE_MEMBERSHIP_REQUIRED) & ~HAS_ALLOWED_ROLES, then=check_membership),
When(WORKSPACE_MEMBERSHIP_REQUIRED & PATH_HAS_APP_ID, then=check_workspace_mismatch),
When(HAS_ALLOWED_ROLES, then=check_workspace_role),
When(PATH_HAS_APP_ID & EDITION_EE & WEBAPP_AUTH_ENABLED, then=check_acl),
When(EDITION_EE & LOADED_APP_IS_PRIVATE, then=check_private_app_permission),
],
)
external_sso_pipeline = AuthPipeline(
prepare=[
When(PATH_HAS_APP_ID, then=load_app),
When(PATH_HAS_APP_ID, then=load_tenant),
When(PATH_HAS_APP_ID, then=resolve_external_user),
When(PATH_HAS_APP_ID, then=load_app_access_mode),
],
auth=[
When(PATH_HAS_APP_ID, then=check_app_api_enabled),
check_scope,
When(PATH_HAS_APP_ID & WEBAPP_AUTH_ENABLED, then=check_acl),
When(LOADED_APP_IS_PRIVATE, then=check_private_app_permission),
],
)
auth_router = PipelineRouter(
{
TokenType.OAUTH_ACCOUNT: PipelineRoute(account_pipeline),
TokenType.OAUTH_EXTERNAL_SSO: PipelineRoute(external_sso_pipeline, required_edition=frozenset({Edition.EE})),
}
)