mirror of
https://github.com/langgenius/dify.git
synced 2026-05-27 04:16:16 +08:00
Replace the single mutable-context Pipeline with a two-phase, condition-driven system dispatched by token type. New architecture: - TokenType(StrEnum) replaces source: str on AuthContext / TokenKind - AuthPipeline: pure prepare→auth step runner; no guard() - PipelineRoute: binds AuthPipeline to an optional required_edition gate - PipelineRouter: single guard() entry point; runs edition/license/token-type pre-gates then dispatches to the registered pipeline for the token type - Cond / When: composable predicates for conditional step dispatch - AuthData: frozen Pydantic model produced by the prepare phase; carries token_id so endpoints don't need to call get_auth_ctx() for identity fields - Edition enum + current_edition(): CE / EE / SAAS discriminator Two pipelines in composition.py: - account_pipeline — OAUTH_ACCOUNT tokens - external_sso_pipeline — OAUTH_EXTERNAL_SSO tokens (EE enforced at route level) All /openapi/v1 endpoints migrated to auth_router.guard(). Old context.py, steps.py, strategies.py, surface_gate.py deleted. WORKSPACE_READ scope added; cached_verdicts renamed to membership_cache.
69 lines
2.9 KiB
Python
69 lines
2.9 KiB
Python
"""User-scoped workspace reads under /openapi/v1/workspaces. Bearer-authed
|
|
counterparts to the cookie-authed /console/api/workspaces endpoints.
|
|
|
|
Account bearers (dfoa_) see every tenant they're a member of. External
|
|
SSO bearers (dfoe_) have no account_id and so see an empty list — that
|
|
matches /openapi/v1/account.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from itertools import starmap
|
|
|
|
from flask_restx import Resource
|
|
from werkzeug.exceptions import NotFound
|
|
|
|
from controllers.openapi import openapi_ns
|
|
from controllers.openapi._models import WorkspaceDetailResponse, WorkspaceListResponse, WorkspaceSummaryResponse
|
|
from controllers.openapi.auth.composition import auth_router
|
|
from controllers.openapi.auth.data import AuthData
|
|
from extensions.ext_database import db
|
|
from libs.oauth_bearer import Scope, TokenType
|
|
from models import Tenant, TenantAccountJoin
|
|
from services.account_service import TenantService
|
|
|
|
|
|
@openapi_ns.route("/workspaces")
|
|
class WorkspacesApi(Resource):
|
|
@openapi_ns.response(200, "Workspace list", openapi_ns.models[WorkspaceListResponse.__name__])
|
|
@auth_router.guard(scope=Scope.WORKSPACE_READ, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}))
|
|
def get(self, *, auth_data: AuthData):
|
|
rows = TenantService.get_workspaces_for_account(db.session, str(auth_data.account_id))
|
|
|
|
return WorkspaceListResponse(workspaces=list(starmap(_workspace_summary, rows))).model_dump(mode="json"), 200
|
|
|
|
|
|
@openapi_ns.route("/workspaces/<string:workspace_id>")
|
|
class WorkspaceByIdApi(Resource):
|
|
@openapi_ns.response(200, "Workspace detail", openapi_ns.models[WorkspaceDetailResponse.__name__])
|
|
@auth_router.guard(scope=Scope.WORKSPACE_READ, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT}))
|
|
def get(self, workspace_id: str, *, auth_data: AuthData):
|
|
row = TenantService.find_workspace_for_account(db.session, str(auth_data.account_id), workspace_id)
|
|
# 404 (not 403) on non-member so workspace IDs don't leak across tenants.
|
|
if row is None:
|
|
raise NotFound("workspace not found")
|
|
|
|
tenant, membership = row
|
|
return _workspace_detail(tenant, membership).model_dump(mode="json"), 200
|
|
|
|
|
|
def _workspace_summary(tenant: Tenant, membership: TenantAccountJoin) -> WorkspaceSummaryResponse:
|
|
return WorkspaceSummaryResponse(
|
|
id=str(tenant.id),
|
|
name=tenant.name,
|
|
role=getattr(membership, "role", ""),
|
|
status=tenant.status,
|
|
current=getattr(membership, "current", False),
|
|
)
|
|
|
|
|
|
def _workspace_detail(tenant: Tenant, membership: TenantAccountJoin) -> WorkspaceDetailResponse:
|
|
return WorkspaceDetailResponse(
|
|
id=str(tenant.id),
|
|
name=tenant.name,
|
|
role=getattr(membership, "role", ""),
|
|
status=tenant.status,
|
|
current=getattr(membership, "current", False),
|
|
created_at=tenant.created_at.isoformat() if tenant.created_at else None,
|
|
)
|