Skip to content

Commit ac4cbe2

Browse files
committed
fix(rls): reserve __tc namespace for dynamic schemas
1 parent f63407a commit ac4cbe2

File tree

5 files changed

+15
-14
lines changed

5 files changed

+15
-14
lines changed

alembic/versions/b5fc4168fe22_apply_model_a_rls_to_dynamic_workspace_.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""apply model a rls to dynamic workspace schemas
22
33
Revision ID: b5fc4168fe22
4-
Revises: c76f9b01fad7
4+
Revises: 6171727be56a
55
Create Date: 2026-02-27 11:43:07.059399
66
77
"""
@@ -19,7 +19,7 @@
1919

2020
# revision identifiers, used by Alembic.
2121
revision: str = "b5fc4168fe22"
22-
down_revision: str | None = "9a6d0e0ec5b1"
22+
down_revision: str | None = "6171727be56a"
2323
branch_labels: str | Sequence[str] | None = None
2424
depends_on: str | Sequence[str] | None = None
2525

tests/unit/test_case_fields_service.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -180,16 +180,14 @@ async def test_create_field_rejects_internal_name(
180180
CaseFieldCreate(name="__TC_workspace_id", type=SqlType.TEXT)
181181
)
182182

183-
async def test_create_field_allows_legacy_tc_prefix_name(
183+
async def test_create_field_rejects_internal_namespace_prefix(
184184
self, case_fields_service: CaseFieldsService
185185
) -> None:
186-
"""Only the exact internal tenant column name should be reserved."""
187-
with patch.object(
188-
case_fields_service.editor, "create_column"
189-
) as mock_create_column:
190-
field_params = CaseFieldCreate(name="__tc_shadow", type=SqlType.TEXT)
191-
await case_fields_service.create_field(field_params)
192-
mock_create_column.assert_called_once_with(field_params)
186+
"""The internal Tracecat namespace prefix should be reserved."""
187+
with pytest.raises(ValueError, match="reserved for internal use"):
188+
await case_fields_service.create_field(
189+
CaseFieldCreate(name="__tc_shadow", type=SqlType.TEXT)
190+
)
193191

194192
async def test_update_field(self, case_fields_service: CaseFieldsService) -> None:
195193
"""Test updating a case field."""

tests/unit/test_tables_service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,10 @@ async def _list_rows(
152152
@pytest.mark.anyio
153153
class TestTablesService:
154154
async def test_internal_column_name_check_is_case_insensitive(self) -> None:
155-
"""Internal column detection should normalize case for exact matches."""
155+
"""Internal column detection should normalize case for reserved namespaces."""
156156
assert is_internal_column_name("__tc_workspace_id") is True
157157
assert is_internal_column_name("__TC_workspace_id") is True
158-
assert is_internal_column_name("__tc_shadow") is False
158+
assert is_internal_column_name("__tc_shadow") is True
159159
assert is_internal_column_name("user_field") is False
160160

161161
async def test_create_and_get_table(self, tables_service: TablesService) -> None:

tracecat/cases/service.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1010,7 +1010,6 @@ class CaseFieldsService(CustomFieldsService):
10101010
"case_id",
10111011
"created_at",
10121012
"updated_at",
1013-
"workspace_id",
10141013
DYNAMIC_WORKSPACE_TENANT_COLUMN,
10151014
}
10161015

tracecat/tables/service.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
RLS_BYPASS_VAR = "app.rls_bypass"
7979
RLS_BYPASS_ON = "on"
8080
INTERNAL_COLUMN_NAMES: frozenset[str] = frozenset({DYNAMIC_WORKSPACE_TENANT_COLUMN})
81+
INTERNAL_COLUMN_PREFIX = "__tc_"
8182
SYSTEM_VISIBLE_COLUMN_NAMES: tuple[str, ...] = ("id", "created_at", "updated_at")
8283

8384

@@ -2240,4 +2241,7 @@ def sanitize_identifier(identifier: str) -> str:
22402241

22412242
def is_internal_column_name(column_name: str) -> bool:
22422243
"""Check whether a column is internal/system-managed for dynamic schemas."""
2243-
return column_name.lower() in INTERNAL_COLUMN_NAMES
2244+
normalized_name = column_name.lower()
2245+
return normalized_name in INTERNAL_COLUMN_NAMES or normalized_name.startswith(
2246+
INTERNAL_COLUMN_PREFIX
2247+
)

0 commit comments

Comments
 (0)