Skip to content

Commit 04f7ab0

Browse files
tercelclaude
andcommitted
fix(acl): enforce list type for identity_types and roles condition values
Per spec, identity_types and roles condition values MUST be lists. _IdentityTypesHandler accepted bare strings (matching by equality), _RolesHandler crashed on non-iterable values like int. Both now return False for non-list input. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b92c941 commit 04f7ab0

4 files changed

Lines changed: 20 additions & 23 deletions

File tree

src/apcore/acl.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,4 +574,3 @@ def reload(self) -> None:
574574
# ---------------------------------------------------------------------------
575575
ACL.register_condition("$or", _OrHandler(ACL._evaluate_conditions))
576576
ACL.register_condition("$not", _NotHandler(ACL._evaluate_conditions))
577-

src/apcore/acl_handlers.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,24 +44,31 @@ async def evaluate(self, value: Any, context: Context) -> bool: ...
4444

4545

4646
class _IdentityTypesHandler:
47-
"""Check context.identity.type matches allowed value(s)."""
47+
"""Check context.identity.type matches allowed value(s).
48+
49+
Per spec, identity_types condition value MUST be a list.
50+
"""
4851

4952
def evaluate(self, value: Any, context: Context) -> bool:
5053
if context.identity is None:
5154
return False
52-
if isinstance(value, list):
53-
return context.identity.type in value
54-
return context.identity.type == value
55+
if not isinstance(value, list):
56+
return False
57+
return context.identity.type in value
5558

5659

5760
class _RolesHandler:
58-
"""Check role overlap between identity and required roles."""
61+
"""Check role overlap between identity and required roles.
62+
63+
Per spec, roles condition value MUST be a list.
64+
"""
5965

6066
def evaluate(self, value: Any, context: Context) -> bool:
6167
if context.identity is None:
6268
return False
63-
required = {value} if isinstance(value, str) else set(value)
64-
return bool(set(context.identity.roles) & required)
69+
if not isinstance(value, list):
70+
return False
71+
return bool(set(context.identity.roles) & set(value))
6572

6673

6774
class _MaxCallDepthHandler:

src/apcore/utils/normalize.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def _to_snake_case(segment: str) -> str:
5050
if i + 1 < len(segment) and segment[i + 1].islower():
5151
res.append("_")
5252
res.append(char.lower())
53-
53+
5454
return "".join(res).replace("__", "_")
5555

5656

tests/test_conformance.py

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from __future__ import annotations
1515

1616
import json
17-
import logging
1817
import os
1918
from pathlib import Path
2019
from typing import Any
@@ -123,8 +122,7 @@ def _cleanup_globals() -> Any:
123122
def test_pattern_matching(case: dict[str, Any]) -> None:
124123
result = match_pattern(case["pattern"], case["value"])
125124
assert result == case["expected"], (
126-
f"match_pattern({case['pattern']!r}, {case['value']!r}) "
127-
f"returned {result}, expected {case['expected']}"
125+
f"match_pattern({case['pattern']!r}, {case['value']!r}) " f"returned {result}, expected {case['expected']}"
128126
)
129127

130128

@@ -143,8 +141,7 @@ def test_pattern_matching(case: dict[str, Any]) -> None:
143141
def test_specificity(case: dict[str, Any]) -> None:
144142
score = calculate_specificity(case["pattern"])
145143
assert score == case["expected_score"], (
146-
f"calculate_specificity({case['pattern']!r}) "
147-
f"returned {score}, expected {case['expected_score']}"
144+
f"calculate_specificity({case['pattern']!r}) " f"returned {score}, expected {case['expected_score']}"
148145
)
149146

150147

@@ -396,10 +393,7 @@ def test_config_env(case: dict[str, Any], monkeypatch: pytest.MonkeyPatch) -> No
396393
result = str(result).lower()
397394
if expected is not None:
398395
expected = str(expected).lower()
399-
assert result == expected, (
400-
f"config.get({case['expected_path']!r}) = {result!r}, "
401-
f"expected {expected!r}"
402-
)
396+
assert result == expected, f"config.get({case['expected_path']!r}) = {result!r}, " f"expected {expected!r}"
403397

404398

405399
# ---------------------------------------------------------------------------
@@ -579,14 +573,11 @@ def test_schema_validation(
579573

580574
result = validator.validate(input_data, model)
581575
assert result.valid == expected_valid, (
582-
f"schema_validate({case['id']}) valid={result.valid}, "
583-
f"expected={expected_valid}, errors={result.errors}"
576+
f"schema_validate({case['id']}) valid={result.valid}, " f"expected={expected_valid}, errors={result.errors}"
584577
)
585578

586579
# Verify error path when expected
587580
if not expected_valid and "expected_error_path" in case:
588581
error_paths = [e.path for e in result.errors]
589582
expected_path = "/" + case["expected_error_path"].replace(".", "/").replace("[", "/").replace("]", "")
590-
assert any(expected_path in p for p in error_paths), (
591-
f"Expected error at {expected_path}, got {error_paths}"
592-
)
583+
assert any(expected_path in p for p in error_paths), f"Expected error at {expected_path}, got {error_paths}"

0 commit comments

Comments
 (0)