Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions apps/api/plane/api/serializers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
import random
from rest_framework import serializers


# Python imports
import re

# Module imports
from plane.db.models import Project, ProjectIdentifier, WorkspaceMember, State, Estimate

Expand Down Expand Up @@ -97,6 +101,15 @@ class Meta:
]

def validate(self, data):
project_name = data.get("name", None)
project_identifier = data.get("identifier", None)

if project_name is not None and re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, project_name):
raise serializers.ValidationError("Project name cannot contain special characters.")

if project_identifier is not None and re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, project_identifier):
raise serializers.ValidationError("Project identifier cannot contain special characters.")

if data.get("project_lead", None) is not None:
# Check if the project lead is a member of the workspace
if not WorkspaceMember.objects.filter(
Expand Down Expand Up @@ -156,6 +169,15 @@ class Meta(ProjectCreateSerializer.Meta):
read_only_fields = ProjectCreateSerializer.Meta.read_only_fields

def update(self, instance, validated_data):
project_name = validated_data.get("name", None)
project_identifier = validated_data.get("identifier", None)

if project_name is not None and re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, project_name):
raise serializers.ValidationError("Project name cannot contain special characters.")

if project_identifier is not None and re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, project_identifier):
raise serializers.ValidationError("Project identifier cannot contain special characters.")

"""Update a project"""
if (
validated_data.get("default_state", None) is not None
Expand Down Expand Up @@ -206,6 +228,15 @@ class Meta:
]

def validate(self, data):
project_name = data.get("name", None)
project_identifier = data.get("identifier", None)

if project_name is not None and re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, project_name):
raise serializers.ValidationError("Project name cannot contain special characters.")

if project_identifier is not None and re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, project_identifier):
raise serializers.ValidationError("Project identifier cannot contain special characters.")

# Check project lead should be a member of the workspace
if (
data.get("project_lead", None) is not None
Expand Down
9 changes: 9 additions & 0 deletions apps/api/plane/app/serializers/project.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Third party imports
from rest_framework import serializers

# Python imports
import re

# Module imports
from .base import BaseSerializer, DynamicBaseSerializer
from django.db.models import Max
Expand Down Expand Up @@ -33,6 +36,9 @@ def validate_name(self, name):
project_id = self.instance.id if self.instance else None
workspace_id = self.context["workspace_id"]

if re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, name):
raise serializers.ValidationError(detail="PROJECT_NAME_CANNOT_CONTAIN_SPECIAL_CHARACTERS")

project = Project.objects.filter(name=name, workspace_id=workspace_id)

if project_id:
Expand All @@ -49,6 +55,9 @@ def validate_identifier(self, identifier):
project_id = self.instance.id if self.instance else None
workspace_id = self.context["workspace_id"]

if re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, identifier):
raise serializers.ValidationError(detail="PROJECT_IDENTIFIER_CANNOT_CONTAIN_SPECIAL_CHARACTERS")

project = Project.objects.filter(identifier=identifier, workspace_id=workspace_id)

if project_id:
Expand Down
2 changes: 2 additions & 0 deletions apps/api/plane/db/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ def __str__(self):
"""Return name of the project"""
return f"{self.name} <{self.workspace.name}>"

FORBIDDEN_IDENTIFIER_CHARS_PATTERN = r"^.*[&+,:;$^}{*=?@#|'<>.()%!-].*$"

Comment on lines +139 to +140
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's read the file around lines 139-140
cat -n apps/api/plane/db/models/project.py | sed -n '130,150p'

Repository: makeplane/plane

Length of output: 895


🏁 Script executed:

# Let's search for any references to this pattern or forbidden characters
rg -i "forbidden.*char|identifier" apps/api/plane/db/models/ -A 3 -B 3

Repository: makeplane/plane

Length of output: 15923


🏁 Script executed:

# Look for tests related to this pattern
fd -e py -path "*/test*" | xargs rg -l "FORBIDDEN_IDENTIFIER_CHARS_PATTERN|forbidden.*char" 2>/dev/null

Repository: makeplane/plane

Length of output: 364


🏁 Script executed:

# Search for any usage of this pattern in the codebase
rg "FORBIDDEN_IDENTIFIER_CHARS_PATTERN" -A 2 -B 2

Repository: makeplane/plane

Length of output: 3775


🏁 Script executed:

# Search for tests related to forbidden chars pattern
rg -i "forbidden|special.*character" apps/api --type py -B 3 -A 3 | head -100

Repository: makeplane/plane

Length of output: 6471


🏁 Script executed:

# Look for PR description or requirements file
find . -type f \( -name "*.md" -o -name "*.txt" -o -name "REQUIREMENTS*" -o -name "PR*" \) | xargs grep -l "forbidden.*char\|identifier.*char" 2>/dev/null

Repository: makeplane/plane

Length of output: 43


🏁 Script executed:

# Check if there are any test files that validate the pattern
fd "test.*project" -e py | xargs rg -l "FORBIDDEN_IDENTIFIER_CHARS_PATTERN\|forbidden.*char" 2>/dev/null

Repository: makeplane/plane

Length of output: 43


🏁 Script executed:

# Search for any comments or documentation about which characters are allowed/forbidden
rg -i "allow.*hyphen|disallow.*hyphen|project.*identifier.*format" apps/api --type py

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Search for test files in various test directories
find . -type f -name "*.py" -path "*/test*" | head -20

Repository: makeplane/plane

Length of output: 986


🏁 Script executed:

# Look for any test that validates identifier or project name validation
rg "identifier.*test\|test.*identifier\|project.*name.*special\|hyphen" --type py -B 2 -A 2

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Check the serializer file more thoroughly to understand the validation logic
cat -n apps/api/plane/api/serializers/project.py | sed -n '1,150p'

Repository: makeplane/plane

Length of output: 5720


🏁 Script executed:

# Let's check if there are any git commits or diffs related to this pattern
git log --oneline --all -S "FORBIDDEN_IDENTIFIER_CHARS_PATTERN" | head -10

Repository: makeplane/plane

Length of output: 145


🏁 Script executed:

# Check if there's any documentation or comments about allowed characters
rg "project.*identifier\|identifier.*format" apps/api/plane/api/serializers/project.py -B 5 -A 5

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Search for test files specifically for project validation
fd "test.*project" --type f | xargs grep -l "validate\|FORBIDDEN" 2>/dev/null

Repository: makeplane/plane

Length of output: 112


🏁 Script executed:

# Look for any validation tests or examples with hyphens
rg "hyphen\|identifier.*-\|name.*-\|My-Project\|PROJ-" apps/api/plane/tests --type py -B 2 -A 2

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Check the app serializers too
cat -n apps/api/plane/app/serializers/project.py | head -200

Repository: makeplane/plane

Length of output: 8124


🏁 Script executed:

# Search for any comments or docstrings that explain identifier rules
rg "identifier.*must|identifier.*should|identifier.*allow|identifier.*contain" apps/api --type py -B 1 -A 1

Repository: makeplane/plane

Length of output: 1114


🏁 Script executed:

# Check the project test file
cat -n apps/api/plane/tests/contract/app/test_project_app.py | head -200

Repository: makeplane/plane

Length of output: 8951


🏁 Script executed:

# Look for any usage examples or comments mentioning hyphen or dashes
rg "identifier.*=\|name.*=" apps/api/plane/tests --type py -B 1 -A 1 | head -50

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Search the entire codebase for actual project identifiers to see patterns used
rg "identifier.*[:=]\s*['\"].*['\"]" apps/api --type py | head -20

Repository: makeplane/plane

Length of output: 2192


🏁 Script executed:

# Check if there are any migration files that might document the intent
fd "migration" --type f -name "*.py" | xargs grep -l "FORBIDDEN\|identifier\|special" 2>/dev/null

Repository: makeplane/plane

Length of output: 291


🏁 Script executed:

# Let's verify if the pattern is actually enforced by testing it directly
python3 << 'EOF'
import re

pattern = r"^.*[&+,:;$^}{*=?@#|'<>.()%!-].*$"

# Test cases found in the codebase
test_cases = [
    ("test-project", True),  # Should fail if hyphen is forbidden
    ("TEST-PROJECT", True),  # Uppercase version
    ("new-project", True),
    ("updated-project", True),
    ("NPT", False),  # All caps, no special chars
    ("PWL", False),
    ("test_project", False),  # Underscore not in forbidden list
    ("testproject", False),  # No special chars
    ("test@project", True),  # @ is forbidden
    ("test#project", True),  # # is forbidden
]

print("Pattern: " + pattern)
print("\nTest Results:")
for test_str, should_match in test_cases:
    result = re.match(pattern, test_str)
    matched = result is not None
    status = "✓" if matched == should_match else "✗ MISMATCH"
    print(f"  {status} '{test_str}': matched={matched}, expected={should_match}")
EOF

Repository: makeplane/plane

Length of output: 145


🏁 Script executed:

# Check when the FORBIDDEN_IDENTIFIER_CHARS_PATTERN was added
git log --all --full-history -S "FORBIDDEN_IDENTIFIER_CHARS_PATTERN" --oneline 2>/dev/null | head -5

Repository: makeplane/plane

Length of output: 145


🏁 Script executed:

# Check when the test with "test-project" identifier was added
git log --all --full-history -S "test-project" --oneline 2>/dev/null | head -5

Repository: makeplane/plane

Length of output: 145


🏁 Script executed:

# Let's check that test file more carefully to see if it actually expects hyphens to work
cat -n apps/api/plane/tests/unit/serializers/test_issue_recent_visit.py | grep -A 10 -B 5 "test-project"

Repository: makeplane/plane

Length of output: 789


🏁 Script executed:

# Check the openapi examples more closely
cat -n apps/api/plane/utils/openapi/examples.py | grep -A 5 -B 5 "new-project\|updated-project"

Repository: makeplane/plane

Length of output: 892


🏁 Script executed:

# Run a simple Python test to validate the regex behavior
python3 << 'EOF'
import re

# The actual pattern from the code
pattern = r"^.*[&+,:;$^}{*=?@#|'<>.()%!-].*$"

# Test identifiers found in the codebase
identifiers_with_hyphens = [
    "test-project",
    "new-project", 
    "updated-project",
]

print("Testing pattern:", pattern)
print("\nIdentifiers with hyphens (found in codebase):")
for identifier in identifiers_with_hyphens:
    matches = re.match(pattern, identifier)
    print(f"  '{identifier}': MATCHES pattern = {matches is not None}")
EOF

Repository: makeplane/plane

Length of output: 280


🏁 Script executed:

# Let's check if the serializer validation is actually applied in the test by looking for error assertions
rg "PROJECT_IDENTIFIER_CANNOT_CONTAIN_SPECIAL_CHARACTERS|special.*character" apps/api/plane/tests --type py -B 5 -A 5

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Let's see if there's any evidence of what the PR objectives actually said
git log --all --oneline | grep -i "forbidden\|identifier\|special" | head -10

Repository: makeplane/plane

Length of output: 1028


🏁 Script executed:

# Check git history for recent changes to the pattern or related code
git log --all --oneline -- apps/api/plane/db/models/project.py | head -10

Repository: makeplane/plane

Length of output: 741


🏁 Script executed:

# Let's verify the actual behavior by checking if these hyphenated identifiers would work
# in the serializer validation context
python3 << 'EOF'
import re

# The pattern in the code
pattern = r"^.*[&+,:;$^}{*=?@#|'<>.()%!-].*$"

# Examples found in the codebase
examples = {
    "test-project": "Test file identifier",
    "new-project": "OpenAPI example",
    "updated-project": "OpenAPI example",
    "NPT": "Test data (no hyphen)",
    "TEST": "Test data (no hyphen)",
}

print("Validation Results (re.match returns True = REJECTED by serializer):")
print("=" * 70)
for identifier, source in examples.items():
    matches = bool(re.match(pattern, identifier))
    status = "REJECTED" if matches else "ALLOWED"
    print(f"{status:10} | {identifier:20} | {source}")
EOF

Repository: makeplane/plane

Length of output: 475


Hyphen - is inadvertently included in forbidden characters pattern, conflicting with codebase examples.

The regex pattern forbids hyphen, but test data (test-project in test_issue_recent_visit.py) and OpenAPI examples (new-project, updated-project) use hyphenated identifiers. These would be rejected by serializer validation due to the pattern match.

Either remove hyphen from the pattern to allow hyphenated identifiers, or update the examples to use allowed formats.

Suggested fix if hyphen should be allowed
-    FORBIDDEN_IDENTIFIER_CHARS_PATTERN = r"^.*[&+,:;$^}{*=?@#|'<>.()%!-].*$"
+    FORBIDDEN_IDENTIFIER_CHARS_PATTERN = r"^.*[&+,:;$^}{*=?@#|'<>.()%!].*$"
🤖 Prompt for AI Agents
In @apps/api/plane/db/models/project.py around lines 139 - 140, The regex
constant FORBIDDEN_IDENTIFIER_CHARS_PATTERN currently forbids the hyphen
character (dash) which breaks existing examples/tests like
test_issue_recent_visit.py ("test-project") and OpenAPI examples ("new-project"
/ "updated-project"); update the pattern in FORBIDDEN_IDENTIFIER_CHARS_PATTERN
by removing the hyphen from the character class so hyphenated identifiers are
allowed (adjust the character class ordering/escaping if needed), then run
relevant serializer validation tests and update any examples only if you
intentionally want to disallow hyphens.

class Meta:
unique_together = [
["identifier", "workspace", "deleted_at"],
Expand Down
Loading