@@ -75,7 +75,14 @@ class ACL:
7575 remove_rule, reload) are safe to call concurrently.
7676 """
7777
78- _condition_handlers : ClassVar [dict [str , ACLConditionHandler ]] = {}
78+ _condition_handlers : ClassVar [dict [str , ACLConditionHandler ]] = {
79+ "identity_types" : _IdentityTypesHandler (),
80+ "identity_type" : _IdentityTypesHandler (),
81+ "roles" : _RolesHandler (),
82+ "role" : _RolesHandler (),
83+ "max_call_depth" : _MaxCallDepthHandler (),
84+ "call_depth" : _MaxCallDepthHandler (),
85+ }
7986
8087 @classmethod
8188 def register_condition (cls , key : str , handler : ACLConditionHandler ) -> None :
@@ -135,20 +142,21 @@ async def _evaluate_conditions_async(
135142
136143 def __init__ (
137144 self ,
138- rules : list [ACLRule ],
145+ rules : list [ACLRule ] | None = None ,
139146 default_effect : str = "deny" ,
140147 * ,
141148 audit_logger : Callable [[AuditEntry ], None ] | None = None ,
142149 ) -> None :
143150 """Initialize ACL with ordered rules and a default effect.
144151
145152 Args:
146- rules: Ordered list of ACL rules (first match wins).
153+ rules: Ordered list of ACL rules (first match wins). Defaults to [].
147154 default_effect: Effect when no rule matches ('allow' or 'deny').
148155 audit_logger: Optional callback invoked with an AuditEntry for
149156 every check() call. Useful for structured audit trails.
150157 """
151- self ._rules : list [ACLRule ] = list (rules )
158+ self ._rules = list (rules ) if rules is not None else []
159+
152160 self ._default_effect : str = default_effect
153161 self ._yaml_path : str | None = None
154162 self ._audit_logger : Callable [[AuditEntry ], None ] | None = audit_logger
@@ -253,7 +261,7 @@ def check(
253261 if self ._matches_rule (rule , effective_caller , target_id , context ):
254262 decision = rule .effect == "allow"
255263 self ._logger .debug (
256- "ACL check: caller =%s target =%s decision=%s rule=%s" ,
264+ "ACL check: caller_id =%s target_id =%s decision=%s rule=%s" ,
257265 caller_id ,
258266 target_id ,
259267 "allow" if decision else "deny" ,
@@ -274,7 +282,7 @@ def check(
274282
275283 default_decision = default_effect == "allow"
276284 self ._logger .debug (
277- "ACL check: caller =%s target =%s decision=%s rule=default" ,
285+ "ACL check: caller_id =%s target_id =%s decision=%s rule=default" ,
278286 caller_id ,
279287 target_id ,
280288 "allow" if default_decision else "deny" ,
@@ -320,7 +328,7 @@ async def async_check(
320328 if await self ._matches_rule_async (rule , effective_caller , target_id , context ):
321329 decision = rule .effect == "allow"
322330 self ._logger .debug (
323- "ACL async_check: caller =%s target =%s decision=%s rule=%s" ,
331+ "ACL async_check: caller_id =%s target_id =%s decision=%s rule=%s" ,
324332 caller_id ,
325333 target_id ,
326334 "allow" if decision else "deny" ,
@@ -341,7 +349,7 @@ async def async_check(
341349
342350 default_decision = default_effect == "allow"
343351 self ._logger .debug (
344- "ACL async_check: caller =%s target =%s decision=%s rule=default" ,
352+ "ACL async_check: caller_id =%s target_id =%s decision=%s rule=default" ,
345353 caller_id ,
346354 target_id ,
347355 "allow" if default_decision else "deny" ,
@@ -360,30 +368,50 @@ async def async_check(
360368 audit_logger (entry )
361369 return default_decision
362370
363- async def _matches_rule_async (
371+ def _match_patterns (self , patterns : list [str ], value : str , context : Context | None = None ) -> bool :
372+ """Match a list of patterns against a value.
373+
374+ Implements compound operators ($or, $not) in pattern lists.
375+ """
376+ if not patterns :
377+ return False
378+
379+ # Check for compound operators
380+ first = patterns [0 ]
381+ if first == "$or" :
382+ return any (self ._match_pattern (p , value , context ) for p in patterns [1 :])
383+ if first == "$not" :
384+ # $not expects exactly one subsequent pattern
385+ if len (patterns ) < 2 :
386+ return False
387+ return not self ._match_pattern (patterns [1 ], value , context )
388+
389+ # Standard OR behavior for flat list
390+ return any (self ._match_pattern (p , value , context ) for p in patterns )
391+
392+ def _matches_rule (
364393 self ,
365394 rule : ACLRule ,
366395 caller : str ,
367396 target : str ,
368397 context : Context | None ,
369398 ) -> bool :
370- """Async version of _matches_rule that awaits async condition handlers."""
371- caller_match = any (self ._match_pattern (p , caller , context ) for p in rule .callers )
372- if not caller_match :
399+ """Check if a rule matches the given caller, target, and context."""
400+ if not self ._match_patterns (rule .callers , caller , context ):
373401 return False
374402
375- target_match = any (self ._match_pattern (p , target , context ) for p in rule .targets )
376- if not target_match :
403+ if not self ._match_patterns (rule .targets , target , context ):
377404 return False
378405
379406 if rule .conditions is not None :
380407 if context is None :
381408 return False
382- if not await self ._evaluate_conditions_async (rule .conditions , context ):
409+ if not self ._evaluate_conditions (rule .conditions , context ):
383410 return False
384411
385412 return True
386413
414+
387415 def _build_audit_entry (
388416 self ,
389417 * ,
@@ -471,12 +499,41 @@ def _check_conditions(self, conditions: dict[str, Any], context: Context | None)
471499 return False
472500 return self ._evaluate_conditions (conditions , context )
473501
474- def add_rule (self , rule : ACLRule ) -> None :
502+ def add_rule (
503+ self ,
504+ rule : ACLRule | None = None ,
505+ * ,
506+ callers : list [str ] | str | None = None ,
507+ targets : list [str ] | str | None = None ,
508+ effect : str = "deny" ,
509+ description : str = "" ,
510+ conditions : dict [str , Any ] | None = None ,
511+ ) -> None :
475512 """Add a rule at position 0 (highest priority).
476513
477514 Args:
478- rule: The ACLRule to add.
515+ rule: Optional pre-built ACLRule.
516+ callers: Caller pattern(s) if *rule* is None.
517+ targets: Target pattern(s) if *rule* is None.
518+ effect: Rule effect if *rule* is None.
519+ description: Rule description if *rule* is None.
520+ conditions: Rule conditions if *rule* is None.
479521 """
522+ if rule is None :
523+ if callers is None or targets is None :
524+ raise ValueError ("Must provide either 'rule' or both 'callers' and 'targets'" )
525+
526+ def _to_list (v : list [str ] | str ) -> list [str ]:
527+ return [v ] if isinstance (v , str ) else list (v )
528+
529+ rule = ACLRule (
530+ callers = _to_list (callers ),
531+ targets = _to_list (targets ),
532+ effect = effect ,
533+ description = description ,
534+ conditions = conditions ,
535+ )
536+
480537 with self ._lock :
481538 self ._rules .insert (0 , rule )
482539
@@ -514,11 +571,8 @@ def reload(self) -> None:
514571
515572
516573# ---------------------------------------------------------------------------
517- # Auto-register built-in handlers at module load time
574+ # Auto-register compound operators at module load time
518575# ---------------------------------------------------------------------------
519-
520- ACL .register_condition ("identity_types" , _IdentityTypesHandler ())
521- ACL .register_condition ("roles" , _RolesHandler ())
522- ACL .register_condition ("max_call_depth" , _MaxCallDepthHandler ())
523576ACL .register_condition ("$or" , _OrHandler (ACL ._evaluate_conditions ))
524577ACL .register_condition ("$not" , _NotHandler (ACL ._evaluate_conditions ))
578+
0 commit comments