mirror of
https://github.com/langgenius/dify.git
synced 2026-05-27 20:36:18 +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.
93 lines
3.6 KiB
Python
93 lines
3.6 KiB
Python
"""GET /openapi/v1/permitted-external-apps — external-subject app discovery (EE only).
|
|
|
|
`dfoe_` (External SSO) callers reach apps gated by ACL access-mode
|
|
(public / sso_verified). License-gated: CE deploys never enable the
|
|
EE blueprint chain so this module is unreachable there.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from flask import request
|
|
from flask_restx import Resource
|
|
from pydantic import ValidationError
|
|
from werkzeug.exceptions import UnprocessableEntity
|
|
|
|
from controllers.openapi import openapi_ns
|
|
from controllers.openapi._models import (
|
|
AppListRow,
|
|
PermittedExternalAppsListQuery,
|
|
PermittedExternalAppsListResponse,
|
|
)
|
|
from controllers.openapi.auth.composition import auth_router
|
|
from controllers.openapi.auth.data import AuthData, Edition
|
|
from extensions.ext_database import db
|
|
from libs.oauth_bearer import Scope, TokenType
|
|
from models import App
|
|
from services.account_service import TenantService
|
|
from services.app_service import AppService
|
|
from services.enterprise.app_permitted_service import list_permitted_apps
|
|
|
|
|
|
@openapi_ns.route("/permitted-external-apps")
|
|
class PermittedExternalAppsListApi(Resource):
|
|
@openapi_ns.response(
|
|
200, "Permitted external apps list", openapi_ns.models[PermittedExternalAppsListResponse.__name__]
|
|
)
|
|
@auth_router.guard(
|
|
scope=Scope.APPS_READ_PERMITTED_EXTERNAL,
|
|
allowed_token_types=frozenset({TokenType.OAUTH_EXTERNAL_SSO}),
|
|
edition=frozenset({Edition.EE}),
|
|
)
|
|
def get(self, *, auth_data: AuthData):
|
|
try:
|
|
query = PermittedExternalAppsListQuery.model_validate(request.args.to_dict(flat=True))
|
|
except ValidationError as exc:
|
|
raise UnprocessableEntity(exc.json())
|
|
|
|
page_result = list_permitted_apps(
|
|
page=query.page,
|
|
limit=query.limit,
|
|
mode=query.mode.value if query.mode else None,
|
|
name=query.name,
|
|
)
|
|
|
|
if not page_result.app_ids:
|
|
env = PermittedExternalAppsListResponse(
|
|
page=query.page, limit=query.limit, total=page_result.total, has_more=False, data=[]
|
|
)
|
|
return env.model_dump(mode="json"), 200
|
|
|
|
apps_by_id: dict[str, App] = {
|
|
str(a.id): a for a in AppService.find_visible_apps_by_ids(db.session, page_result.app_ids)
|
|
}
|
|
tenant_ids = list({str(a.tenant_id) for a in apps_by_id.values()})
|
|
tenants_by_id = {str(t.id): t for t in TenantService.get_tenants_by_ids(db.session, tenant_ids)}
|
|
|
|
items: list[AppListRow] = []
|
|
for app_id in page_result.app_ids:
|
|
app = apps_by_id.get(app_id)
|
|
if not app or app.status != "normal":
|
|
continue
|
|
tenant = tenants_by_id.get(str(app.tenant_id))
|
|
items.append(
|
|
AppListRow(
|
|
id=str(app.id),
|
|
name=app.name,
|
|
description=app.description,
|
|
mode=app.mode,
|
|
tags=[], # tenant-scoped; not surfaced cross-tenant
|
|
updated_at=app.updated_at.isoformat() if app.updated_at else None,
|
|
created_by_name=None, # cross-tenant author leak prevention
|
|
workspace_id=str(app.tenant_id),
|
|
workspace_name=tenant.name if tenant else None,
|
|
)
|
|
)
|
|
env = PermittedExternalAppsListResponse(
|
|
page=query.page,
|
|
limit=query.limit,
|
|
total=page_result.total,
|
|
has_more=query.page * query.limit < page_result.total,
|
|
data=items,
|
|
)
|
|
return env.model_dump(mode="json"), 200
|