mirror of
https://github.com/langgenius/dify.git
synced 2026-11-13 06:41:07 +08:00
255 lines
13 KiB
Python
255 lines
13 KiB
Python
"""Personal credentials and saved selections, separate from workspace credentials.
|
|
|
|
Account, EndUser, App, Workflow, business Session, and Run references are logical pointers.
|
|
Callers must validate their complete ownership chain before writing selections;
|
|
the foreign keys here only enforce the credential namespace and Provider.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from constants.end_user_connections import (
|
|
ConnectionCredentialStatus,
|
|
ConnectionCredentialType,
|
|
ConnectionRevisionAcceptanceReason,
|
|
OAuthClientSource,
|
|
)
|
|
from libs.datetime_utils import naive_utc_now
|
|
from libs.uuid_utils import uuidv7
|
|
|
|
from .base import DefaultFieldsDCMixin, TypeBase
|
|
from .types import EnumText, LongText, StringUUID
|
|
|
|
|
|
class EndUserConnectionRevision(TypeBase):
|
|
"""Immutable authorization snapshots deduplicated within one Web App.
|
|
|
|
Keep old snapshots for consent validation after upgrades and for revocation
|
|
references after uninstall. They contain no personal credentials or Client secrets.
|
|
"""
|
|
|
|
__tablename__ = "end_user_connection_revisions"
|
|
|
|
__table_args__ = (
|
|
sa.UniqueConstraint("tenant_id", "app_id", "fingerprint", name="end_user_connection_revision_content_key"),
|
|
sa.UniqueConstraint("id", "tenant_id", "app_id", name="end_user_connection_revision_scope_key"),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(StringUUID, primary_key=True, default_factory=lambda: str(uuidv7()), init=False)
|
|
fingerprint: Mapped[str] = mapped_column(sa.String(64), nullable=False)
|
|
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
|
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
|
snapshot: Mapped[str] = mapped_column(LongText, nullable=False, repr=False)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
sa.DateTime,
|
|
nullable=False,
|
|
default_factory=naive_utc_now,
|
|
server_default=sa.func.current_timestamp(),
|
|
)
|
|
|
|
|
|
class EndUserConnectionRevisionAcceptance(TypeBase):
|
|
"""First acceptance of an authorization contract by one App-scoped EndUser.
|
|
|
|
Preserve the original reason when a later request resubmits an inherited ID.
|
|
"""
|
|
|
|
__tablename__ = "end_user_connection_revision_acceptances"
|
|
__table_args__ = (
|
|
sa.CheckConstraint(
|
|
"(reason = 'approved' AND source_revision_id IS NULL) OR "
|
|
"(reason = 'inherited' AND source_revision_id IS NOT NULL AND source_revision_id <> revision_id)",
|
|
name=sa.schema.conv("end_user_revision_acceptance_reason_check"),
|
|
),
|
|
sa.UniqueConstraint(
|
|
"tenant_id", "app_id", "end_user_id", "revision_id", name="end_user_revision_acceptance_end_user_key"
|
|
),
|
|
sa.ForeignKeyConstraint(
|
|
["revision_id", "tenant_id", "app_id"],
|
|
[
|
|
"end_user_connection_revisions.id",
|
|
"end_user_connection_revisions.tenant_id",
|
|
"end_user_connection_revisions.app_id",
|
|
],
|
|
name="end_user_revision_acceptance_scope_fkey",
|
|
),
|
|
sa.ForeignKeyConstraint(
|
|
["source_revision_id", "tenant_id", "app_id"],
|
|
[
|
|
"end_user_connection_revisions.id",
|
|
"end_user_connection_revisions.tenant_id",
|
|
"end_user_connection_revisions.app_id",
|
|
],
|
|
name="end_user_revision_acceptance_source_scope_fkey",
|
|
),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(StringUUID, primary_key=True, default_factory=lambda: str(uuidv7()), init=False)
|
|
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
|
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
|
end_user_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
|
revision_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
|
reason: Mapped[ConnectionRevisionAcceptanceReason] = mapped_column(
|
|
EnumText(ConnectionRevisionAcceptanceReason), nullable=False
|
|
)
|
|
source_revision_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
sa.DateTime, nullable=False, default_factory=naive_utc_now, server_default=sa.func.current_timestamp()
|
|
)
|
|
|
|
|
|
class EndUserConnection(DefaultFieldsDCMixin, TypeBase):
|
|
"""An Account owns credentials across Apps within their tenant namespace.
|
|
|
|
Guest and service identities own credentials through their App-scoped EndUser.
|
|
Neither queries nor checks automatically extend last_active_at. Credential
|
|
mutation services must condition writes on owner and revision after external I/O.
|
|
"""
|
|
|
|
__tablename__ = "end_user_connections"
|
|
__table_args__ = (
|
|
sa.CheckConstraint(
|
|
"(account_id IS NOT NULL AND end_user_id IS NULL) OR (account_id IS NULL AND end_user_id IS NOT NULL)",
|
|
name=sa.schema.conv("end_user_connection_owner_check"),
|
|
),
|
|
sa.CheckConstraint(
|
|
"credential_type IN ('api-key', 'oauth2')", name=sa.schema.conv("end_user_connection_credential_type_check")
|
|
),
|
|
sa.CheckConstraint(
|
|
"status IN ('connected', 'reconnect_required', 'invalid')",
|
|
name=sa.schema.conv("end_user_connection_status_check"),
|
|
),
|
|
sa.CheckConstraint(
|
|
"credential_type = 'oauth2' OR account_label IS NULL",
|
|
name=sa.schema.conv("end_user_connection_account_label_check"),
|
|
),
|
|
sa.CheckConstraint("revision > 0", name=sa.schema.conv("end_user_connection_revision_check")),
|
|
sa.CheckConstraint(
|
|
sa.func.char_length(sa.column("name")).between(1, 100),
|
|
name=sa.schema.conv("end_user_connection_name_check"),
|
|
),
|
|
sa.CheckConstraint(
|
|
"(credential_type = 'api-key' AND oauth_client_source IS NULL "
|
|
"AND oauth_client_configuration_id IS NULL AND oauth_client_id IS NULL AND expires_at IS NULL) OR "
|
|
"(credential_type = 'oauth2' AND oauth_client_source IS NOT NULL "
|
|
"AND oauth_client_source IN ('workspace', 'platform'))",
|
|
name=sa.schema.conv("end_user_connection_oauth_check"),
|
|
),
|
|
# Selection FKs prevent a valid ID from being paired with another tenant or Provider.
|
|
sa.UniqueConstraint("id", "tenant_id", "provider", name="end_user_connection_scope_key"),
|
|
sa.Index("end_user_connection_account_idx", "tenant_id", "account_id", "id"),
|
|
sa.Index("end_user_connection_consumer_idx", "account_id", "provider", "credential_type", "id"),
|
|
sa.Index("end_user_connection_end_user_idx", "tenant_id", "end_user_id", "id"),
|
|
sa.Index("end_user_connection_activity_idx", "last_active_at", "id"),
|
|
)
|
|
|
|
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
|
# Exactly one owner reference is populated; EndUser carries its own identity type.
|
|
account_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
|
end_user_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
|
provider: Mapped[str] = mapped_column(sa.String(255), nullable=False)
|
|
credential_type: Mapped[ConnectionCredentialType] = mapped_column(
|
|
EnumText(ConnectionCredentialType), nullable=False
|
|
)
|
|
name: Mapped[str] = mapped_column(sa.String(100), nullable=False)
|
|
encrypted_credentials: Mapped[str] = mapped_column(LongText, nullable=False, repr=False)
|
|
status: Mapped[ConnectionCredentialStatus] = mapped_column(EnumText(ConnectionCredentialStatus), nullable=False)
|
|
# All client fields are absent for API keys. OAuth requires only a source.
|
|
oauth_client_source: Mapped[OAuthClientSource | None] = mapped_column(EnumText(OAuthClientSource), nullable=True)
|
|
# None means the client has no independent configuration record or no available public ID.
|
|
oauth_client_configuration_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
|
oauth_client_id: Mapped[str | None] = mapped_column(LongText, nullable=True)
|
|
# None means unknown expiration, or not applicable for API keys.
|
|
expires_at: Mapped[datetime | None] = mapped_column(sa.DateTime, nullable=True)
|
|
# Original OAuth account identity, independent of the editable name; masked in API responses.
|
|
# None means unavailable identity metadata or an API-key connection.
|
|
account_label: Mapped[str | None] = mapped_column(LongText, nullable=True, default=None)
|
|
oauth_plugin_unique_identifier: Mapped[str | None] = mapped_column(sa.String(512), nullable=True, default=None)
|
|
# Legacy grants without provenance must be reauthorized before use.
|
|
oauth_client_fingerprint: Mapped[str | None] = mapped_column(sa.String(64), nullable=True, default=None, repr=False)
|
|
revision: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=1, server_default=sa.text("1"))
|
|
last_active_at: Mapped[datetime] = mapped_column(
|
|
sa.DateTime,
|
|
nullable=False,
|
|
default_factory=naive_utc_now,
|
|
server_default=sa.func.current_timestamp(),
|
|
)
|
|
|
|
|
|
class AppConnection(DefaultFieldsDCMixin, TypeBase):
|
|
"""One explicitly saved Tool node choice for an App-scoped EndUser.
|
|
|
|
Replace the whole set under a stable owner lock, including for an empty set.
|
|
Authorization is recorded separately for the App-scoped EndUser, shared by all
|
|
Conversations. Execution never updates defaults; saved choices do not reference Runs.
|
|
workflow_id and node_id identify the published graph version where this choice
|
|
was saved. Changed authorization requirements require reconfirmation; republishing the
|
|
same requirements preserves the choice. Callers validate Workflow ownership and node authentication requirements
|
|
before replacing the saved version and choice; these rows are not version history.
|
|
"""
|
|
|
|
__tablename__ = "app_connections"
|
|
__table_args__ = (
|
|
sa.UniqueConstraint("app_id", "end_user_id", "node_id", name="app_connection_selection_key"),
|
|
sa.ForeignKeyConstraint(
|
|
["connection_id", "tenant_id", "provider"],
|
|
["end_user_connections.id", "end_user_connections.tenant_id", "end_user_connections.provider"],
|
|
name="app_connection_scope_fkey",
|
|
ondelete="CASCADE",
|
|
),
|
|
sa.Index("app_connection_connection_idx", "connection_id"),
|
|
sa.CheckConstraint(
|
|
sa.func.char_length(sa.column("node_id")).between(1, 255),
|
|
name=sa.schema.conv("app_connection_node_id_check"),
|
|
),
|
|
)
|
|
|
|
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
|
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
|
workflow_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
|
node_id: Mapped[str] = mapped_column(sa.String(255), nullable=False)
|
|
end_user_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
|
provider: Mapped[str] = mapped_column(sa.String(255), nullable=False)
|
|
connection_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
|
|
|
|
|
class SessionConnection(DefaultFieldsDCMixin, TypeBase):
|
|
"""A business Session's saved choice, independent of App defaults and Run inputs.
|
|
|
|
session_id refers to a Conversation for Chatflow. Plain Workflow's authoritative
|
|
Session mapping is owned by the Run integration. It must never refer to
|
|
EndUser.session_id, an OAuth context, or a Portal token. Ownership is resolved
|
|
through the business Session, even when no selection rows exist. Retained
|
|
references after identity migration do not transfer credential ownership. Like
|
|
App defaults, workflow_id records the published graph version of the saved choice.
|
|
These rows only store choices. Authorization is shared by the App-scoped EndUser;
|
|
confirming a changed contract in one Conversation authorizes the other Conversations.
|
|
An empty set remains empty instead of inheriting App defaults.
|
|
"""
|
|
|
|
__tablename__ = "session_connections"
|
|
__table_args__ = (
|
|
sa.UniqueConstraint("app_id", "session_id", "node_id", name="session_connection_selection_key"),
|
|
sa.ForeignKeyConstraint(
|
|
["connection_id", "tenant_id", "provider"],
|
|
["end_user_connections.id", "end_user_connections.tenant_id", "end_user_connections.provider"],
|
|
name="session_connection_scope_fkey",
|
|
ondelete="CASCADE",
|
|
),
|
|
sa.Index("session_connection_connection_idx", "connection_id"),
|
|
sa.CheckConstraint(
|
|
sa.func.char_length(sa.column("node_id")).between(1, 255),
|
|
name=sa.schema.conv("session_connection_node_id_check"),
|
|
),
|
|
)
|
|
|
|
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
|
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
|
workflow_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
|
node_id: Mapped[str] = mapped_column(sa.String(255), nullable=False)
|
|
session_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
|
provider: Mapped[str] = mapped_column(sa.String(255), nullable=False)
|
|
connection_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|