mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 13:51:25 +08:00
# Conflicts: # api/.importlinter # api/controllers/console/__init__.py # api/controllers/console/workspace/account.py # api/core/app/apps/message_based_app_generator.py # api/core/app/entities/app_invoke_entities.py # api/core/tools/tool_manager.py # api/extensions/application_services/installed_app.py # api/extensions/ext_application_services.py # api/models/__init__.py # api/models/workflow.py # api/openapi/markdown/console-openapi.md # api/openapi/markdown/web-openapi.md # api/repositories/account/repository.py # api/repositories/api_workflow_run_repository.py # api/repositories/app/console_repository.py # api/repositories/app_definition_query_repository.py # api/services/app_definition_query_service.py # api/services/app_generate_service.py # api/services/conversation_service.py # api/services/message_service.py # api/services/web_conversation_service.py # api/tasks/app_generate/workflow_execute_task.py # api/tests/unit_tests/controllers/console/auth/test_login.py # api/tests/unit_tests/controllers/web/test_app.py # api/tests/unit_tests/core/app/apps/advanced_chat/test_app_generator.py # api/tests/unit_tests/repositories/app/test_console_repository.py # api/tests/unit_tests/services/test_app_definition_query_service.py # api/tests/unit_tests/services/test_app_generate_service.py # api/tests/unit_tests/services/test_message_service.py # docker/envs/core-services/api.env.example # packages/contracts/generated/api/console/account/types.gen.ts # packages/contracts/generated/api/console/account/zod.gen.ts # packages/contracts/generated/api/console/apps/errors.gen.ts # packages/contracts/generated/api/console/apps/orpc.gen.ts # packages/contracts/generated/api/console/errors.gen.ts # packages/contracts/generated/api/console/workspaces/errors.gen.ts # packages/contracts/generated/api/console/workspaces/orpc.gen.ts # packages/contracts/generated/api/web/errors.gen.ts # packages/contracts/generated/api/web/orpc.gen.ts # packages/contracts/generated/api/web/types.gen.ts # packages/contracts/generated/api/web/zod.gen.ts
124 lines
4.7 KiB
Python
124 lines
4.7 KiB
Python
"""Resolve tool credentials through scoped snapshots and serialized OAuth refreshes."""
|
|
|
|
import time
|
|
from collections.abc import Mapping
|
|
from dataclasses import dataclass
|
|
from typing import Protocol
|
|
|
|
from services.credentials.refresh import CredentialRefreshLock, credential_needs_refresh
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ToolRuntimeCredential:
|
|
"""Detached provider state; absent ciphertext represents empty credentials."""
|
|
|
|
id: str
|
|
workspace_id: str
|
|
owner_id: str
|
|
provider: str
|
|
credential_type: str
|
|
encrypted_credentials: str | None
|
|
expires_at: int
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class RefreshedToolCredential:
|
|
credentials: Mapping[str, object]
|
|
expires_at: int
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ResolvedToolCredential:
|
|
credential_id: str
|
|
credentials: Mapping[str, object]
|
|
credential_type: str
|
|
|
|
|
|
class ToolCredentialNotFoundError(ValueError):
|
|
pass
|
|
|
|
|
|
class ToolCredentialRefreshError(ValueError):
|
|
pass
|
|
|
|
|
|
class ToolCredentialStore(Protocol):
|
|
def get_tool_credential(
|
|
self, *, workspace_id: str, providers: tuple[str, ...], credential_id: str | None
|
|
) -> ToolRuntimeCredential | None:
|
|
"""Return a detached snapshot, scoped to workspace and provider, preferring the default when ID is absent."""
|
|
...
|
|
|
|
def update_tool_credential_if_unchanged(
|
|
self, *, record: ToolRuntimeCredential, encrypted_credentials: str, expires_at: int
|
|
) -> bool:
|
|
"""Commit in a short transaction, guarding the owner, auth type, expiry and previous ciphertext."""
|
|
...
|
|
|
|
|
|
class ToolCredentialRuntime(Protocol):
|
|
def check_policy(self, record: ToolRuntimeCredential) -> None: ...
|
|
|
|
def decrypt(self, record: ToolRuntimeCredential) -> dict[str, object]: ...
|
|
|
|
def encrypt(self, record: ToolRuntimeCredential, credentials: Mapping[str, object]) -> str: ...
|
|
|
|
def refresh(self, record: ToolRuntimeCredential, credentials: Mapping[str, object]) -> RefreshedToolCredential: ...
|
|
|
|
def invalidate(self, record: ToolRuntimeCredential) -> None: ...
|
|
|
|
|
|
class ToolCredentialService:
|
|
def __init__(
|
|
self,
|
|
*,
|
|
credentials: ToolCredentialStore,
|
|
runtime: ToolCredentialRuntime,
|
|
refresh_lock: CredentialRefreshLock,
|
|
providers: tuple[str, ...],
|
|
) -> None:
|
|
self._credentials = credentials
|
|
self._runtime = runtime
|
|
self._refresh_lock = refresh_lock
|
|
self._providers = providers
|
|
|
|
def resolve(self, *, workspace_id: str, credential_id: str | None) -> ResolvedToolCredential:
|
|
record = self._load(workspace_id=workspace_id, credential_id=credential_id)
|
|
self._runtime.check_policy(record)
|
|
if not credential_needs_refresh(record.expires_at, now=time.time()):
|
|
return self._resolved(record)
|
|
|
|
with self._refresh_lock.acquire(
|
|
credential_type="tool", workspace_id=workspace_id, credential_id=record.id
|
|
) as lease:
|
|
record = self._load(workspace_id=workspace_id, credential_id=record.id)
|
|
self._runtime.check_policy(record)
|
|
if not credential_needs_refresh(record.expires_at, now=time.time()):
|
|
return self._resolved(record)
|
|
decrypted = self._runtime.decrypt(record)
|
|
lease.ensure_owned()
|
|
refreshed = self._runtime.refresh(record, decrypted)
|
|
encrypted = self._runtime.encrypt(record, refreshed.credentials)
|
|
lease.ensure_owned()
|
|
if not self._credentials.update_tool_credential_if_unchanged(
|
|
record=record, encrypted_credentials=encrypted, expires_at=refreshed.expires_at
|
|
):
|
|
latest = self._load(workspace_id=workspace_id, credential_id=record.id)
|
|
self._runtime.check_policy(latest)
|
|
if credential_needs_refresh(latest.expires_at, now=time.time()):
|
|
raise ToolCredentialRefreshError("Credential changed while it was being refreshed. Please retry.")
|
|
return self._resolved(latest)
|
|
self._runtime.invalidate(record)
|
|
return ResolvedToolCredential(record.id, refreshed.credentials, record.credential_type)
|
|
|
|
def _load(self, *, workspace_id: str, credential_id: str | None) -> ToolRuntimeCredential:
|
|
record = self._credentials.get_tool_credential(
|
|
workspace_id=workspace_id, providers=self._providers, credential_id=credential_id
|
|
)
|
|
if record is None:
|
|
raise ToolCredentialNotFoundError("Tool credential not found")
|
|
return record
|
|
|
|
def _resolved(self, record: ToolRuntimeCredential) -> ResolvedToolCredential:
|
|
return ResolvedToolCredential(record.id, self._runtime.decrypt(record), record.credential_type)
|