fix(api): validate workflow mentions against tenant members

This commit is contained in:
hjlarry 2026-04-12 16:23:19 +08:00
parent 3288f5e100
commit 977af3399e
2 changed files with 66 additions and 20 deletions

View File

@ -28,8 +28,10 @@ class WorkflowCommentService:
raise ValueError("Comment content cannot exceed 1000 characters")
@staticmethod
def _filter_valid_mentioned_user_ids(mentioned_user_ids: Sequence[str]) -> list[str]:
"""Return deduplicated UUID user IDs in the order provided."""
def _filter_valid_mentioned_user_ids(
mentioned_user_ids: Sequence[str], *, session: Session, tenant_id: str
) -> list[str]:
"""Return deduplicated UUID user IDs that belong to the tenant, preserving input order."""
unique_user_ids: list[str] = []
seen: set[str] = set()
for user_id in mentioned_user_ids:
@ -41,7 +43,20 @@ class WorkflowCommentService:
continue
seen.add(user_id)
unique_user_ids.append(user_id)
return unique_user_ids
if not unique_user_ids:
return []
tenant_member_ids = {
str(account_id)
for account_id in session.scalars(
select(TenantAccountJoin.account_id).where(
TenantAccountJoin.tenant_id == tenant_id,
TenantAccountJoin.account_id.in_(unique_user_ids),
)
).all()
}
return [user_id for user_id in unique_user_ids if user_id in tenant_member_ids]
@staticmethod
def _format_comment_excerpt(content: str, max_length: int = 200) -> str:
@ -220,7 +235,11 @@ class WorkflowCommentService:
session.flush() # Get the comment ID for mentions
# Create mentions if specified
mentioned_user_ids = WorkflowCommentService._filter_valid_mentioned_user_ids(mentioned_user_ids or [])
mentioned_user_ids = WorkflowCommentService._filter_valid_mentioned_user_ids(
mentioned_user_ids or [],
session=session,
tenant_id=tenant_id,
)
for user_id in mentioned_user_ids:
mention = WorkflowCommentMention(
comment_id=comment.id,
@ -293,7 +312,11 @@ class WorkflowCommentService:
session.delete(mention)
# Add new mentions
mentioned_user_ids = WorkflowCommentService._filter_valid_mentioned_user_ids(mentioned_user_ids or [])
mentioned_user_ids = WorkflowCommentService._filter_valid_mentioned_user_ids(
mentioned_user_ids or [],
session=session,
tenant_id=tenant_id,
)
new_mentioned_user_ids = [
user_id for user_id in mentioned_user_ids if user_id not in existing_mentioned_user_ids
]
@ -380,7 +403,11 @@ class WorkflowCommentService:
session.flush() # Get the reply ID for mentions
# Create mentions if specified
mentioned_user_ids = WorkflowCommentService._filter_valid_mentioned_user_ids(mentioned_user_ids or [])
mentioned_user_ids = WorkflowCommentService._filter_valid_mentioned_user_ids(
mentioned_user_ids or [],
session=session,
tenant_id=comment.tenant_id,
)
for user_id in mentioned_user_ids:
# Create mention linking to specific reply
mention = WorkflowCommentMention(comment_id=comment_id, reply_id=reply.id, mentioned_user_id=user_id)
@ -425,7 +452,15 @@ class WorkflowCommentService:
session.delete(mention)
# Add mentions
mentioned_user_ids = WorkflowCommentService._filter_valid_mentioned_user_ids(mentioned_user_ids or [])
raw_mentioned_user_ids = mentioned_user_ids or []
comment = session.get(WorkflowComment, reply.comment_id)
mentioned_user_ids = []
if comment:
mentioned_user_ids = WorkflowCommentService._filter_valid_mentioned_user_ids(
raw_mentioned_user_ids,
session=session,
tenant_id=comment.tenant_id,
)
new_mentioned_user_ids = [
user_id for user_id in mentioned_user_ids if user_id not in existing_mentioned_user_ids
]
@ -436,7 +471,6 @@ class WorkflowCommentService:
session.add(mention)
mention_email_payloads: list[dict[str, str]] = []
comment = session.get(WorkflowComment, reply.comment_id)
if comment:
mention_email_payloads = WorkflowCommentService._build_mention_email_payloads(
session=session,

View File

@ -39,20 +39,28 @@ class TestWorkflowCommentService:
with pytest.raises(ValueError):
WorkflowCommentService._validate_content("a" * 1001)
def test_filter_valid_mentioned_user_ids_deduplicates_and_preserves_order(self) -> None:
def test_filter_valid_mentioned_user_ids_filters_by_tenant_and_preserves_order(self, mock_session: Mock) -> None:
tenant_member_1 = "123e4567-e89b-12d3-a456-426614174000"
tenant_member_2 = "123e4567-e89b-12d3-a456-426614174002"
non_tenant_member = "123e4567-e89b-12d3-a456-426614174001"
mock_session.scalars.return_value = _mock_scalars([tenant_member_1, tenant_member_2])
result = WorkflowCommentService._filter_valid_mentioned_user_ids(
[
"123e4567-e89b-12d3-a456-426614174000",
tenant_member_1,
"",
123, # type: ignore[list-item]
"123e4567-e89b-12d3-a456-426614174000",
"123e4567-e89b-12d3-a456-426614174001",
]
tenant_member_1,
non_tenant_member,
tenant_member_2,
],
session=mock_session,
tenant_id="tenant-1",
)
assert result == [
"123e4567-e89b-12d3-a456-426614174000",
"123e4567-e89b-12d3-a456-426614174001",
tenant_member_1,
tenant_member_2,
]
def test_format_comment_excerpt_handles_short_and_long_limits(self) -> None:
@ -140,7 +148,7 @@ class TestWorkflowCommentService:
with (
patch.object(service_module, "WorkflowComment", return_value=comment),
patch.object(service_module, "WorkflowCommentMention", return_value=Mock()),
patch.object(service_module, "uuid_value", side_effect=[True, False]),
patch.object(WorkflowCommentService, "_filter_valid_mentioned_user_ids", return_value=["user-2"]),
):
result = WorkflowCommentService.create_comment(
tenant_id="tenant-1",
@ -192,7 +200,7 @@ class TestWorkflowCommentService:
existing_mentions = [Mock(), Mock()]
mock_session.scalars.return_value = _mock_scalars(existing_mentions)
with patch.object(service_module, "uuid_value", side_effect=[True, False]):
with patch.object(WorkflowCommentService, "_filter_valid_mentioned_user_ids", return_value=["user-2"]):
result = WorkflowCommentService.update_comment(
tenant_id="tenant-1",
app_id="app-1",
@ -218,7 +226,11 @@ class TestWorkflowCommentService:
mock_session.scalars.return_value = _mock_scalars([existing_mention])
with (
patch.object(service_module, "uuid_value", side_effect=[True, True]),
patch.object(
WorkflowCommentService,
"_filter_valid_mentioned_user_ids",
return_value=["user-2", "user-3"],
),
patch.object(
WorkflowCommentService,
"_build_mention_email_payloads",
@ -369,7 +381,7 @@ class TestWorkflowCommentService:
with (
patch.object(service_module, "WorkflowCommentReply", return_value=reply),
patch.object(service_module, "WorkflowCommentMention", return_value=Mock()),
patch.object(service_module, "uuid_value", side_effect=[True, False]),
patch.object(WorkflowCommentService, "_filter_valid_mentioned_user_ids", return_value=["user-2"]),
):
result = WorkflowCommentService.create_reply(
comment_id="comment-1",
@ -405,7 +417,7 @@ class TestWorkflowCommentService:
mock_session.get.return_value = reply
mock_session.scalars.return_value = _mock_scalars([Mock()])
with patch.object(service_module, "uuid_value", side_effect=[True, False]):
with patch.object(WorkflowCommentService, "_filter_valid_mentioned_user_ids", return_value=["user-2"]):
result = WorkflowCommentService.update_reply(
reply_id="reply-1",
user_id="owner",