1"""PermissionChecker — verifies a user has the required permissions to run a skill."""
2
3from __future__ import annotations
4
5from lexigram.logging import (
6 get_logger,
7)
8
9logger = get_logger(__name__)
10
11
12class PermissionChecker:
13 """Grant and verify per-user permission sets for skill execution.
14
15 Permissions are plain strings (e.g. ``"files.read"``, ``"db.query"``).
16 A user with no registered permissions is treated as having *no* permissions.
17
18 Example::
19
20 checker = PermissionChecker()
21 checker.grant("user-123", {"files.read", "web.search"})
22 allowed = checker.check("user-123", {"files.read"}) # True
23 denied = checker.check("user-123", {"db.write"}) # False
24 """
25
26 def __init__(self) -> None:
27 """Initialise an empty permission store."""
28 self._permissions: dict[str, set[str]] = {}
29
30 def grant(self, user_id: str, permissions: set[str]) -> None:
31 """Add permissions for a user.
32
33 Args:
34 user_id: The user identifier.
35 permissions: Set of permission strings to grant.
36 """
37 if user_id not in self._permissions:
38 self._permissions[user_id] = set()
39 self._permissions[user_id].update(permissions)
40 logger.debug("permissions_granted", user_id=user_id, count=len(permissions))
41
42 def revoke(self, user_id: str, permissions: set[str]) -> None:
43 """Remove permissions for a user.
44
45 Args:
46 user_id: The user identifier.
47 permissions: Set of permission strings to revoke.
48 """
49 if user_id in self._permissions:
50 self._permissions[user_id].difference_update(permissions)
51
52 def set_permissions(self, user_id: str, permissions: set[str]) -> None:
53 """Replace all permissions for a user with the given set.
54
55 Args:
56 user_id: The user identifier.
57 permissions: Complete replacement permission set.
58 """
59 self._permissions[user_id] = set(permissions)
60
61 def get_permissions(self, user_id: str) -> set[str]:
62 """Return the current permission set for a user.
63
64 Args:
65 user_id: The user identifier.
66
67 Returns:
68 Set of permission strings (may be empty).
69 """
70 return set(self._permissions.get(user_id, set()))
71
72 def check(self, user_id: str, required: set[str]) -> bool:
73 """Check whether a user possesses *all* required permissions.
74
75 Args:
76 user_id: The user to check.
77 required: Permissions that must all be present.
78
79 Returns:
80 ``True`` if every required permission is granted; ``False``
81 otherwise (including when *required* is empty — returns ``True``).
82 """
83 if not required:
84 return True
85 user_perms = self._permissions.get(user_id, set())
86 allowed = required.issubset(user_perms)
87 if not allowed:
88 missing = required - user_perms
89 logger.debug(
90 "permission_check_denied",
91 user_id=user_id,
92 missing=sorted(missing),
93 )
94 return allowed