mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 22:02:23 +08:00
Allow ordinary workflows to reuse exact saved node credentials while keeping Builder actor-scoped. Recheck bindings at read and OAuth refresh. Scope isolated runs to their executed nodes and resolve native provider aliases. Persist refreshes without database-specific UPDATE RETURNING.
98 lines
4.8 KiB
Python
98 lines
4.8 KiB
Python
"""Persist ordinary draft OAuth rotation after native external refresh has closed.
|
|
|
|
Admission remains metadata-only. This mutation receives already encrypted
|
|
native output and updates the exact checked credential in one bounded SQL
|
|
write, repeating actor access or the exact saved ordinary workflow binding and
|
|
comparing the original native read. A changed binding, revoked actor access,
|
|
ownership change, deletion or concurrent rotation cannot be overwritten.
|
|
Published and Builder confirmed refresh retain their separate existing owners.
|
|
"""
|
|
|
|
from sqlalchemy import select, update
|
|
from sqlalchemy.engine import CursorResult
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
|
|
from core.plugin.entities.plugin_daemon import CredentialType as ToolCredentialType
|
|
from core.tools.errors import ToolProviderCredentialValidationError
|
|
from models.credential_permission import CredentialType
|
|
from models.tools import BuiltinToolProvider
|
|
from repositories.credentials.query_repository import (
|
|
apply_credential_visibility_filter_for_actor,
|
|
workflow_tool_credential_binding_condition,
|
|
)
|
|
from services.credentials.query import WorkflowToolCredentialMetadata
|
|
from services.tools.draft_tool_setup import DraftToolRuntimeSelection, tool_credential_fingerprint
|
|
|
|
|
|
def persist_draft_tool_refresh(
|
|
*,
|
|
session_maker: sessionmaker[Session],
|
|
selection: DraftToolRuntimeSelection,
|
|
original_encrypted_credentials: str | None,
|
|
original_expires_at: int,
|
|
original_user_id: str | None,
|
|
refreshed_encrypted_credentials: str,
|
|
refreshed_expires_at: int,
|
|
) -> str:
|
|
"""Commit one authorized unchanged selection and return its opaque version; no external I/O occurs here.
|
|
|
|
The original opaque ciphertext/expiry/owner come from the native constrained
|
|
read, before OAuth I/O. ``None`` represents the actual stored SQL NULL;
|
|
its comparison retains SQLAlchemy's IS NULL semantics rather than a blank
|
|
ciphertext fallback. The short transaction rechecks actor visibility or the
|
|
saved node binding. Exactly one updated row is required; the fingerprint
|
|
is read in the same transaction, without unsupported UPDATE RETURNING.
|
|
Neither token plaintext nor a graph/client permission marker enters this owner.
|
|
"""
|
|
credential = selection.credential
|
|
if credential is None:
|
|
raise ToolProviderCredentialValidationError("Tool credential changed or access was revoked during refresh.")
|
|
statement = update(BuiltinToolProvider).where(
|
|
BuiltinToolProvider.id == credential.id,
|
|
BuiltinToolProvider.tenant_id == selection.tenant_id,
|
|
BuiltinToolProvider.provider == credential.provider,
|
|
BuiltinToolProvider.credential_type == ToolCredentialType.OAUTH2,
|
|
BuiltinToolProvider.user_id == original_user_id,
|
|
BuiltinToolProvider.encrypted_credentials == original_encrypted_credentials,
|
|
BuiltinToolProvider.expires_at == original_expires_at,
|
|
)
|
|
if not isinstance(credential, WorkflowToolCredentialMetadata):
|
|
statement = apply_credential_visibility_filter_for_actor(
|
|
statement,
|
|
tenant_id=selection.tenant_id,
|
|
model_id_column=BuiltinToolProvider.id,
|
|
model_user_id_column=BuiltinToolProvider.user_id,
|
|
model_visibility_column=BuiltinToolProvider.visibility,
|
|
credential_type=CredentialType.BUILTIN_TOOL_PROVIDER,
|
|
actor_id=selection.actor_id,
|
|
)
|
|
statement = statement.values(
|
|
encrypted_credentials=refreshed_encrypted_credentials,
|
|
expires_at=refreshed_expires_at,
|
|
)
|
|
try:
|
|
with session_maker.begin() as session:
|
|
if isinstance(credential, WorkflowToolCredentialMetadata):
|
|
statement = statement.where(workflow_tool_credential_binding_condition(session, credential))
|
|
result = session.execute(statement.execution_options(synchronize_session=False))
|
|
assert isinstance(result, CursorResult)
|
|
if result.rowcount != 1:
|
|
raise ToolProviderCredentialValidationError(
|
|
"Tool credential changed or access was revoked during refresh."
|
|
)
|
|
updated = session.scalar(
|
|
select(BuiltinToolProvider).where(
|
|
BuiltinToolProvider.id == credential.id,
|
|
BuiltinToolProvider.tenant_id == selection.tenant_id,
|
|
BuiltinToolProvider.provider == credential.provider,
|
|
)
|
|
)
|
|
if updated is None:
|
|
raise ToolProviderCredentialValidationError("Tool credential refresh could not be persisted.")
|
|
fingerprint = tool_credential_fingerprint(updated)
|
|
return fingerprint
|
|
except ToolProviderCredentialValidationError:
|
|
raise
|
|
except Exception:
|
|
raise ToolProviderCredentialValidationError("Tool credential refresh could not be persisted.") from None
|