Files
非法操作andautofix-ci[bot] ce26b22970 refactor(api): isolate trigger execution lifecycle (#45950)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-11-10 08:17:39 +00:00

732 lines
31 KiB
Python

import logging
from http import HTTPStatus
from typing import Any, NoReturn, Self
from flask import make_response, redirect, request
from flask_restx import Resource
from pydantic import BaseModel, RootModel, model_validator
from werkzeug import Response
from configs import dify_config
from controllers.common.errors import ForbiddenError, NotFoundError
from controllers.common.fields import SimpleResultResponse
from controllers.common.rbac import RBACCheck, Workspace
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.flask_admission import console_account_admission
from controllers.console.workspace.error import (
TriggerOAuthClientChangedError,
TriggerOAuthClientConfigInvalidError,
TriggerOAuthContextInvalidError,
TriggerOAuthProviderRequestError,
TriggerSubscriptionChangedError,
TriggerSubscriptionInvalidError,
TriggerSubscriptionLimitError,
TriggerSubscriptionNameConflictError,
TriggerSubscriptionNotFoundError,
TriggerSubscriptionProviderError,
)
from controllers.console.wraps import (
RBACPermission,
model_validate,
setup_required,
)
from core.entities.provider_entities import ProviderConfig
from core.plugin.entities.plugin_daemon import CredentialType
from core.trigger.entities.api_entities import (
SubscriptionBuilderApiEntity,
TriggerProviderApiEntity,
TriggerProviderSubscriptionApiEntity,
)
from core.trigger.entities.entities import RequestLog, SubscriptionBuilderUpdater
from enums.account import TenantAccountRole
from extensions.ext_application_services import application_services
from fields.base import ResponseModel
from libs.external_api import error_responses
from libs.helper import dump_response
from machinery.context import RequestContext
from models.provider_ids import TriggerProviderID
from services.trigger.oauth_client_service import TriggerOAuthClientChangedError as TriggerOAuthClientChanged
from services.trigger.oauth_client_service import TriggerOAuthClientConfigError
from services.trigger.oauth_service import (
TriggerOAuthCallbackRequest,
TriggerOAuthClientNotConfiguredError,
TriggerOAuthContextError,
TriggerOAuthProviderError,
)
from services.trigger.subscription_service import TriggerSubscriptionChangedError as SubscriptionChanged
from services.trigger.subscription_service import TriggerSubscriptionError
from services.trigger.subscription_service import TriggerSubscriptionInvalidError as SubscriptionInvalid
from services.trigger.subscription_service import TriggerSubscriptionLimitError as SubscriptionLimit
from services.trigger.subscription_service import TriggerSubscriptionNameConflictError as SubscriptionNameConflict
from services.trigger.subscription_service import TriggerSubscriptionNotFoundError as SubscriptionNotFound
from services.trigger.subscription_service import TriggerSubscriptionProviderError as SubscriptionProviderError
logger = logging.getLogger(__name__)
def _raise_subscription_error(error: TriggerSubscriptionError) -> NoReturn:
if isinstance(error, SubscriptionNotFound):
raise TriggerSubscriptionNotFoundError(str(error)) from error
if isinstance(error, SubscriptionNameConflict):
raise TriggerSubscriptionNameConflictError(str(error)) from error
if isinstance(error, SubscriptionChanged):
raise TriggerSubscriptionChangedError(str(error)) from error
if isinstance(error, SubscriptionLimit):
raise TriggerSubscriptionLimitError(str(error)) from error
if isinstance(error, SubscriptionInvalid):
raise TriggerSubscriptionInvalidError(str(error)) from error
if isinstance(error, SubscriptionProviderError):
raise TriggerSubscriptionProviderError(str(error)) from error
raise error
class TriggerSubscriptionBuilderCreatePayload(BaseModel):
credential_type: str = CredentialType.UNAUTHORIZED
class TriggerSubscriptionBuilderVerifyPayload(BaseModel):
credentials: dict[str, Any] | None = None
class TriggerSubscriptionVerifyPayload(BaseModel):
credentials: dict[str, Any]
class TriggerSubscriptionBuilderUpdatePayload(BaseModel):
name: str | None = None
parameters: dict[str, Any] | None = None
properties: dict[str, Any] | None = None
credentials: dict[str, Any] | None = None
@model_validator(mode="after")
def check_at_least_one_field(self) -> Self:
if all(v is None for v in self.model_dump().values()):
raise ValueError("At least one of name, credentials, parameters, or properties must be provided")
return self
class TriggerOAuthClientPayload(BaseModel):
client_params: dict[str, object] | None = None
enabled: bool | None = None
class TriggerProviderListResponse(RootModel[list[TriggerProviderApiEntity]]):
pass
class TriggerProviderSubscriptionListResponse(RootModel[list[TriggerProviderSubscriptionApiEntity]]):
pass
class TriggerSubscriptionBuilderCreateResponse(ResponseModel):
subscription_builder: SubscriptionBuilderApiEntity
class TriggerVerificationResponse(ResponseModel):
verified: bool
class TriggerSubscriptionBuilderLogsResponse(ResponseModel):
logs: list[RequestLog]
class TriggerOAuthAuthorizeResponse(ResponseModel):
authorization_url: str
subscription_builder_id: str
subscription_builder: SubscriptionBuilderApiEntity
class TriggerOAuthClientResponse(ResponseModel):
configured: bool
system_configured: bool
custom_configured: bool
oauth_client_schema: list[ProviderConfig]
custom_enabled: bool
redirect_uri: str
params: dict[str, object]
class TriggerProviderErrorResponse(ResponseModel):
error: str
register_schema_models(
console_ns,
TriggerSubscriptionBuilderCreatePayload,
TriggerSubscriptionBuilderVerifyPayload,
TriggerSubscriptionBuilderUpdatePayload,
TriggerSubscriptionVerifyPayload,
TriggerOAuthClientPayload,
)
register_response_schema_models(
console_ns,
SimpleResultResponse,
TriggerOAuthAuthorizeResponse,
TriggerOAuthClientResponse,
TriggerProviderApiEntity,
TriggerProviderErrorResponse,
TriggerProviderListResponse,
TriggerProviderSubscriptionListResponse,
TriggerSubscriptionBuilderCreateResponse,
TriggerSubscriptionBuilderLogsResponse,
SubscriptionBuilderApiEntity,
TriggerVerificationResponse,
)
@console_ns.route("/workspaces/current/trigger-provider/<path:provider>/icon")
class TriggerProviderIconApi(Resource):
# response-contract:ignore legacy JSON string containing the icon URL
@console_ns.response(HTTPStatus.OK, "Trigger provider icon")
@console_account_admission()
def get(self, context: RequestContext, provider: str) -> str:
return application_services().triggers.providers.get_icon(
tenant_id=context.active_workspace_id, provider_id=TriggerProviderID(provider)
)
@console_ns.route("/workspaces/current/triggers")
class TriggerProviderListApi(Resource):
@console_ns.response(
HTTPStatus.OK,
"Trigger providers retrieved successfully",
console_ns.models[TriggerProviderListResponse.__name__],
)
@console_account_admission()
def get(self, context: RequestContext) -> list[dict[str, Any]]:
"""List all trigger providers for the current tenant"""
return TriggerProviderListResponse(
application_services().triggers.providers.list(tenant_id=context.active_workspace_id),
).model_dump(mode="json")
@console_ns.route("/workspaces/current/trigger-provider/<path:provider>/info")
class TriggerProviderInfoApi(Resource):
@console_ns.response(
HTTPStatus.OK,
"Trigger provider retrieved successfully",
console_ns.models[TriggerProviderApiEntity.__name__],
)
@console_account_admission()
def get(self, context: RequestContext, provider: str) -> dict[str, Any]:
"""Get info for a trigger provider"""
return dump_response(
TriggerProviderApiEntity,
application_services().triggers.providers.get(
tenant_id=context.active_workspace_id, provider_id=TriggerProviderID(provider)
),
)
@console_ns.route("/workspaces/current/trigger-provider/<path:provider>/subscriptions/list")
class TriggerSubscriptionListApi(Resource):
@console_ns.response(
HTTPStatus.OK,
"Trigger subscriptions retrieved successfully",
console_ns.models[TriggerProviderSubscriptionListResponse.__name__],
)
@console_ns.response(
HTTPStatus.NOT_FOUND, "Trigger provider not found", console_ns.models[TriggerProviderErrorResponse.__name__]
)
@console_account_admission(
allowed_roles=frozenset({TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR}),
rbac_checks=[RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())],
)
def get(self, context: RequestContext, provider: str) -> list[dict[str, Any]] | tuple[dict[str, Any], HTTPStatus]:
"""List all trigger subscriptions for the current tenant's provider"""
try:
return TriggerProviderSubscriptionListResponse(
application_services().triggers.subscriptions.list(
tenant_id=context.active_workspace_id,
account_id=context.account_id,
provider_id=TriggerProviderID(provider),
),
).model_dump(mode="json")
except ValueError as e:
return TriggerProviderErrorResponse(error=str(e)).model_dump(mode="json"), HTTPStatus.NOT_FOUND
except Exception as e:
logger.exception("Error listing trigger providers", exc_info=e)
raise
@console_ns.route(
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/create",
)
class TriggerSubscriptionBuilderCreateApi(Resource):
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderCreatePayload.__name__])
@console_ns.response(
HTTPStatus.OK,
"Trigger subscription builder created successfully",
console_ns.models[TriggerSubscriptionBuilderCreateResponse.__name__],
)
@console_account_admission(
allowed_roles=frozenset({TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR}),
rbac_checks=[RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())],
)
@model_validate(TriggerSubscriptionBuilderCreatePayload)
def post(
self, req_data: TriggerSubscriptionBuilderCreatePayload, context: RequestContext, provider: str
) -> dict[str, Any]:
"""Add a new subscription instance for a trigger provider"""
try:
credential_type = CredentialType.of(req_data.credential_type)
subscription_builder = application_services().triggers.builders.create_trigger_subscription_builder(
tenant_id=context.active_workspace_id,
user_id=context.account_id,
provider_id=TriggerProviderID(provider),
credential_type=credential_type,
)
return TriggerSubscriptionBuilderCreateResponse(subscription_builder=subscription_builder).model_dump(
mode="json"
)
except Exception as e:
logger.exception("Error adding provider credential", exc_info=e)
raise
@console_ns.route(
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/<path:subscription_builder_id>",
)
class TriggerSubscriptionBuilderGetApi(Resource):
@console_ns.response(
HTTPStatus.OK,
"Trigger subscription builder retrieved successfully",
console_ns.models[SubscriptionBuilderApiEntity.__name__],
)
@console_account_admission(
allowed_roles=frozenset({TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR}),
rbac_checks=[RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())],
)
def get(self, context: RequestContext, provider: str, subscription_builder_id: str) -> dict[str, Any]:
"""Get a subscription instance for a trigger provider"""
subscription_builder = application_services().triggers.builders.get_subscription_builder_by_id(
tenant_id=context.active_workspace_id,
user_id=context.account_id,
provider_id=TriggerProviderID(provider),
subscription_builder_id=subscription_builder_id,
)
return subscription_builder.model_dump(mode="json")
@console_ns.route(
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/verify-and-update/<path:subscription_builder_id>",
)
class TriggerSubscriptionBuilderVerifyApi(Resource):
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderVerifyPayload.__name__])
@console_ns.response(
HTTPStatus.OK,
"Trigger subscription builder verified successfully",
console_ns.models[TriggerVerificationResponse.__name__],
)
@console_account_admission(
allowed_roles=frozenset({TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR}),
rbac_checks=[RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())],
)
@model_validate(TriggerSubscriptionBuilderVerifyPayload)
def post(
self,
req_data: TriggerSubscriptionBuilderVerifyPayload,
context: RequestContext,
provider: str,
subscription_builder_id: str,
) -> dict[str, Any]:
"""Verify and update a subscription instance for a trigger provider"""
try:
# Use atomic update_and_verify to prevent race conditions
result = application_services().triggers.builders.update_and_verify_builder(
tenant_id=context.active_workspace_id,
user_id=context.account_id,
provider_id=TriggerProviderID(provider),
subscription_builder_id=subscription_builder_id,
subscription_builder_updater=SubscriptionBuilderUpdater(
credentials=req_data.credentials,
),
)
return dump_response(TriggerVerificationResponse, result)
except Exception as e:
logger.exception("Error verifying provider credential", exc_info=e)
raise ValueError(str(e)) from e
@console_ns.route(
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/update/<path:subscription_builder_id>",
)
class TriggerSubscriptionBuilderUpdateApi(Resource):
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderUpdatePayload.__name__])
@console_ns.response(
HTTPStatus.OK,
"Trigger subscription builder updated successfully",
console_ns.models[SubscriptionBuilderApiEntity.__name__],
)
@console_account_admission(
allowed_roles=frozenset({TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR}),
rbac_checks=[RBACCheck(RBACPermission.CREDENTIAL_MANAGE, Workspace())],
)
@model_validate(TriggerSubscriptionBuilderUpdatePayload)
def post(
self,
req_data: TriggerSubscriptionBuilderUpdatePayload,
context: RequestContext,
provider: str,
subscription_builder_id: str,
) -> dict[str, Any]:
"""Update a subscription instance for a trigger provider"""
try:
return (
application_services()
.triggers.builders.update_trigger_subscription_builder(
tenant_id=context.active_workspace_id,
user_id=context.account_id,
provider_id=TriggerProviderID(provider),
subscription_builder_id=subscription_builder_id,
subscription_builder_updater=SubscriptionBuilderUpdater(
name=req_data.name,
parameters=req_data.parameters,
properties=req_data.properties,
credentials=req_data.credentials,
),
)
.model_dump(mode="json")
)
except Exception as e:
logger.exception("Error updating provider credential", exc_info=e)
raise
@console_ns.route(
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/logs/<path:subscription_builder_id>",
)
class TriggerSubscriptionBuilderLogsApi(Resource):
@console_ns.response(
HTTPStatus.OK,
"Trigger subscription builder logs retrieved successfully",
console_ns.models[TriggerSubscriptionBuilderLogsResponse.__name__],
)
@console_account_admission(
allowed_roles=frozenset({TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR}),
rbac_checks=[RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())],
)
def get(self, context: RequestContext, provider: str, subscription_builder_id: str) -> dict[str, Any]:
"""Get the request logs for a subscription instance for a trigger provider"""
try:
logs = application_services().triggers.builders.list_logs(
tenant_id=context.active_workspace_id,
user_id=context.account_id,
provider_id=TriggerProviderID(provider),
subscription_builder_id=subscription_builder_id,
)
return dump_response(TriggerSubscriptionBuilderLogsResponse, {"logs": logs})
except Exception as e:
logger.exception("Error getting request logs for subscription builder", exc_info=e)
raise
@console_ns.route(
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/build/<path:subscription_builder_id>",
)
class TriggerSubscriptionBuilderBuildApi(Resource):
@error_responses(
TriggerSubscriptionNameConflictError, TriggerSubscriptionLimitError, TriggerSubscriptionProviderError
)
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderUpdatePayload.__name__])
@console_ns.response(
HTTPStatus.OK,
"Trigger subscription builder built successfully",
console_ns.models[SimpleResultResponse.__name__],
)
@console_account_admission(
allowed_roles=frozenset({TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR}),
rbac_checks=[RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())],
)
@model_validate(TriggerSubscriptionBuilderUpdatePayload)
def post(
self,
req_data: TriggerSubscriptionBuilderUpdatePayload,
context: RequestContext,
provider: str,
subscription_builder_id: str,
) -> dict[str, Any]:
"""Build a subscription instance for a trigger provider"""
try:
# Use atomic update_and_build to prevent race conditions
application_services().triggers.builders.update_and_build_builder(
tenant_id=context.active_workspace_id,
user_id=context.account_id,
provider_id=TriggerProviderID(provider),
subscription_builder_id=subscription_builder_id,
subscription_builder_updater=SubscriptionBuilderUpdater(
name=req_data.name,
parameters=req_data.parameters,
properties=req_data.properties,
),
)
return SimpleResultResponse(result="success").model_dump(mode="json")
except TriggerSubscriptionError as error:
_raise_subscription_error(error)
except Exception as e:
logger.exception("Error building provider credential", exc_info=e)
raise ValueError(str(e)) from e
@console_ns.route(
"/workspaces/current/trigger-provider/<path:subscription_id>/subscriptions/update",
)
class TriggerSubscriptionUpdateApi(Resource):
@error_responses(
TriggerSubscriptionNotFoundError,
TriggerSubscriptionNameConflictError,
TriggerSubscriptionChangedError,
TriggerSubscriptionProviderError,
)
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderUpdatePayload.__name__])
@console_ns.response(
HTTPStatus.OK, "Trigger subscription updated successfully", console_ns.models[SimpleResultResponse.__name__]
)
@console_account_admission(
allowed_roles=frozenset({TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR}),
rbac_checks=[RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())],
)
@model_validate(TriggerSubscriptionBuilderUpdatePayload)
def post(
self, req_data: TriggerSubscriptionBuilderUpdatePayload, context: RequestContext, subscription_id: str
) -> dict[str, Any]:
"""Rename a subscription or rebuild its provider configuration."""
try:
application_services().triggers.subscription_management.update(
tenant_id=context.active_workspace_id,
subscription_id=subscription_id,
name=req_data.name,
properties=req_data.properties,
parameters=req_data.parameters,
credentials=req_data.credentials,
)
except TriggerSubscriptionError as error:
_raise_subscription_error(error)
return SimpleResultResponse(result="success").model_dump(mode="json")
@console_ns.route(
"/workspaces/current/trigger-provider/<path:subscription_id>/subscriptions/delete",
)
class TriggerSubscriptionDeleteApi(Resource):
@error_responses(
TriggerSubscriptionNotFoundError, TriggerSubscriptionChangedError, TriggerSubscriptionProviderError
)
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SimpleResultResponse.__name__])
@console_account_admission(
allowed_roles=frozenset({TenantAccountRole.OWNER, TenantAccountRole.ADMIN}),
rbac_checks=[RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())],
)
def post(self, context: RequestContext, subscription_id: str) -> dict[str, Any]:
"""Delete the subscription and its workflow trigger bindings."""
try:
application_services().triggers.subscription_management.delete(
tenant_id=context.active_workspace_id,
subscription_id=subscription_id,
)
except TriggerSubscriptionError as error:
_raise_subscription_error(error)
return SimpleResultResponse(result="success").model_dump(mode="json")
@console_ns.route("/workspaces/current/trigger-provider/<path:provider>/subscriptions/oauth/authorize")
class TriggerOAuthAuthorizeApi(Resource):
# response-contract:ignore cookie-bearing Flask response
@error_responses(NotFoundError, TriggerOAuthClientConfigInvalidError, TriggerOAuthProviderRequestError)
@console_ns.response(
HTTPStatus.OK,
"Trigger OAuth authorization URL generated successfully",
console_ns.models[TriggerOAuthAuthorizeResponse.__name__],
)
@console_account_admission()
def get(self, context: RequestContext, provider: str) -> Response:
"""Initiate OAuth authorization flow for a trigger provider."""
try:
result = application_services().triggers.oauth.authorize(
tenant_id=context.active_workspace_id,
user_id=context.account_id,
provider_id=TriggerProviderID(provider),
redirect_uri=f"{dify_config.CONSOLE_API_URL}/console/api/oauth/plugin/{provider}/trigger/callback",
)
except TriggerOAuthClientNotConfiguredError as error:
raise NotFoundError(str(error)) from error
except TriggerOAuthClientConfigError as error:
raise TriggerOAuthClientConfigInvalidError(str(error)) from error
except TriggerOAuthProviderError as error:
raise TriggerOAuthProviderRequestError(str(error)) from error
response = make_response(
dump_response(
TriggerOAuthAuthorizeResponse,
{
"authorization_url": result.authorization_url,
"subscription_builder_id": result.subscription_builder.id,
"subscription_builder": result.subscription_builder,
},
)
)
response.set_cookie(
"context_id",
result.context_id,
httponly=True,
samesite="Lax",
max_age=result.context_max_age,
)
return response
@console_ns.route("/oauth/plugin/<path:provider>/trigger/callback")
class TriggerOAuthCallbackApi(Resource):
# response-contract:ignore redirect response
@error_responses(
ForbiddenError,
TriggerOAuthContextInvalidError,
TriggerOAuthClientConfigInvalidError,
TriggerOAuthProviderRequestError,
)
@console_ns.response(HTTPStatus.FOUND, "Redirect to OAuth callback page")
@setup_required
def get(self, provider: str) -> Response:
"""Complete the cookie-bound flow without requiring a Console login."""
try:
application_services().triggers.oauth.callback(
provider=provider,
context_id=request.cookies.get("context_id"),
redirect_uri=f"{dify_config.CONSOLE_API_URL}/console/api/oauth/plugin/{provider}/trigger/callback",
callback_loader=lambda: TriggerOAuthCallbackRequest(
method=request.method,
full_path=request.full_path,
headers=tuple(request.headers.items()),
body=request.get_data(as_text=False),
),
)
except TriggerOAuthContextError as error:
raise TriggerOAuthContextInvalidError(str(error)) from error
except TriggerOAuthClientNotConfiguredError as error:
raise ForbiddenError(str(error)) from error
except TriggerOAuthClientConfigError as error:
raise TriggerOAuthClientConfigInvalidError(str(error)) from error
except TriggerOAuthProviderError as error:
raise TriggerOAuthProviderRequestError(str(error)) from error
return redirect(f"{dify_config.CONSOLE_WEB_URL}/oauth-callback", code=HTTPStatus.FOUND)
@console_ns.route("/workspaces/current/trigger-provider/<path:provider>/oauth/client")
class TriggerOAuthClientManageApi(Resource):
@error_responses(TriggerOAuthClientConfigInvalidError)
@console_ns.response(
HTTPStatus.OK,
"Trigger OAuth client retrieved successfully",
console_ns.models[TriggerOAuthClientResponse.__name__],
)
@console_account_admission(
allowed_roles=frozenset({TenantAccountRole.OWNER, TenantAccountRole.ADMIN}),
rbac_checks=[RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())],
)
def get(self, context: RequestContext, provider: str) -> dict[str, object]:
"""Get the masked OAuth client configuration for a provider."""
try:
configuration = application_services().triggers.oauth_clients.get(
tenant_id=context.active_workspace_id,
provider_id=TriggerProviderID(provider),
)
except TriggerOAuthClientConfigError as error:
raise TriggerOAuthClientConfigInvalidError(str(error)) from error
return dump_response(
TriggerOAuthClientResponse,
{
"configured": bool(configuration.params or configuration.system_configured),
"system_configured": configuration.system_configured,
"custom_configured": bool(configuration.params),
"oauth_client_schema": configuration.schema,
"custom_enabled": configuration.custom_enabled,
"redirect_uri": f"{dify_config.CONSOLE_API_URL}/console/api/oauth/plugin/{provider}/trigger/callback",
"params": dict(configuration.params),
},
)
@error_responses(TriggerOAuthClientConfigInvalidError, TriggerOAuthClientChangedError)
@console_ns.expect(console_ns.models[TriggerOAuthClientPayload.__name__])
@console_ns.response(
HTTPStatus.OK, "Trigger OAuth client saved successfully", console_ns.models[SimpleResultResponse.__name__]
)
@console_account_admission(
allowed_roles=frozenset({TenantAccountRole.OWNER, TenantAccountRole.ADMIN}),
rbac_checks=[RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())],
)
@model_validate(TriggerOAuthClientPayload)
def post(self, req_data: TriggerOAuthClientPayload, context: RequestContext, provider: str) -> dict[str, object]:
"""Configure the workspace's custom OAuth client for a provider."""
try:
application_services().triggers.oauth_clients.save(
tenant_id=context.active_workspace_id,
provider_id=TriggerProviderID(provider),
client_params=req_data.client_params,
enabled=req_data.enabled,
)
except TriggerOAuthClientChanged as error:
raise TriggerOAuthClientChangedError(str(error)) from error
except TriggerOAuthClientConfigError as error:
raise TriggerOAuthClientConfigInvalidError(str(error)) from error
return dump_response(SimpleResultResponse, {"result": "success"})
@console_ns.response(
HTTPStatus.OK, "Trigger OAuth client deleted successfully", console_ns.models[SimpleResultResponse.__name__]
)
@console_account_admission(
allowed_roles=frozenset({TenantAccountRole.OWNER, TenantAccountRole.ADMIN}),
rbac_checks=[RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())],
)
def delete(self, context: RequestContext, provider: str) -> dict[str, object]:
"""Remove the workspace's custom OAuth client configuration."""
application_services().triggers.oauth_clients.delete(
tenant_id=context.active_workspace_id,
provider_id=TriggerProviderID(provider),
)
return dump_response(SimpleResultResponse, {"result": "success"})
@console_ns.route(
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/verify/<path:subscription_id>",
)
class TriggerSubscriptionVerifyApi(Resource):
@error_responses(
TriggerSubscriptionNotFoundError, TriggerSubscriptionInvalidError, TriggerSubscriptionProviderError
)
@console_ns.expect(console_ns.models[TriggerSubscriptionVerifyPayload.__name__])
@console_ns.response(
HTTPStatus.OK,
"Trigger subscription verified successfully",
console_ns.models[TriggerVerificationResponse.__name__],
)
@console_account_admission(
allowed_roles=frozenset({TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR}),
rbac_checks=[RBACCheck(RBACPermission.PLUGIN_PREFERENCES, Workspace())],
)
@model_validate(TriggerSubscriptionVerifyPayload)
def post(
self,
req_data: TriggerSubscriptionVerifyPayload,
context: RequestContext,
provider: str,
subscription_id: str,
) -> dict[str, Any]:
"""Verify credentials against the subscription's own provider without updating it."""
try:
result = application_services().triggers.subscription_management.verify(
tenant_id=context.active_workspace_id,
user_id=context.account_id,
provider_id=TriggerProviderID(provider),
subscription_id=subscription_id,
credentials=req_data.credentials,
)
except TriggerSubscriptionError as error:
_raise_subscription_error(error)
return dump_response(TriggerVerificationResponse, result)