|
| 1 | +# Access Control System |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Pattern-based Access Control List (ACL) with first-match-wins evaluation for module access control. The system enforces which callers may invoke which target modules, using wildcard patterns, special identity patterns (`@external`, `@system`), and optional conditions based on identity type, roles, and call depth. Configuration can be loaded from YAML files and hot-reloaded at runtime. |
| 6 | + |
| 7 | +## Requirements |
| 8 | + |
| 9 | +- Implement first-match-wins rule evaluation: rules are evaluated in order, and the first rule whose patterns match the caller and target determines the access decision (allow or deny). |
| 10 | +- Support wildcard patterns for caller and target matching (e.g., `admin.*`, `*`), delegating to a shared pattern-matching utility. |
| 11 | +- Handle special patterns: `@external` matches calls with no caller (external entry points), and `@system` matches calls where the execution context has a system-type identity. |
| 12 | +- Support conditional rules with `identity_types` (identity type must be in list), `roles` (at least one role must overlap), and `max_call_depth` (call chain length must not exceed threshold). |
| 13 | +- Provide `default_effect` fallback (allow or deny) when no rule matches. |
| 14 | +- Load ACL configuration from YAML files via `ACL.load()`, with strict validation of structure and rule fields. |
| 15 | +- Support runtime rule management: `add_rule()` inserts at highest priority (position 0), `remove_rule()` removes by caller/target pattern match. |
| 16 | +- Support hot reload from the original YAML file via `reload()`. |
| 17 | +- All public methods must be thread-safe. |
| 18 | + |
| 19 | +## Technical Design |
| 20 | + |
| 21 | +### Architecture |
| 22 | + |
| 23 | +The ACL system consists of two primary components: the `ACLRule` dataclass representing individual rules, and the `ACL` class that manages a rule list and evaluates access decisions. |
| 24 | + |
| 25 | +#### Rule Evaluation |
| 26 | + |
| 27 | +``` |
| 28 | +check(caller_id, target_id, context) |
| 29 | + | |
| 30 | + +--> effective_caller = "@external" if caller_id is None else caller_id |
| 31 | + | |
| 32 | + +--> for each rule in rules (first-match-wins): |
| 33 | + | 1. Test caller patterns (OR logic: any pattern matching is sufficient) |
| 34 | + | 2. Test target patterns (OR logic) |
| 35 | + | 3. Test conditions (AND logic: all conditions must pass) |
| 36 | + | 4. If all pass -> return rule.effect == "allow" |
| 37 | + | |
| 38 | + +--> No rule matched -> return default_effect == "allow" |
| 39 | +``` |
| 40 | + |
| 41 | +#### Pattern Matching |
| 42 | + |
| 43 | +Pattern matching is handled at two levels: |
| 44 | +- **Special patterns** (`@external`, `@system`) are resolved directly in `ACL._match_pattern()` using caller identity and context. |
| 45 | +- **All other patterns** (exact strings, wildcard `*`, prefix wildcards like `executor.*`) are delegated to the foundation `match_pattern()` utility in `utils/pattern.py`, which implements Algorithm A08 with support for `*` wildcards matching any character sequence including dots. |
| 46 | + |
| 47 | +#### Conditional Rules |
| 48 | + |
| 49 | +When a rule has a `conditions` dict, all specified conditions must be satisfied (AND logic): |
| 50 | +- `identity_types`: Context identity's type must be in the provided list. |
| 51 | +- `roles`: At least one of the context identity's roles must overlap with the condition's role list (set intersection). |
| 52 | +- `max_call_depth`: The length of `context.call_chain` must not exceed the threshold. |
| 53 | + |
| 54 | +If no context is provided but conditions are present, the rule does not match. |
| 55 | + |
| 56 | +### Components |
| 57 | + |
| 58 | +- **`ACLRule`** -- Dataclass with fields: `callers` (list of patterns), `targets` (list of patterns), `effect` ("allow" or "deny"), optional `description`, and optional `conditions` dict. |
| 59 | +- **`ACL`** -- Main class managing an ordered rule list. Provides `check()`, `add_rule()`, `remove_rule()`, `reload()`, and the `ACL.load()` classmethod for YAML loading. All public methods are protected by `threading.Lock`. |
| 60 | +- **`match_pattern()`** -- Wildcard pattern matcher in `utils/pattern.py`. Supports `*` as a wildcard matching any character sequence. Handles prefix, suffix, and infix wildcards via segment splitting. |
| 61 | + |
| 62 | +### Thread Safety |
| 63 | + |
| 64 | +The `ACL` class uses an internal `threading.Lock` on all public methods. The `check()` method copies the rule list and default effect under the lock, then performs evaluation outside the lock. `add_rule()`, `remove_rule()`, and `reload()` all hold the lock for the duration of their mutations. |
| 65 | + |
| 66 | +### YAML Configuration Format |
| 67 | + |
| 68 | +```yaml |
| 69 | +version: "1.0" |
| 70 | +default_effect: deny |
| 71 | +rules: |
| 72 | + - callers: ["api.*"] |
| 73 | + targets: ["db.*"] |
| 74 | + effect: allow |
| 75 | + description: "API modules can access database modules" |
| 76 | + - callers: ["@external"] |
| 77 | + targets: ["public.*"] |
| 78 | + effect: allow |
| 79 | + - callers: ["*"] |
| 80 | + targets: ["admin.*"] |
| 81 | + effect: deny |
| 82 | + conditions: |
| 83 | + identity_types: ["service"] |
| 84 | + roles: ["admin"] |
| 85 | + max_call_depth: 5 |
| 86 | +``` |
| 87 | +
|
| 88 | +## Key Files |
| 89 | +
|
| 90 | +| File | Lines | Purpose | |
| 91 | +|------|-------|---------| |
| 92 | +| `src/apcore/acl.py` | 279 | `ACLRule` dataclass and `ACL` class with pattern matching, YAML loading, and runtime management | |
| 93 | +| `src/apcore/utils/pattern.py` | 46 | `match_pattern()` wildcard utility (Algorithm A08) | |
| 94 | + |
| 95 | +## Dependencies |
| 96 | + |
| 97 | +### Internal |
| 98 | +- `apcore.context.Context` -- Provides `identity`, `call_chain`, and other context fields for conditional rule evaluation. |
| 99 | +- `apcore.context.Identity` -- Dataclass with `id`, `type`, and `roles` fields used by `@system` pattern and condition checks. |
| 100 | +- `apcore.errors.ACLRuleError` -- Raised for invalid ACL configuration (bad YAML structure, missing keys, invalid effect values). |
| 101 | +- `apcore.errors.ConfigNotFoundError` -- Raised when the YAML file path does not exist. |
| 102 | +- `apcore.utils.pattern.match_pattern` -- Foundation wildcard matching for non-special patterns. |
| 103 | + |
| 104 | +### External |
| 105 | +- `yaml` (PyYAML) -- YAML parsing for configuration loading. |
| 106 | +- `threading` (stdlib) -- Lock for thread-safe access to the rule list. |
| 107 | +- `os` (stdlib) -- File existence checks in `ACL.load()`. |
| 108 | +- `logging` (stdlib) -- Debug-level logging of access decisions. |
| 109 | + |
| 110 | +## Testing Strategy |
| 111 | + |
| 112 | +### Unit Tests (`tests/test_acl.py`) |
| 113 | + |
| 114 | +- **Pattern matching**: Tests for `@external` matching None callers (and not matching string callers), `@system` matching system-type identities (and failing for None or non-system identities), exact patterns, wildcard `*`, and prefix wildcards like `executor.*`. |
| 115 | +- **First-match-wins evaluation**: Verifies that the first matching allow returns True, first matching deny returns False, and that rule order takes precedence over specificity. |
| 116 | +- **Default effect**: Tests both `default_effect="deny"` and `default_effect="allow"` when no rule matches. |
| 117 | +- **YAML loading**: Validates correct loading of rules with descriptions and conditions, and error handling for missing files (`ConfigNotFoundError`), invalid YAML, missing `rules` key, non-list `rules`, missing required keys (`callers`, `targets`, `effect`), invalid effect values, and non-list `callers`. |
| 118 | +- **Conditional rules**: Tests `identity_types` matching and failing, `roles` intersection matching and failing, `max_call_depth` within and exceeding limits, and conditions failing when context or identity is None. |
| 119 | +- **Runtime modification**: `add_rule()` inserts at position 0, `remove_rule()` returns True/False, `reload()` re-reads the YAML file and updates rules. |
| 120 | +- **Context interaction**: Verifies `caller_id=None` maps to `@external`, and context is forwarded to conditional evaluation. |
| 121 | +- **Thread safety**: Concurrent `check()` calls (10 threads x 200 iterations) with no errors, and concurrent `add_rule()` + `check()` with no corruption. |
| 122 | + |
| 123 | +### Integration Tests (`tests/integration/test_acl_enforcement.py`) |
| 124 | +- End-to-end tests exercising ACL enforcement through the `Executor` pipeline. |
0 commit comments