Files
dify/api/controllers/openapi/apps_permitted_external.py
L1nSn0w 6ca8b7bd6e refactor(openapi): adopt resource-oriented paths and colon custom methods
Align the /openapi/v1 surface with the API Design Guide: replace verb path
suffixes with standard resource GETs and AIP-136 `:action` custom methods.

  GET  /apps/{id}/describe                          -> GET  /apps/{id}
  GET  /permitted-external-apps/{id}/describe        -> GET  /permitted-external-apps/{id}
  POST /apps/{id}/run                                -> POST /apps/{id}:run
  POST /apps/{id}/tasks/{tid}/stop                   -> POST /apps/{id}/tasks/{tid}:stop
  POST /apps/{id}/files/upload                       -> POST /apps/{id}/files
  GET  /apps/{id}/export                             -> GET  /apps/{id}/dsl
  GET  /apps/{id}/check-dependencies                 -> GET  /apps/{id}/dependencies
  POST /workspaces/{ws}/apps/imports/{iid}/confirm   -> POST /workspaces/{ws}/apps/imports/{iid}:confirm
  POST /workspaces/{ws}/switch                       -> POST /workspaces/{ws}:switch
  GET|POST /apps/{id}/form/human_input/{tok}         -> /apps/{id}/human-input-forms/{tok} (+ :submit for POST)
  PUT  /workspaces/{ws}/members/{mid}/role           -> PATCH /workspaces/{ws}/members/{mid}

difyctl is the sole consumer and ships in lockstep, so this is a hard cutover
(no /v2, no dual-mount). RFC 8628 device-flow routes and the `limit` pagination
param are intentionally left unchanged.
2026-07-03 19:22:23 +08:00

105 lines
4.1 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_restx import Resource
from werkzeug.exceptions import NotFound
from controllers.openapi import openapi_ns
from controllers.openapi._contract import accepts, returns
from controllers.openapi._models import (
AppDescribeQuery,
AppDescribeResponse,
AppListRow,
PermittedExternalAppsListQuery,
PermittedExternalAppsListResponse,
)
from controllers.openapi.apps import build_app_describe_response
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 models.enums import AppStatus
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):
@auth_router.guard(
scope=Scope.APPS_READ_PERMITTED_EXTERNAL,
allowed_token_types=frozenset({TokenType.OAUTH_EXTERNAL_SSO}),
edition=frozenset({Edition.EE}),
)
@returns(200, PermittedExternalAppsListResponse, description="Permitted external apps list")
@accepts(query=PermittedExternalAppsListQuery)
def get(self, *, auth_data: AuthData, query: PermittedExternalAppsListQuery):
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
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 != AppStatus.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,
updated_at=app.updated_at.isoformat() if app.updated_at else None,
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
@openapi_ns.route("/permitted-external-apps/<string:app_id>")
class PermittedExternalAppDescribeApi(Resource):
@auth_router.guard(
scope=Scope.APPS_READ_PERMITTED_EXTERNAL,
allowed_token_types=frozenset({TokenType.OAUTH_EXTERNAL_SSO}),
edition=frozenset({Edition.EE}),
)
@returns(200, AppDescribeResponse, description="Permitted external app description")
@accepts(query=AppDescribeQuery)
def get(self, app_id: str, *, auth_data: AuthData, query: AppDescribeQuery):
# App already loaded and ACL-checked by the external_sso pipeline; project it.
app = auth_data.app
if app is None:
raise NotFound("app not found")
return build_app_describe_response(app, query.fields)