mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 22:02:23 +08:00
222 lines
8.6 KiB
Python
222 lines
8.6 KiB
Python
"""Trigger OAuth client configuration and credential masking."""
|
|
|
|
import json
|
|
from collections.abc import Mapping
|
|
from dataclasses import dataclass
|
|
from typing import Protocol
|
|
|
|
from constants import HIDDEN_VALUE, UNKNOWN_VALUE
|
|
from core.entities.provider_entities import ProviderConfig
|
|
from core.helper.provider_cache import NoOpProviderCredentialCache
|
|
from core.helper.provider_encryption import create_provider_encrypter
|
|
from core.tools.utils.system_encryption import EncryptionError, decrypt_system_params
|
|
from models.provider_ids import TriggerProviderID
|
|
from services.plugin.dispatch_identity import PluginOAuthClientConfig
|
|
|
|
|
|
class TriggerOAuthClientConfigError(Exception):
|
|
"""The stored tenant or system OAuth client configuration cannot be read."""
|
|
|
|
|
|
class TriggerOAuthClientChangedError(Exception):
|
|
"""The OAuth client changed while settings were being prepared."""
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class TriggerOAuthTenantClientRecord:
|
|
id: str
|
|
encrypted_oauth_params: str
|
|
enabled: bool
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class TriggerOAuthClientSnapshot:
|
|
custom: TriggerOAuthTenantClientRecord | None
|
|
# None means no system client exists; an empty ciphertext is corrupt config.
|
|
encrypted_system_oauth_params: str | None
|
|
|
|
@property
|
|
def system_configured(self) -> bool:
|
|
return self.encrypted_system_oauth_params is not None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class TriggerOAuthProviderMetadata:
|
|
schema: list[ProviderConfig]
|
|
plugin_unique_identifier: str
|
|
|
|
|
|
class TriggerOAuthProviderMetadataGateway(Protocol):
|
|
"""Read only the plugin metadata needed to configure and resolve OAuth clients."""
|
|
|
|
def get_metadata(self, *, tenant_id: str, provider_id: TriggerProviderID) -> TriggerOAuthProviderMetadata: ...
|
|
|
|
def is_verified(self, *, tenant_id: str, plugin_unique_identifier: str) -> bool: ...
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class TriggerOAuthClientConfiguration:
|
|
schema: list[ProviderConfig]
|
|
params: Mapping[str, object]
|
|
system_configured: bool
|
|
custom_enabled: bool
|
|
|
|
|
|
class TriggerOAuthClientStore(Protocol):
|
|
"""Persistence operations finish their sessions before returning."""
|
|
|
|
def get(self, *, tenant_id: str, provider_id: TriggerProviderID) -> TriggerOAuthClientSnapshot: ...
|
|
|
|
def save(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
provider_id: TriggerProviderID,
|
|
expected: TriggerOAuthTenantClientRecord | None,
|
|
encrypted_oauth_params: str,
|
|
enabled: bool | None,
|
|
) -> None:
|
|
"""Create when expected is None; otherwise update only an unchanged snapshot.
|
|
|
|
None enabled leaves it unchanged or uses the new-row default.
|
|
"""
|
|
...
|
|
|
|
def delete(self, *, tenant_id: str, provider_id: TriggerProviderID) -> None: ...
|
|
|
|
|
|
class TriggerOAuthClientService:
|
|
"""Handle credentials outside repository transactions.
|
|
|
|
TODO: The shared RSA key provider still owns a scoped DB session; remove that
|
|
dependency when its tenant key reads return detached values.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
clients: TriggerOAuthClientStore,
|
|
providers: TriggerOAuthProviderMetadataGateway,
|
|
) -> None:
|
|
self._clients = clients
|
|
self._providers = providers
|
|
|
|
def get(self, *, tenant_id: str, provider_id: TriggerProviderID) -> TriggerOAuthClientConfiguration:
|
|
snapshot = self._clients.get(tenant_id=tenant_id, provider_id=provider_id)
|
|
provider = self._providers.get_metadata(tenant_id=tenant_id, provider_id=provider_id)
|
|
schema = provider.schema
|
|
params: Mapping[str, object] = {}
|
|
if snapshot.custom is not None:
|
|
encrypter, _ = create_provider_encrypter(
|
|
tenant_id=tenant_id,
|
|
config=[field.to_basic_provider_config() for field in schema],
|
|
cache=NoOpProviderCredentialCache(),
|
|
)
|
|
params = encrypter.mask_plugin_credentials(
|
|
encrypter.decrypt(self._parse_params(snapshot.custom, provider_id))
|
|
)
|
|
|
|
return TriggerOAuthClientConfiguration(
|
|
schema=schema,
|
|
params=params,
|
|
system_configured=snapshot.system_configured
|
|
and self._providers.is_verified(
|
|
tenant_id=tenant_id, plugin_unique_identifier=provider.plugin_unique_identifier
|
|
),
|
|
custom_enabled=snapshot.custom is not None and snapshot.custom.enabled,
|
|
)
|
|
|
|
def resolve(self, *, tenant_id: str, provider_id: TriggerProviderID) -> PluginOAuthClientConfig | None:
|
|
"""Return credentials bound to their plugin package, or None if no permitted client exists.
|
|
|
|
An enabled tenant client takes priority, including an empty configuration.
|
|
Otherwise only a verified plugin may use its system client.
|
|
"""
|
|
snapshot = self._clients.get(tenant_id=tenant_id, provider_id=provider_id)
|
|
provider = self._providers.get_metadata(tenant_id=tenant_id, provider_id=provider_id)
|
|
if snapshot.custom is not None and snapshot.custom.enabled:
|
|
encrypter, _ = create_provider_encrypter(
|
|
tenant_id=tenant_id,
|
|
config=[field.to_basic_provider_config() for field in provider.schema],
|
|
cache=NoOpProviderCredentialCache(),
|
|
)
|
|
return PluginOAuthClientConfig(
|
|
plugin_unique_identifier=provider.plugin_unique_identifier,
|
|
credentials=encrypter.decrypt(self._parse_params(snapshot.custom, provider_id)),
|
|
)
|
|
|
|
encrypted_system_params = snapshot.encrypted_system_oauth_params
|
|
if encrypted_system_params is None or not self._providers.is_verified(
|
|
tenant_id=tenant_id, plugin_unique_identifier=provider.plugin_unique_identifier
|
|
):
|
|
return None
|
|
try:
|
|
return PluginOAuthClientConfig(
|
|
plugin_unique_identifier=provider.plugin_unique_identifier,
|
|
credentials=decrypt_system_params(encrypted_system_params),
|
|
)
|
|
except (EncryptionError, ValueError) as error:
|
|
raise TriggerOAuthClientConfigError(
|
|
f"Stored system OAuth client configuration for trigger provider {provider_id} is invalid."
|
|
) from error
|
|
|
|
def save(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
provider_id: TriggerProviderID,
|
|
client_params: Mapping[str, object] | None,
|
|
enabled: bool | None,
|
|
) -> None:
|
|
"""Save client settings, preserving the existing request semantics.
|
|
|
|
When both values are None this is a no-op. Otherwise None parameters
|
|
clear the saved credentials; None enabled retains the current value.
|
|
"""
|
|
if client_params is None and enabled is None:
|
|
return
|
|
|
|
snapshot = self._clients.get(tenant_id=tenant_id, provider_id=provider_id)
|
|
provider = self._providers.get_metadata(tenant_id=tenant_id, provider_id=provider_id)
|
|
encrypted_params = "{}"
|
|
if client_params is not None:
|
|
encrypter, cache = create_provider_encrypter(
|
|
tenant_id=tenant_id,
|
|
config=[field.to_basic_provider_config() for field in provider.schema],
|
|
cache=NoOpProviderCredentialCache(),
|
|
)
|
|
original_params = encrypter.decrypt(
|
|
self._parse_params(snapshot.custom, provider_id) if snapshot.custom is not None else {}
|
|
)
|
|
new_params = {
|
|
key: value if value != HIDDEN_VALUE else original_params.get(key, UNKNOWN_VALUE)
|
|
for key, value in client_params.items()
|
|
}
|
|
encrypted_params = json.dumps(encrypter.encrypt(new_params))
|
|
cache.delete()
|
|
|
|
self._clients.save(
|
|
tenant_id=tenant_id,
|
|
provider_id=provider_id,
|
|
expected=snapshot.custom,
|
|
encrypted_oauth_params=encrypted_params,
|
|
enabled=enabled,
|
|
)
|
|
|
|
def delete(self, *, tenant_id: str, provider_id: TriggerProviderID) -> None:
|
|
self._clients.delete(tenant_id=tenant_id, provider_id=provider_id)
|
|
|
|
@staticmethod
|
|
def _parse_params(custom: TriggerOAuthTenantClientRecord, provider_id: TriggerProviderID) -> dict[str, object]:
|
|
try:
|
|
params: object = json.loads(custom.encrypted_oauth_params or "{}")
|
|
except json.JSONDecodeError as error:
|
|
raise TriggerOAuthClientConfigError(
|
|
f"Stored OAuth client configuration for trigger provider {provider_id} is invalid."
|
|
) from error
|
|
if not isinstance(params, dict):
|
|
raise TriggerOAuthClientConfigError(
|
|
f"Stored OAuth client configuration for trigger provider {provider_id} must be an object."
|
|
)
|
|
return params
|