diff --git a/api/controllers/console/human_input_form.py b/api/controllers/console/human_input_form.py index 9dce401ae9..d7538f585b 100644 --- a/api/controllers/console/human_input_form.py +++ b/api/controllers/console/human_input_form.py @@ -25,7 +25,7 @@ from models import App from models.enums import CreatorUserRole from models.human_input import RecipientType from models.model import AppMode -from models.workflow import Workflow, WorkflowRun +from models.workflow import WorkflowRun from repositories.factory import DifyAPIRepositoryFactory from services.human_input_service import Form, HumanInputService @@ -42,21 +42,10 @@ class ConsoleHumanInputFormApi(Resource): @staticmethod def _ensure_console_access(form: Form): - current_user, current_tenant_id = current_account_with_tenant() + _, current_tenant_id = current_account_with_tenant() - workflow_run = db.session.get(WorkflowRun, form.workflow_run_id) - if workflow_run is None or workflow_run.tenant_id != current_tenant_id: - raise NotFoundError("Workflow run not found") - - if workflow_run.app_id: - app = db.session.get(App, workflow_run.app_id) - if app is None or app.tenant_id != current_tenant_id: - raise NotFoundError("App not found") - owner_account_id = app.created_by - else: - workflow = db.session.get(Workflow, workflow_run.workflow_id) - if workflow is None or workflow.tenant_id != current_tenant_id: - raise NotFoundError("Workflow not found") + if form.tenant_id != current_tenant_id: + raise NotFoundError("App not found") @setup_required @login_required diff --git a/api/controllers/web/human_input_form.py b/api/controllers/web/human_input_form.py index 1447398587..51cab031ff 100644 --- a/api/controllers/web/human_input_form.py +++ b/api/controllers/web/human_input_form.py @@ -14,9 +14,8 @@ from controllers.web.error import NotFoundError from controllers.web.site import serialize_site from extensions.ext_database import db from models.account import TenantStatus -from models.model import App, Site -from models.workflow import WorkflowRun from models.human_input import RecipientType +from models.model import App, Site from services.human_input_service import Form, FormNotFoundError, HumanInputService logger = logging.getLogger(__name__) @@ -96,15 +95,9 @@ class HumanInputFormApi(Resource): def _get_site_from_form(form: Form) -> Site: - """Resolve Site for the form's workflow run and validate tenant status.""" - workflow_run = ( - db.session.query(WorkflowRun).where(WorkflowRun.id == form.workflow_run_id).first() - ) - if workflow_run is None: - raise NotFoundError("Form not found") - - app_model = db.session.query(App).where(App.id == workflow_run.app_id).first() - if app_model is None: + """Resolve Site for the form's app and validate tenant status.""" + app_model = db.session.query(App).where(App.id == form.app_id).first() + if app_model is None or app_model.tenant_id != form.tenant_id: raise NotFoundError("Form not found") site = db.session.query(Site).where(Site.app_id == app_model.id).first() diff --git a/api/core/repositories/human_input_reposotiry.py b/api/core/repositories/human_input_reposotiry.py index 794206455d..0b26b45db5 100644 --- a/api/core/repositories/human_input_reposotiry.py +++ b/api/core/repositories/human_input_reposotiry.py @@ -134,6 +134,7 @@ class HumanInputFormRecord: workflow_run_id: str node_id: str tenant_id: str + app_id: str definition: FormDefinition rendered_content: str expiration_time: datetime @@ -161,6 +162,7 @@ class HumanInputFormRecord: workflow_run_id=form_model.workflow_run_id, node_id=form_model.node_id, tenant_id=form_model.tenant_id, + app_id=form_model.app_id, definition=FormDefinition.model_validate_json(form_model.form_definition), rendered_content=form_model.rendered_content, expiration_time=form_model.expiration_time, @@ -334,6 +336,7 @@ class HumanInputFormRepositoryImpl: form_model = HumanInputForm( id=form_id, tenant_id=self._tenant_id, + app_id=params.app_id, workflow_run_id=params.workflow_execution_id, node_id=params.node_id, form_definition=form_definition.model_dump_json(), diff --git a/api/core/workflow/nodes/human_input/human_input_node.py b/api/core/workflow/nodes/human_input/human_input_node.py index 4599927079..8a7d6c3c6b 100644 --- a/api/core/workflow/nodes/human_input/human_input_node.py +++ b/api/core/workflow/nodes/human_input/human_input_node.py @@ -208,6 +208,7 @@ class HumanInputNode(Node[HumanInputNodeData]): if form is None: display_in_ui = self._display_in_ui() params = FormCreateParams( + app_id=self.app_id, workflow_execution_id=self._workflow_execution_id, node_id=self.id, form_config=self._node_data, diff --git a/api/core/workflow/repositories/human_input_form_repository.py b/api/core/workflow/repositories/human_input_form_repository.py index 8ceee1aad9..3ad6c53ce5 100644 --- a/api/core/workflow/repositories/human_input_form_repository.py +++ b/api/core/workflow/repositories/human_input_form_repository.py @@ -18,6 +18,7 @@ class FormNotFoundError(HumanInputError): @dataclasses.dataclass class FormCreateParams: + app_id: str workflow_execution_id: str # node_id is the identifier for a specific diff --git a/api/migrations/versions/2026_01_14_0001-7a1c4d2f9b8e_add_app_id_to_human_input_forms.py b/api/migrations/versions/2026_01_14_0001-7a1c4d2f9b8e_add_app_id_to_human_input_forms.py new file mode 100644 index 0000000000..65cb770c9a --- /dev/null +++ b/api/migrations/versions/2026_01_14_0001-7a1c4d2f9b8e_add_app_id_to_human_input_forms.py @@ -0,0 +1,29 @@ +"""Add app_id to human_input_forms + +Revision ID: 7a1c4d2f9b8e +Revises: e63797cc11c2 +Create Date: 2026-01-14 00:01:00.000000 + +""" + +from alembic import op +import models as models +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "7a1c4d2f9b8e" +down_revision = "e63797cc11c2" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + "human_input_forms", + sa.Column("app_id", models.types.StringUUID(), nullable=False), + ) + + +def downgrade(): + op.drop_column("human_input_forms", "app_id") diff --git a/api/models/human_input.py b/api/models/human_input.py index 3a342cd124..a55ca53d75 100644 --- a/api/models/human_input.py +++ b/api/models/human_input.py @@ -31,6 +31,7 @@ class HumanInputForm(DefaultFieldsMixin, Base): __tablename__ = "human_input_forms" tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + app_id: Mapped[str] = mapped_column(StringUUID, nullable=False) workflow_run_id: Mapped[str] = mapped_column(StringUUID, nullable=False) # The human input node the current form corresponds to. diff --git a/api/services/human_input_service.py b/api/services/human_input_service.py index d1d31a7ab6..d9b76da6ae 100644 --- a/api/services/human_input_service.py +++ b/api/services/human_input_service.py @@ -42,6 +42,14 @@ class Form: def workflow_run_id(self) -> str: return self._record.workflow_run_id + @property + def tenant_id(self) -> str: + return self._record.tenant_id + + @property + def app_id(self) -> str: + return self._record.app_id + @property def recipient_id(self) -> str | None: return self._record.recipient_id diff --git a/api/tests/test_containers_integration_tests/core/repositories/test_human_input_form_repository_impl.py b/api/tests/test_containers_integration_tests/core/repositories/test_human_input_form_repository_impl.py index 4af492c22a..f17ee4becb 100644 --- a/api/tests/test_containers_integration_tests/core/repositories/test_human_input_form_repository_impl.py +++ b/api/tests/test_containers_integration_tests/core/repositories/test_human_input_form_repository_impl.py @@ -67,6 +67,7 @@ def _build_form_params(delivery_methods: list[EmailDeliveryMethod]) -> FormCreat user_actions=[UserAction(id="approve", title="Approve")], ) return FormCreateParams( + app_id="app-1", workflow_execution_id=str(uuid4()), node_id="human-input-node", form_config=form_config, @@ -174,6 +175,7 @@ class TestHumanInputFormRepositoryImplWithContainers: repository = HumanInputFormRepositoryImpl(session_factory=engine, tenant_id=tenant.id) resolved_values = {"greeting": "Hello!"} params = FormCreateParams( + app_id="app-1", workflow_execution_id=str(uuid4()), node_id="human-input-node", form_config=HumanInputNodeData( @@ -209,6 +211,7 @@ class TestHumanInputFormRepositoryImplWithContainers: repository = HumanInputFormRepositoryImpl(session_factory=engine, tenant_id=tenant.id) params = FormCreateParams( + app_id="app-1", workflow_execution_id=str(uuid4()), node_id="human-input-node", form_config=HumanInputNodeData( diff --git a/api/tests/test_containers_integration_tests/helpers/execution_extra_content.py b/api/tests/test_containers_integration_tests/helpers/execution_extra_content.py index f937c2e55b..bcfa6e7c82 100644 --- a/api/tests/test_containers_integration_tests/helpers/execution_extra_content.py +++ b/api/tests/test_containers_integration_tests/helpers/execution_extra_content.py @@ -123,6 +123,7 @@ def create_human_input_message_fixture(db_session) -> HumanInputMessageFixture: ) form = HumanInputForm( tenant_id=tenant.id, + app_id=app.id, workflow_run_id=workflow_run_id, node_id="node-id", form_definition=form_definition.model_dump_json(), diff --git a/api/tests/test_containers_integration_tests/tasks/test_mail_human_input_delivery_task.py b/api/tests/test_containers_integration_tests/tasks/test_mail_human_input_delivery_task.py index 51c045a924..adb9de9c25 100644 --- a/api/tests/test_containers_integration_tests/tasks/test_mail_human_input_delivery_task.py +++ b/api/tests/test_containers_integration_tests/tasks/test_mail_human_input_delivery_task.py @@ -88,6 +88,7 @@ def _build_form(db_session_with_containers, tenant, account): engine = db_session_with_containers.get_bind() repo = HumanInputFormRepositoryImpl(session_factory=engine, tenant_id=tenant.id) params = FormCreateParams( + app_id="app-1", workflow_execution_id=str(uuid.uuid4()), node_id="node-1", form_config=node_data, diff --git a/api/tests/unit_tests/controllers/web/test_human_input_form.py b/api/tests/unit_tests/controllers/web/test_human_input_form.py index 24683b6c55..04fc15493f 100644 --- a/api/tests/unit_tests/controllers/web/test_human_input_form.py +++ b/api/tests/unit_tests/controllers/web/test_human_input_form.py @@ -38,7 +38,7 @@ class _FakeSession: self._model_name = model.__name__ return self - def where(self, *args, **kwargs): # noqa: ANN002, ANN003 + def where(self, *args, **kwargs): return self def first(self): @@ -63,6 +63,8 @@ def test_get_form_includes_site(monkeypatch: pytest.MonkeyPatch, app: Flask): class _FakeForm: workflow_run_id = "workflow-1" + app_id = "app-1" + tenant_id = "tenant-1" def get_definition(self): return _FakeDefinition() @@ -70,7 +72,7 @@ def test_get_form_includes_site(monkeypatch: pytest.MonkeyPatch, app: Flask): form = _FakeForm() tenant = SimpleNamespace(status=TenantStatus.NORMAL) - app_model = SimpleNamespace(id="app-1", tenant=tenant) + app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", tenant=tenant) workflow_run = SimpleNamespace(app_id="app-1") site_model = SimpleNamespace( title="My Site", @@ -122,13 +124,15 @@ def test_get_form_raises_forbidden_when_site_missing(monkeypatch: pytest.MonkeyP class _FakeForm: workflow_run_id = "workflow-1" + app_id = "app-1" + tenant_id = "tenant-1" def get_definition(self): return _FakeDefinition() form = _FakeForm() tenant = SimpleNamespace(status=TenantStatus.NORMAL) - app_model = SimpleNamespace(id="app-1", tenant=tenant) + app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", tenant=tenant) workflow_run = SimpleNamespace(app_id="app-1") service_mock = MagicMock() diff --git a/api/tests/unit_tests/core/repositories/test_human_input_form_repository_impl.py b/api/tests/unit_tests/core/repositories/test_human_input_form_repository_impl.py index 88b7ffe3f5..93cd288f03 100644 --- a/api/tests/unit_tests/core/repositories/test_human_input_form_repository_impl.py +++ b/api/tests/unit_tests/core/repositories/test_human_input_form_repository_impl.py @@ -173,6 +173,7 @@ class _DummyForm: workflow_run_id: str node_id: str tenant_id: str + app_id: str form_definition: str rendered_content: str expiration_time: datetime @@ -283,6 +284,7 @@ class TestHumanInputFormRepositoryImplPublicMethods: workflow_run_id="run-1", node_id="node-1", tenant_id="tenant-id", + app_id="app-id", form_definition=_make_form_definition(), rendered_content="

hello

", expiration_time=naive_utc_now(), @@ -316,6 +318,7 @@ class TestHumanInputFormRepositoryImplPublicMethods: workflow_run_id="run-1", node_id="node-1", tenant_id="tenant-id", + app_id="app-id", form_definition=_make_form_definition(), rendered_content="

hello

", expiration_time=naive_utc_now(), @@ -336,6 +339,7 @@ class TestHumanInputFormRepositoryImplPublicMethods: workflow_run_id="run-1", node_id="node-1", tenant_id="tenant-id", + app_id="app-id", form_definition=_make_form_definition(), rendered_content="

hello

", expiration_time=naive_utc_now(), @@ -361,6 +365,7 @@ class TestHumanInputFormSubmissionRepository: workflow_run_id="run-1", node_id="node-1", tenant_id="tenant-1", + app_id="app-1", form_definition=_make_form_definition(), rendered_content="

hello

", expiration_time=naive_utc_now(), @@ -388,6 +393,7 @@ class TestHumanInputFormSubmissionRepository: workflow_run_id="run-1", node_id="node-1", tenant_id="tenant-1", + app_id="app-1", form_definition=_make_form_definition(), rendered_content="

hello

", expiration_time=naive_utc_now(), @@ -420,6 +426,7 @@ class TestHumanInputFormSubmissionRepository: workflow_run_id="run-1", node_id="node-1", tenant_id="tenant-1", + app_id="app-1", form_definition=_make_form_definition(), rendered_content="

hello

", expiration_time=fixed_now, diff --git a/api/tests/unit_tests/repositories/test_sqlalchemy_execution_extra_content_repository.py b/api/tests/unit_tests/repositories/test_sqlalchemy_execution_extra_content_repository.py index 51239809da..cdb362edce 100644 --- a/api/tests/unit_tests/repositories/test_sqlalchemy_execution_extra_content_repository.py +++ b/api/tests/unit_tests/repositories/test_sqlalchemy_execution_extra_content_repository.py @@ -65,6 +65,7 @@ def _build_form(action_id: str, action_title: str, rendered_content: str) -> Hum form = HumanInputForm( id=f"form-{action_id}", tenant_id="tenant-id", + app_id="app-id", workflow_run_id="workflow-run", node_id="node-id", form_definition=definition.model_dump_json(), @@ -133,6 +134,7 @@ def test_get_by_message_ids_returns_unsubmitted_form_definition() -> None: form = HumanInputForm( id="form-1", tenant_id="tenant-id", + app_id="app-id", workflow_run_id="workflow-run", node_id="node-id", form_definition=definition.model_dump_json(), diff --git a/api/tests/unit_tests/services/test_human_input_service.py b/api/tests/unit_tests/services/test_human_input_service.py index 1238aea089..b2f4780ddd 100644 --- a/api/tests/unit_tests/services/test_human_input_service.py +++ b/api/tests/unit_tests/services/test_human_input_service.py @@ -41,6 +41,7 @@ def sample_form_record(): workflow_run_id="workflow-run-id", node_id="node-id", tenant_id="tenant-id", + app_id="app-id", definition=FormDefinition( form_content="hello", inputs=[],