mirror of
https://github.com/makeplane/plane.git
synced 2026-07-09 12:00:38 +02:00
fix(api): require at least one alphanumeric char in workspace name (#9278)
* fix(api): require at least one alphanumeric char in workspace name
Workspace name validation was enforced only on the frontend
(validateWorkspaceName), which gates the UI submit but is bypassable
via a direct API call. The backend WorkSpaceSerializer.validate_name
only rejected URLs, so a symbol-only name like "-_________-" could
still be saved via create or the rename (partial_update) path.
Add a Unicode-aware has_alphanumeric() helper and enforce it in both
the app and instance/license workspace serializers, mirroring the
frontend HAS_ALPHANUMERIC_REGEX (/[\p{L}\p{N}]/u) added in #9263.
International names (日本語, José, محمد) still pass since str.isalnum()
covers all scripts.
Adds unit tests covering symbol-only rejection and international
acceptance on both serializers.
Refs #9255
Signed-off-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
* fix(api): reject URLs in instance workspace name for parity
Address CodeRabbit review on #9278: the instance/license
WorkspaceSerializer.validate_name rejected symbol-only names but, unlike
the app-level WorkSpaceSerializer, still accepted names containing URLs.
Add the same contains_url() guard (imported from plane.utils.url, not
content_validator) so both workspace-create paths validate identically.
Add unit tests asserting URL-containing names are rejected on both
serializers.
Signed-off-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
---------
Signed-off-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
This commit is contained in:
committed by
GitHub
parent
ad32dc7f70
commit
7b0704d9cc
@@ -31,6 +31,7 @@ from plane.utils.url import contains_url
|
||||
from plane.utils.content_validator import (
|
||||
validate_html_content,
|
||||
validate_binary_data,
|
||||
has_alphanumeric,
|
||||
)
|
||||
|
||||
# Django imports
|
||||
@@ -48,6 +49,13 @@ class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||
# Check if the name contains a URL
|
||||
if contains_url(value):
|
||||
raise serializers.ValidationError("Name must not contain URLs")
|
||||
# Reject symbol-only names like "-_________-" that have no letter or
|
||||
# digit. Mirrors the frontend HAS_ALPHANUMERIC_REGEX check so the rule
|
||||
# cannot be bypassed via a direct API call.
|
||||
if not has_alphanumeric(value):
|
||||
raise serializers.ValidationError(
|
||||
"Name must contain at least one letter or number"
|
||||
)
|
||||
return value
|
||||
|
||||
def validate_slug(self, value):
|
||||
|
||||
@@ -10,6 +10,8 @@ from .base import BaseSerializer
|
||||
from .user import UserLiteSerializer
|
||||
from plane.db.models import Workspace
|
||||
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
from plane.utils.content_validator import has_alphanumeric
|
||||
from plane.utils.url import contains_url
|
||||
|
||||
|
||||
class WorkspaceSerializer(BaseSerializer):
|
||||
@@ -18,6 +20,20 @@ class WorkspaceSerializer(BaseSerializer):
|
||||
total_projects = serializers.IntegerField(read_only=True)
|
||||
total_members = serializers.IntegerField(read_only=True)
|
||||
|
||||
def validate_name(self, value):
|
||||
# Check if the name contains a URL (kept consistent with the app-level
|
||||
# WorkSpaceSerializer so both workspace-create paths validate alike).
|
||||
if contains_url(value):
|
||||
raise serializers.ValidationError("Name must not contain URLs")
|
||||
# Reject symbol-only names like "-_________-" that have no letter or
|
||||
# digit. Mirrors the frontend HAS_ALPHANUMERIC_REGEX check so the rule
|
||||
# cannot be bypassed via a direct API call.
|
||||
if not has_alphanumeric(value):
|
||||
raise serializers.ValidationError(
|
||||
"Name must contain at least one letter or number"
|
||||
)
|
||||
return value
|
||||
|
||||
def validate_slug(self, value):
|
||||
# Check if the slug is restricted
|
||||
if value in RESTRICTED_WORKSPACE_SLUGS:
|
||||
|
||||
@@ -5,9 +5,34 @@
|
||||
import pytest
|
||||
from uuid import uuid4
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from plane.api.serializers import WorkspaceLiteSerializer
|
||||
from plane.app.serializers import WorkSpaceSerializer
|
||||
from plane.license.api.serializers import WorkspaceSerializer as InstanceWorkspaceSerializer
|
||||
from plane.db.models import Workspace, User
|
||||
|
||||
# Names with no letter or digit — must be rejected (issue #9255)
|
||||
SYMBOL_ONLY_NAMES = ["-_________-", "---", "___", "- - -", " ", " -_ "]
|
||||
|
||||
# Names containing at least one letter or digit — must be accepted, including
|
||||
# international scripts, so the rule does not regress non-Latin workspace names
|
||||
VALID_NAMES = [
|
||||
"Acme Corp",
|
||||
"Acme_Corp-123",
|
||||
"123",
|
||||
"a",
|
||||
"R&D",
|
||||
"José",
|
||||
"Müller GmbH",
|
||||
"日本語",
|
||||
"株式会社",
|
||||
"محمد",
|
||||
]
|
||||
|
||||
# Names embedding a URL — must be rejected on both create paths
|
||||
URL_NAMES = ["https://evil.com", "www.example.com", "example.com"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestWorkspaceLiteSerializer:
|
||||
@@ -52,3 +77,53 @@ class TestWorkspaceLiteSerializer:
|
||||
updated_workspace = serializer.save()
|
||||
assert updated_workspace.name == "Test Workspace"
|
||||
assert updated_workspace.slug == "test-workspace"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestWorkSpaceSerializerNameValidation:
|
||||
"""validate_name must reject symbol-only workspace names (issue #9255).
|
||||
|
||||
Frontend validation is bypassable via a direct API call, so the rule is
|
||||
enforced server-side on both the create and rename (partial_update) paths,
|
||||
which share this serializer's field-level validation.
|
||||
"""
|
||||
|
||||
@pytest.mark.parametrize("name", SYMBOL_ONLY_NAMES)
|
||||
def test_rejects_symbol_only_names(self, name):
|
||||
serializer = WorkSpaceSerializer()
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
serializer.validate_name(name)
|
||||
|
||||
@pytest.mark.parametrize("name", VALID_NAMES)
|
||||
def test_accepts_names_with_alphanumeric(self, name):
|
||||
serializer = WorkSpaceSerializer()
|
||||
assert serializer.validate_name(name) == name
|
||||
|
||||
@pytest.mark.parametrize("name", URL_NAMES)
|
||||
def test_rejects_names_containing_urls(self, name):
|
||||
serializer = WorkSpaceSerializer()
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
serializer.validate_name(name)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestInstanceWorkspaceSerializerNameValidation:
|
||||
"""The instance/license workspace create path must enforce the same rules
|
||||
as the app serializer (symbol-only rejection AND URL rejection)."""
|
||||
|
||||
@pytest.mark.parametrize("name", SYMBOL_ONLY_NAMES)
|
||||
def test_rejects_symbol_only_names(self, name):
|
||||
serializer = InstanceWorkspaceSerializer()
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
serializer.validate_name(name)
|
||||
|
||||
@pytest.mark.parametrize("name", VALID_NAMES)
|
||||
def test_accepts_names_with_alphanumeric(self, name):
|
||||
serializer = InstanceWorkspaceSerializer()
|
||||
assert serializer.validate_name(name) == name
|
||||
|
||||
@pytest.mark.parametrize("name", URL_NAMES)
|
||||
def test_rejects_names_containing_urls(self, name):
|
||||
serializer = InstanceWorkspaceSerializer()
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
serializer.validate_name(name)
|
||||
|
||||
@@ -241,3 +241,21 @@ def validate_html_content(html_content: str):
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return False, "Failed to sanitize HTML", None
|
||||
|
||||
|
||||
def has_alphanumeric(value):
|
||||
"""
|
||||
Check whether a string contains at least one alphanumeric character.
|
||||
|
||||
`str.isalnum()` is Unicode-aware, so letters and digits from any script
|
||||
(Latin, CJK, Arabic, Cyrillic, etc.) all count. This mirrors the frontend
|
||||
HAS_ALPHANUMERIC_REGEX (/[\\p{L}\\p{N}]/u) check and is used to reject
|
||||
symbol-only names such as "-_________-".
|
||||
|
||||
Args:
|
||||
value (str): The string to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the value contains at least one letter or digit.
|
||||
"""
|
||||
return any(char.isalnum() for char in (value or ""))
|
||||
|
||||
Reference in New Issue
Block a user