diff --git a/api/migrations/versions/2025_11_18_1400-a7b4e8f2c9d1_add_enduser_authentication_provider.py b/api/migrations/versions/2025_11_18_1400-a7b4e8f2c9d1_add_enduser_authentication_provider.py new file mode 100644 index 0000000000..75a24ec343 --- /dev/null +++ b/api/migrations/versions/2025_11_18_1400-a7b4e8f2c9d1_add_enduser_authentication_provider.py @@ -0,0 +1,91 @@ +"""add enduser authentication provider + +Revision ID: a7b4e8f2c9d1 +Revises: 132392a2635f +Create Date: 2025-11-18 14:00:00.000000 + +""" +import models as models +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "a7b4e8f2c9d1" +down_revision = "132392a2635f" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "tool_enduser_authentication_providers", + sa.Column( + "id", + models.types.StringUUID(), + nullable=False, + ), + sa.Column( + "name", + sa.String(length=256), + server_default="API KEY 1", + nullable=False, + ), + sa.Column("tenant_id", models.types.StringUUID(), nullable=False), + sa.Column("end_user_id", models.types.StringUUID(), nullable=False), + sa.Column("provider", sa.Text(), nullable=False), + sa.Column("encrypted_credentials", sa.Text(), default="", nullable=False), + sa.Column( + "created_at", + sa.DateTime(), + server_default=sa.func.current_timestamp(), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(), + server_default=sa.func.current_timestamp(), + nullable=False, + ), + sa.Column( + "credential_type", + sa.String(length=32), + server_default="api-key", + nullable=False, + ), + sa.Column("expires_at", sa.BigInteger(), server_default=sa.text("-1"), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint( + "end_user_id", + "provider", + ), + ) + op.create_index( + op.f("ix_tool_enduser_authentication_providers_end_user_id"), + "tool_enduser_authentication_providers", + ["end_user_id"], + unique=False, + ) + op.create_index( + op.f("ix_tool_enduser_authentication_providers_provider"), + "tool_enduser_authentication_providers", + ["provider"], + unique=False, + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index( + op.f("ix_tool_enduser_authentication_providers_provider"), + table_name="tool_enduser_authentication_providers", + ) + op.drop_index( + op.f("ix_tool_enduser_authentication_providers_end_user_id"), + table_name="tool_enduser_authentication_providers", + ) + op.drop_table("tool_enduser_authentication_providers") + # ### end Alembic commands ### + + diff --git a/api/models/__init__.py b/api/models/__init__.py index 906bc3198e..b10b6918a4 100644 --- a/api/models/__init__.py +++ b/api/models/__init__.py @@ -80,6 +80,7 @@ from .task import CeleryTask, CeleryTaskSet from .tools import ( ApiToolProvider, BuiltinToolProvider, + EndUserAuthenticationProvider, ToolConversationVariables, ToolFile, ToolLabelBinding, @@ -149,6 +150,7 @@ __all__ = [ "DocumentSegment", "Embedding", "EndUser", + "EndUserAuthenticationProvider", "ExternalKnowledgeApis", "ExternalKnowledgeBindings", "IconType", diff --git a/api/models/tools.py b/api/models/tools.py index 0a79f95a70..95cadeb030 100644 --- a/api/models/tools.py +++ b/api/models/tools.py @@ -9,9 +9,11 @@ from deprecated import deprecated from sqlalchemy import ForeignKey, String, func from sqlalchemy.orm import Mapped, mapped_column +from core.plugin.entities.plugin_daemon import CredentialType from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_bundle import ApiToolBundle from core.tools.entities.tool_entities import ApiProviderSchemaType, WorkflowToolParameterConfiguration +from libs.uuid_utils import uuidv7 from .base import TypeBase from .engine import db @@ -109,6 +111,59 @@ class BuiltinToolProvider(TypeBase): return cast(dict[str, Any], json.loads(self.encrypted_credentials)) +class EndUserAuthenticationProvider(TypeBase): + """ + This table stores the authentication credentials for end users in tools. + Mimics the BuiltinToolProvider structure but for end users instead of tenants. + """ + + __tablename__ = "tool_enduser_authentication_providers" + __table_args__ = ( + sa.UniqueConstraint("end_user_id", "provider"), + ) + + # id of the authentication provider + id: Mapped[str] = mapped_column(StringUUID, primary_key=True, default=lambda: str(uuid4()), init=False) + name: Mapped[str] = mapped_column( + String(256), + nullable=False, + default="API KEY 1", + ) + # id of the tenant + tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + # id of the end user + end_user_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + # name of the tool provider + provider: Mapped[str] = mapped_column(LongText, nullable=False) + # encrypted credentials for the end user + encrypted_credentials: Mapped[str] = mapped_column(LongText, nullable=False, default="") + created_at: Mapped[datetime] = mapped_column( + sa.DateTime, nullable=False, server_default=func.current_timestamp(), init=False + ) + updated_at: Mapped[datetime] = mapped_column( + sa.DateTime, + nullable=False, + server_default=func.current_timestamp(), + onupdate=func.current_timestamp(), + init=False, + ) + # credential type, e.g., "api-key", "oauth2" + credential_type: Mapped[CredentialType] = mapped_column( + String(32), nullable=False, default=CredentialType.API_KEY + ) + # Unix timestamp in seconds since epoch (1970-01-01 UTC); -1 indicates no expiration + expires_at: Mapped[int] = mapped_column(sa.BigInteger, nullable=False, default=-1) + + @property + def credentials(self) -> dict[str, Any]: + if not self.encrypted_credentials: + return {} + try: + return cast(dict[str, Any], json.loads(self.encrypted_credentials)) + except json.JSONDecodeError: + return {} + + class ApiToolProvider(TypeBase): """ The table stores the api providers.