mirror of
https://github.com/langgenius/dify.git
synced 2026-11-12 14:21:29 +08:00
143 lines
5.6 KiB
Python
143 lines
5.6 KiB
Python
"""Detailed credential checks and OAuth revocation.
|
|
|
|
Callers resolve and authorize credentials before this boundary. These calls
|
|
never refresh tokens, persist Connection state, or retry provider operations
|
|
automatically.
|
|
|
|
Legacy true results preserve the existing validator's success semantics. Legacy
|
|
false results cannot distinguish invalid credentials from other failures, so
|
|
they do not invalidate credentials. Transport and protocol errors propagate as
|
|
plugin daemon exceptions. Connection lifecycle services own best-effort cleanup.
|
|
"""
|
|
|
|
import time
|
|
from collections.abc import Mapping
|
|
from contextlib import closing
|
|
from typing import Any
|
|
|
|
from pydantic import ValidationError
|
|
|
|
from clients.plugin.auth_entities import (
|
|
CredentialCheckResponse,
|
|
CredentialCheckResult,
|
|
OAuthRevokeResult,
|
|
)
|
|
from clients.plugin.identifiers import plugin_id_from_unique_identifier
|
|
from core.plugin.entities.plugin_daemon import CredentialType
|
|
from core.plugin.impl.base import BasePluginClient, use_plugin_daemon_request_timeout
|
|
from core.plugin.impl.exc import PluginDaemonInternalServerError
|
|
from models.provider_ids import ToolProviderID
|
|
from services.plugin.dispatch_identity import plugin_dispatch_headers
|
|
|
|
|
|
class PluginCredentialAuthClient(BasePluginClient):
|
|
def check_provider_credentials(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
user_id: str,
|
|
provider: str,
|
|
credentials: Mapping[str, Any],
|
|
credential_type: CredentialType,
|
|
plugin_unique_identifier: str | None = None,
|
|
) -> CredentialCheckResult:
|
|
"""Accept legacy success without interpreting legacy failure as invalid."""
|
|
match credential_type:
|
|
case CredentialType.API_KEY:
|
|
wire_credential_type = "api_key"
|
|
case CredentialType.OAUTH2:
|
|
wire_credential_type = "oauth2"
|
|
case _:
|
|
raise ValueError("Detailed checks require API key or OAuth2 credentials")
|
|
|
|
provider_id = ToolProviderID(provider)
|
|
result = self._dispatch(
|
|
tenant_id=tenant_id,
|
|
user_id=user_id,
|
|
provider_id=provider_id,
|
|
operation="tool/validate_credentials",
|
|
plugin_unique_identifier=plugin_unique_identifier,
|
|
data={
|
|
"credentials": dict(credentials),
|
|
"credential_type": wire_credential_type,
|
|
"result_format": "detailed",
|
|
},
|
|
response_type=CredentialCheckResponse,
|
|
)
|
|
return CredentialCheckResult(
|
|
status=result.status,
|
|
reason_code=result.reason_code,
|
|
retryable=result.retryable,
|
|
checked_at=int(time.time()),
|
|
)
|
|
|
|
def revoke_credentials(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
user_id: str,
|
|
provider: str,
|
|
plugin_unique_identifier: str,
|
|
system_credentials: Mapping[str, Any],
|
|
credentials: Mapping[str, Any],
|
|
) -> OAuthRevokeResult:
|
|
"""Return the provider result; callers handle best-effort cleanup on errors.
|
|
|
|
The caller supplies the OAuth client configuration used for the original
|
|
grant. App unbinding must not call this operation.
|
|
"""
|
|
provider_id = ToolProviderID(provider)
|
|
if plugin_id_from_unique_identifier(plugin_unique_identifier) != provider_id.plugin_id:
|
|
raise ValueError("OAuth revocation artifact does not match the Provider.")
|
|
return self._dispatch(
|
|
tenant_id=tenant_id,
|
|
user_id=user_id,
|
|
provider_id=provider_id,
|
|
operation="oauth/revoke_credentials",
|
|
plugin_unique_identifier=plugin_unique_identifier,
|
|
data={"system_credentials": dict(system_credentials), "credentials": dict(credentials)},
|
|
response_type=OAuthRevokeResult,
|
|
)
|
|
|
|
def _dispatch[T: CredentialCheckResponse | OAuthRevokeResult](
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
user_id: str,
|
|
provider_id: ToolProviderID,
|
|
operation: str,
|
|
plugin_unique_identifier: str | None,
|
|
data: dict[str, Any],
|
|
response_type: type[T],
|
|
) -> T:
|
|
path = f"plugin/{tenant_id}/dispatch/{operation}"
|
|
headers = plugin_dispatch_headers(provider_id.plugin_id, plugin_unique_identifier)
|
|
payload = {"user_id": user_id, "data": {"provider": provider_id.provider_name, **data}}
|
|
if plugin_unique_identifier is not None:
|
|
if plugin_id_from_unique_identifier(plugin_unique_identifier) != provider_id.plugin_id:
|
|
raise ValueError("Authentication artifact does not match the Provider.")
|
|
with (
|
|
use_plugin_daemon_request_timeout(10),
|
|
closing(
|
|
self._request_with_plugin_daemon_response_stream(
|
|
"POST",
|
|
path,
|
|
dict[str, object],
|
|
headers=headers,
|
|
data=payload,
|
|
)
|
|
) as response,
|
|
):
|
|
for payload in response:
|
|
if (
|
|
response_type is CredentialCheckResponse
|
|
and payload.keys() == {"result"}
|
|
and payload["result"] is True
|
|
):
|
|
payload = {"status": "valid", "reason_code": None, "retryable": False}
|
|
try:
|
|
return response_type.model_validate(payload)
|
|
except ValidationError as exc:
|
|
raise PluginDaemonInternalServerError("Invalid authentication result from plugin daemon") from exc
|
|
raise PluginDaemonInternalServerError("No authentication result received from plugin daemon")
|