mirror of
https://github.com/langgenius/dify.git
synced 2026-04-15 18:06:36 +08:00
guard workflow comment writes with edit permission
This commit is contained in:
parent
f6d0fdefc5
commit
e7de5919a0
@ -6,7 +6,7 @@ from pydantic import BaseModel, Field, TypeAdapter
|
||||
from controllers.common.schema import register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from controllers.console.wraps import account_initialization_required, edit_permission_required, setup_required
|
||||
from fields.member_fields import AccountWithRole
|
||||
from fields.workflow_comment_fields import (
|
||||
workflow_comment_basic_fields,
|
||||
@ -99,6 +99,7 @@ class WorkflowCommentListApi(Resource):
|
||||
@account_initialization_required
|
||||
@get_app_model()
|
||||
@marshal_with(workflow_comment_create_model)
|
||||
@edit_permission_required
|
||||
def post(self, app_model: App):
|
||||
"""Create a new workflow comment."""
|
||||
payload = WorkflowCommentCreatePayload.model_validate(console_ns.payload or {})
|
||||
@ -147,6 +148,7 @@ class WorkflowCommentDetailApi(Resource):
|
||||
@account_initialization_required
|
||||
@get_app_model()
|
||||
@marshal_with(workflow_comment_update_model)
|
||||
@edit_permission_required
|
||||
def put(self, app_model: App, comment_id: str):
|
||||
"""Update a workflow comment."""
|
||||
payload = WorkflowCommentUpdatePayload.model_validate(console_ns.payload or {})
|
||||
@ -172,6 +174,7 @@ class WorkflowCommentDetailApi(Resource):
|
||||
@setup_required
|
||||
@account_initialization_required
|
||||
@get_app_model()
|
||||
@edit_permission_required
|
||||
def delete(self, app_model: App, comment_id: str):
|
||||
"""Delete a workflow comment."""
|
||||
WorkflowCommentService.delete_comment(
|
||||
@ -197,6 +200,7 @@ class WorkflowCommentResolveApi(Resource):
|
||||
@account_initialization_required
|
||||
@get_app_model()
|
||||
@marshal_with(workflow_comment_resolve_model)
|
||||
@edit_permission_required
|
||||
def post(self, app_model: App, comment_id: str):
|
||||
"""Resolve a workflow comment."""
|
||||
comment = WorkflowCommentService.resolve_comment(
|
||||
@ -223,6 +227,7 @@ class WorkflowCommentReplyApi(Resource):
|
||||
@account_initialization_required
|
||||
@get_app_model()
|
||||
@marshal_with(workflow_comment_reply_create_model)
|
||||
@edit_permission_required
|
||||
def post(self, app_model: App, comment_id: str):
|
||||
"""Add a reply to a workflow comment."""
|
||||
# Validate comment access first
|
||||
@ -256,6 +261,7 @@ class WorkflowCommentReplyDetailApi(Resource):
|
||||
@account_initialization_required
|
||||
@get_app_model()
|
||||
@marshal_with(workflow_comment_reply_update_model)
|
||||
@edit_permission_required
|
||||
def put(self, app_model: App, comment_id: str, reply_id: str):
|
||||
"""Update a comment reply."""
|
||||
# Validate comment access first
|
||||
@ -285,6 +291,7 @@ class WorkflowCommentReplyDetailApi(Resource):
|
||||
@setup_required
|
||||
@account_initialization_required
|
||||
@get_app_model()
|
||||
@edit_permission_required
|
||||
def delete(self, app_model: App, comment_id: str, reply_id: str):
|
||||
"""Delete a comment reply."""
|
||||
# Validate comment access first
|
||||
|
||||
@ -0,0 +1,174 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import nullcontext
|
||||
from dataclasses import dataclass
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from controllers.console import console_ns
|
||||
from controllers.console import wraps as console_wraps
|
||||
from controllers.console.app import workflow_comment as workflow_comment_module
|
||||
from controllers.console.app import wraps as app_wraps
|
||||
from libs import login as login_lib
|
||||
from models.account import Account, AccountStatus, TenantAccountRole
|
||||
|
||||
|
||||
def _make_account(role: TenantAccountRole) -> Account:
|
||||
account = Account(name="tester", email="tester@example.com")
|
||||
account.status = AccountStatus.ACTIVE
|
||||
account.role = role
|
||||
account.id = "account-123" # type: ignore[assignment]
|
||||
account._current_tenant = SimpleNamespace(id="tenant-123") # type: ignore[attr-defined]
|
||||
account._get_current_object = lambda: account # type: ignore[attr-defined]
|
||||
return account
|
||||
|
||||
|
||||
def _make_app() -> SimpleNamespace:
|
||||
return SimpleNamespace(id="app-123", tenant_id="tenant-123", status="normal", mode="workflow")
|
||||
|
||||
|
||||
def _patch_console_guards(monkeypatch: pytest.MonkeyPatch, account: Account, app_model: SimpleNamespace) -> None:
|
||||
monkeypatch.setattr(login_lib.dify_config, "LOGIN_DISABLED", True)
|
||||
monkeypatch.setattr(login_lib, "current_user", account)
|
||||
monkeypatch.setattr(login_lib, "current_account_with_tenant", lambda: (account, account.current_tenant_id))
|
||||
monkeypatch.setattr(login_lib, "check_csrf_token", lambda *_, **__: None)
|
||||
monkeypatch.setattr(console_wraps, "current_account_with_tenant", lambda: (account, account.current_tenant_id))
|
||||
monkeypatch.setattr(console_wraps.dify_config, "EDITION", "CLOUD")
|
||||
monkeypatch.setattr(app_wraps, "current_account_with_tenant", lambda: (account, account.current_tenant_id))
|
||||
monkeypatch.setattr(app_wraps, "_load_app_model", lambda _app_id: app_model)
|
||||
monkeypatch.setattr(workflow_comment_module, "current_user", account)
|
||||
|
||||
|
||||
def _patch_write_services(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
for method_name in (
|
||||
"create_comment",
|
||||
"update_comment",
|
||||
"delete_comment",
|
||||
"resolve_comment",
|
||||
"validate_comment_access",
|
||||
"create_reply",
|
||||
"update_reply",
|
||||
"delete_reply",
|
||||
):
|
||||
monkeypatch.setattr(workflow_comment_module.WorkflowCommentService, method_name, MagicMock())
|
||||
|
||||
|
||||
def _patch_payload(payload: dict[str, object] | None):
|
||||
if payload is None:
|
||||
return nullcontext()
|
||||
return patch.object(
|
||||
type(console_ns),
|
||||
"payload",
|
||||
new_callable=PropertyMock,
|
||||
return_value=payload,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WriteCase:
|
||||
resource_cls: type
|
||||
method_name: str
|
||||
path: str
|
||||
kwargs: dict[str, str]
|
||||
payload: dict[str, object] | None = None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"case",
|
||||
[
|
||||
WriteCase(
|
||||
resource_cls=workflow_comment_module.WorkflowCommentListApi,
|
||||
method_name="post",
|
||||
path="/console/api/apps/app-123/workflow/comments",
|
||||
kwargs={"app_id": "app-123"},
|
||||
payload={"content": "hello", "position_x": 1.0, "position_y": 2.0, "mentioned_user_ids": []},
|
||||
),
|
||||
WriteCase(
|
||||
resource_cls=workflow_comment_module.WorkflowCommentDetailApi,
|
||||
method_name="put",
|
||||
path="/console/api/apps/app-123/workflow/comments/comment-1",
|
||||
kwargs={"app_id": "app-123", "comment_id": "comment-1"},
|
||||
payload={"content": "hello", "position_x": 1.0, "position_y": 2.0, "mentioned_user_ids": []},
|
||||
),
|
||||
WriteCase(
|
||||
resource_cls=workflow_comment_module.WorkflowCommentDetailApi,
|
||||
method_name="delete",
|
||||
path="/console/api/apps/app-123/workflow/comments/comment-1",
|
||||
kwargs={"app_id": "app-123", "comment_id": "comment-1"},
|
||||
),
|
||||
WriteCase(
|
||||
resource_cls=workflow_comment_module.WorkflowCommentResolveApi,
|
||||
method_name="post",
|
||||
path="/console/api/apps/app-123/workflow/comments/comment-1/resolve",
|
||||
kwargs={"app_id": "app-123", "comment_id": "comment-1"},
|
||||
),
|
||||
WriteCase(
|
||||
resource_cls=workflow_comment_module.WorkflowCommentReplyApi,
|
||||
method_name="post",
|
||||
path="/console/api/apps/app-123/workflow/comments/comment-1/replies",
|
||||
kwargs={"app_id": "app-123", "comment_id": "comment-1"},
|
||||
payload={"content": "reply", "mentioned_user_ids": []},
|
||||
),
|
||||
WriteCase(
|
||||
resource_cls=workflow_comment_module.WorkflowCommentReplyDetailApi,
|
||||
method_name="put",
|
||||
path="/console/api/apps/app-123/workflow/comments/comment-1/replies/reply-1",
|
||||
kwargs={"app_id": "app-123", "comment_id": "comment-1", "reply_id": "reply-1"},
|
||||
payload={"content": "reply", "mentioned_user_ids": []},
|
||||
),
|
||||
WriteCase(
|
||||
resource_cls=workflow_comment_module.WorkflowCommentReplyDetailApi,
|
||||
method_name="delete",
|
||||
path="/console/api/apps/app-123/workflow/comments/comment-1/replies/reply-1",
|
||||
kwargs={"app_id": "app-123", "comment_id": "comment-1", "reply_id": "reply-1"},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_write_endpoints_require_edit_permission(
|
||||
app: Flask, monkeypatch: pytest.MonkeyPatch, case: WriteCase
|
||||
) -> None:
|
||||
app.config.setdefault("RESTX_MASK_HEADER", "X-Fields")
|
||||
account = _make_account(TenantAccountRole.NORMAL)
|
||||
app_model = _make_app()
|
||||
_patch_console_guards(monkeypatch, account, app_model)
|
||||
_patch_write_services(monkeypatch)
|
||||
|
||||
with app.test_request_context(case.path, method=case.method_name.upper(), json=case.payload):
|
||||
with _patch_payload(case.payload):
|
||||
handler = getattr(case.resource_cls(), case.method_name)
|
||||
with pytest.raises(Forbidden):
|
||||
handler(**case.kwargs)
|
||||
|
||||
|
||||
def test_create_comment_allows_editor(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
app.config.setdefault("RESTX_MASK_HEADER", "X-Fields")
|
||||
account = _make_account(TenantAccountRole.EDITOR)
|
||||
app_model = _make_app()
|
||||
_patch_console_guards(monkeypatch, account, app_model)
|
||||
|
||||
create_comment_mock = MagicMock(return_value={"id": "comment-1"})
|
||||
monkeypatch.setattr(workflow_comment_module.WorkflowCommentService, "create_comment", create_comment_mock)
|
||||
payload = {"content": "hello", "position_x": 1.0, "position_y": 2.0, "mentioned_user_ids": []}
|
||||
|
||||
with app.test_request_context("/console/api/apps/app-123/workflow/comments", method="POST", json=payload):
|
||||
with _patch_payload(payload):
|
||||
result = workflow_comment_module.WorkflowCommentListApi().post(app_id="app-123")
|
||||
|
||||
if isinstance(result, tuple):
|
||||
response = result[0]
|
||||
else:
|
||||
response = result
|
||||
assert response["id"] == "comment-1"
|
||||
create_comment_mock.assert_called_once_with(
|
||||
tenant_id="tenant-123",
|
||||
app_id="app-123",
|
||||
created_by="account-123",
|
||||
content="hello",
|
||||
position_x=1.0,
|
||||
position_y=2.0,
|
||||
mentioned_user_ids=[],
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user