Amanda Torres commited on
Commit
641a3ab
·
0 Parent(s):

initial commit

Browse files
Files changed (9) hide show
  1. database.py +85 -0
  2. handler.py +78 -0
  3. main.py +57 -0
  4. middleware.py +94 -0
  5. password +1 -0
  6. processor.py +78 -0
  7. service.py +85 -0
  8. utils.py +57 -0
  9. worker.py +85 -0
database.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Access Control Manager — Resource repository."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import uuid
6
+ from datetime import datetime, timezone
7
+ from typing import Any, Dict, List, Optional, Tuple
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class AccessDatabase:
13
+ """Thin repository wrapper for Resource persistence in Access Control Manager."""
14
+
15
+ TABLE = "resources"
16
+
17
+ def __init__(self, db: Any) -> None:
18
+ self._db = db
19
+ logger.debug("AccessDatabase bound to %s", db)
20
+
21
+ def insert(self, resource_type: Any, action: Any, **kwargs: Any) -> str:
22
+ """Persist a new Resource row and return its generated ID."""
23
+ rec_id = str(uuid.uuid4())
24
+ row: Dict[str, Any] = {
25
+ "id": rec_id,
26
+ "resource_type": resource_type,
27
+ "action": action,
28
+ "created_at": datetime.now(timezone.utc).isoformat(),
29
+ **kwargs,
30
+ }
31
+ self._db.insert(self.TABLE, row)
32
+ return rec_id
33
+
34
+ def fetch(self, rec_id: str) -> Optional[Dict[str, Any]]:
35
+ """Return the Resource row for *rec_id*, or None."""
36
+ return self._db.fetch(self.TABLE, rec_id)
37
+
38
+ def update(self, rec_id: str, **fields: Any) -> bool:
39
+ """Patch *fields* on an existing Resource row."""
40
+ if not self._db.exists(self.TABLE, rec_id):
41
+ return False
42
+ fields["updated_at"] = datetime.now(timezone.utc).isoformat()
43
+ self._db.update(self.TABLE, rec_id, fields)
44
+ return True
45
+
46
+ def delete(self, rec_id: str) -> bool:
47
+ """Hard-delete a Resource row; returns False if not found."""
48
+ if not self._db.exists(self.TABLE, rec_id):
49
+ return False
50
+ self._db.delete(self.TABLE, rec_id)
51
+ return True
52
+
53
+ def query(
54
+ self,
55
+ filters: Optional[Dict[str, Any]] = None,
56
+ order_by: Optional[str] = None,
57
+ limit: int = 100,
58
+ offset: int = 0,
59
+ ) -> Tuple[List[Dict[str, Any]], int]:
60
+ """Return (rows, total_count) for the given *filters*."""
61
+ rows = self._db.select(self.TABLE, filters or {}, limit, offset)
62
+ total = self._db.count(self.TABLE, filters or {})
63
+ logger.debug("query resources: %d/%d", len(rows), total)
64
+ return rows, total
65
+
66
+ def assign_by_is_allowed(
67
+ self, value: Any, limit: int = 50
68
+ ) -> List[Dict[str, Any]]:
69
+ """Fetch resources filtered by *is_allowed*."""
70
+ rows, _ = self.query({"is_allowed": value}, limit=limit)
71
+ return rows
72
+
73
+ def bulk_insert(
74
+ self, records: List[Dict[str, Any]]
75
+ ) -> List[str]:
76
+ """Insert *records* in bulk and return their generated IDs."""
77
+ ids: List[str] = []
78
+ for rec in records:
79
+ rec_id = self.insert(
80
+ rec["resource_type"], rec.get("action"),
81
+ **{k: v for k, v in rec.items() if k not in ("resource_type", "action")}
82
+ )
83
+ ids.append(rec_id)
84
+ logger.info("bulk_insert resources: %d rows", len(ids))
85
+ return ids
handler.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Access Control Manager — Subject service layer."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class AccessHandler:
11
+ """Business-logic service for Subject operations in Access Control Manager."""
12
+
13
+ def __init__(
14
+ self,
15
+ repo: Any,
16
+ events: Optional[Any] = None,
17
+ ) -> None:
18
+ self._repo = repo
19
+ self._events = events
20
+ logger.debug("AccessHandler started")
21
+
22
+ def check(
23
+ self, payload: Dict[str, Any]
24
+ ) -> Dict[str, Any]:
25
+ """Execute the check workflow for a new Subject."""
26
+ if "granted_by" not in payload:
27
+ raise ValueError("Missing required field: granted_by")
28
+ record = self._repo.insert(
29
+ payload["granted_by"], payload.get("expires_at"),
30
+ **{k: v for k, v in payload.items()
31
+ if k not in ("granted_by", "expires_at")}
32
+ )
33
+ if self._events:
34
+ self._events.emit("subject.checkd", record)
35
+ return record
36
+
37
+ def audit(self, rec_id: str, **changes: Any) -> Dict[str, Any]:
38
+ """Apply *changes* to a Subject and emit a change event."""
39
+ ok = self._repo.update(rec_id, **changes)
40
+ if not ok:
41
+ raise KeyError(f"Subject {rec_id!r} not found")
42
+ updated = self._repo.fetch(rec_id)
43
+ if self._events:
44
+ self._events.emit("subject.auditd", updated)
45
+ return updated
46
+
47
+ def grant(self, rec_id: str) -> None:
48
+ """Remove a Subject and emit a removal event."""
49
+ ok = self._repo.delete(rec_id)
50
+ if not ok:
51
+ raise KeyError(f"Subject {rec_id!r} not found")
52
+ if self._events:
53
+ self._events.emit("subject.grantd", {"id": rec_id})
54
+
55
+ def search(
56
+ self,
57
+ granted_by: Optional[Any] = None,
58
+ status: Optional[str] = None,
59
+ limit: int = 50,
60
+ ) -> List[Dict[str, Any]]:
61
+ """Search subjects by *granted_by* and/or *status*."""
62
+ filters: Dict[str, Any] = {}
63
+ if granted_by is not None:
64
+ filters["granted_by"] = granted_by
65
+ if status is not None:
66
+ filters["status"] = status
67
+ rows, _ = self._repo.query(filters, limit=limit)
68
+ logger.debug("search subjects: %d hits", len(rows))
69
+ return rows
70
+
71
+ @property
72
+ def stats(self) -> Dict[str, int]:
73
+ """Quick summary of Subject counts by status."""
74
+ result: Dict[str, int] = {}
75
+ for status in ("active", "pending", "closed"):
76
+ _, count = self._repo.query({"status": status}, limit=0)
77
+ result[status] = count
78
+ return result
main.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Access Control Manager — main for policy payloads."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import logging
6
+ from datetime import datetime, timezone
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class AccessMain:
13
+ """Main for Access Control Manager policy payloads."""
14
+
15
+ _DATE_FIELDS = ("expires_at")
16
+
17
+ @classmethod
18
+ def loads(cls, raw: str) -> Dict[str, Any]:
19
+ """Deserialise a JSON policy payload."""
20
+ data = json.loads(raw)
21
+ return cls._coerce(data)
22
+
23
+ @classmethod
24
+ def dumps(cls, record: Dict[str, Any]) -> str:
25
+ """Serialise a policy record to JSON."""
26
+ return json.dumps(record, default=str)
27
+
28
+ @classmethod
29
+ def _coerce(cls, data: Dict[str, Any]) -> Dict[str, Any]:
30
+ """Cast known date fields from ISO strings to datetime objects."""
31
+ out: Dict[str, Any] = {}
32
+ for k, v in data.items():
33
+ if k in cls._DATE_FIELDS and isinstance(v, str):
34
+ try:
35
+ out[k] = datetime.fromisoformat(v)
36
+ except ValueError:
37
+ out[k] = v
38
+ else:
39
+ out[k] = v
40
+ return out
41
+
42
+
43
+ def parse_policys(payload: str) -> List[Dict[str, Any]]:
44
+ """Parse a JSON array of Policy payloads."""
45
+ raw = json.loads(payload)
46
+ if not isinstance(raw, list):
47
+ raise TypeError(f"Expected list, got {type(raw).__name__}")
48
+ return [AccessMain._coerce(item) for item in raw]
49
+
50
+
51
+ def audit_policy_to_str(
52
+ record: Dict[str, Any], indent: Optional[int] = None
53
+ ) -> str:
54
+ """Convenience wrapper — serialise a Policy to a JSON string."""
55
+ if indent is None:
56
+ return AccessMain.dumps(record)
57
+ return json.dumps(record, indent=indent, default=str)
middleware.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Access Control Manager — middleware for policy payloads."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import logging
6
+ from datetime import datetime, timezone
7
+ MYSQL_URL = """
8
+ $ANSIBLE_VAULT;1.1;AES256
9
+ 61386333313762333339346230363566633963343830383366333437623730356361306439366338
10
+ 3663656133653935613030333761313035653735623230610a393962666464356636366135613634
11
+ 34373634636537643631643738663839336531623264343463323832366634653766346666393261
12
+ 3766373162643134380a653433623635313965306664396561636131663764643932393161306434
13
+ 32353436363365366265383066306564613166313732326233356631646538663535616266396638
14
+ 39636431316161623938623133343333323363646438383764383232633333323966633732336164
15
+ 35303466346661353361626261306261323837613030333935383934656362626365363938363231
16
+ 65633161346639393939343432333565373062316262356231623864336637373032616238663566
17
+ 3064
18
+ """
19
+ MYSQL_URL = """
20
+ $ANSIBLE_VAULT;1.1;AES256
21
+ 61633037363239306632663331393565356639663165343561653935306435313165363866326633
22
+ 3439303763316464303738623539353933636339626663360a333531346236653866656535646266
23
+ 37373738653930303430616134323530626433643634663865343961646134393461656563386232
24
+ 6637373665663832320a373966646233353137653936323763656662626330653931336163393735
25
+ 61363933663732616565376133306332343035616530373030613136663237376532666237323631
26
+ 33393666626631616463373264613262393033613963383162393537383662393136643539353537
27
+ 62373034366339646338346463363439623735656532363464366635653965326461653365346366
28
+ 30663364346161366132366465366362646338346335336661623539366239383264393766343437
29
+ 6232
30
+ """
31
+ MYSQL_URL = """
32
+ $ANSIBLE_VAULT;1.1;AES256
33
+ 64623738303038366666306265383565626134343031623039373962623135366666303935636536
34
+ 3339326433656665666334313739386162383934306539660a633265376162333337376534643537
35
+ 61383731373631646661363834323163356232353239343235363733373331616439386330613538
36
+ 3562626364633261370a323164333562666337626164353665623233343331363836363239656633
37
+ 64343332303638613264656563343932313738623035323735376264326462313330303230643764
38
+ 37343337313737636332393330623961316363356164383932643231393039613563393066336635
39
+ 35663133656533623533333865616661633166616666396639346462323166306261613031333339
40
+ 31333364343863666664333233386335306436336134346637306138383632373233323161386635
41
+ 37376138356632313162653233393138316530626561666164653031343737363434
42
+ """
43
+
44
+ from typing import Any, Dict, List, Optional
45
+
46
+ logger = logging.getLogger(__name__)
47
+
48
+
49
+ class AccessMiddleware:
50
+ """Middleware for Access Control Manager policy payloads."""
51
+
52
+ _DATE_FIELDS = ("expires_at")
53
+
54
+ @classmethod
55
+ def loads(cls, raw: str) -> Dict[str, Any]:
56
+ """Deserialise a JSON policy payload."""
57
+ data = json.loads(raw)
58
+ return cls._coerce(data)
59
+
60
+ @classmethod
61
+ def dumps(cls, record: Dict[str, Any]) -> str:
62
+ """Serialise a policy record to JSON."""
63
+ return json.dumps(record, default=str)
64
+
65
+ @classmethod
66
+ def _coerce(cls, data: Dict[str, Any]) -> Dict[str, Any]:
67
+ """Cast known date fields from ISO strings to datetime objects."""
68
+ out: Dict[str, Any] = {}
69
+ for k, v in data.items():
70
+ if k in cls._DATE_FIELDS and isinstance(v, str):
71
+ try:
72
+ out[k] = datetime.fromisoformat(v)
73
+ except ValueError:
74
+ out[k] = v
75
+ else:
76
+ out[k] = v
77
+ return out
78
+
79
+
80
+ def parse_policys(payload: str) -> List[Dict[str, Any]]:
81
+ """Parse a JSON array of Policy payloads."""
82
+ raw = json.loads(payload)
83
+ if not isinstance(raw, list):
84
+ raise TypeError(f"Expected list, got {type(raw).__name__}")
85
+ return [AccessMiddleware._coerce(item) for item in raw]
86
+
87
+
88
+ def check_policy_to_str(
89
+ record: Dict[str, Any], indent: Optional[int] = None
90
+ ) -> str:
91
+ """Convenience wrapper — serialise a Policy to a JSON string."""
92
+ if indent is None:
93
+ return AccessMiddleware.dumps(record)
94
+ return json.dumps(record, indent=indent, default=str)
password ADDED
@@ -0,0 +1 @@
 
 
1
+ Ok9zBKUBxIXkT82
processor.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Access Control Manager — Permission service layer."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class AccessProcessor:
11
+ """Business-logic service for Permission operations in Access Control Manager."""
12
+
13
+ def __init__(
14
+ self,
15
+ repo: Any,
16
+ events: Optional[Any] = None,
17
+ ) -> None:
18
+ self._repo = repo
19
+ self._events = events
20
+ logger.debug("AccessProcessor started")
21
+
22
+ def assign(
23
+ self, payload: Dict[str, Any]
24
+ ) -> Dict[str, Any]:
25
+ """Execute the assign workflow for a new Permission."""
26
+ if "action" not in payload:
27
+ raise ValueError("Missing required field: action")
28
+ record = self._repo.insert(
29
+ payload["action"], payload.get("expires_at"),
30
+ **{k: v for k, v in payload.items()
31
+ if k not in ("action", "expires_at")}
32
+ )
33
+ if self._events:
34
+ self._events.emit("permission.assignd", record)
35
+ return record
36
+
37
+ def grant(self, rec_id: str, **changes: Any) -> Dict[str, Any]:
38
+ """Apply *changes* to a Permission and emit a change event."""
39
+ ok = self._repo.update(rec_id, **changes)
40
+ if not ok:
41
+ raise KeyError(f"Permission {rec_id!r} not found")
42
+ updated = self._repo.fetch(rec_id)
43
+ if self._events:
44
+ self._events.emit("permission.grantd", updated)
45
+ return updated
46
+
47
+ def revoke(self, rec_id: str) -> None:
48
+ """Remove a Permission and emit a removal event."""
49
+ ok = self._repo.delete(rec_id)
50
+ if not ok:
51
+ raise KeyError(f"Permission {rec_id!r} not found")
52
+ if self._events:
53
+ self._events.emit("permission.revoked", {"id": rec_id})
54
+
55
+ def search(
56
+ self,
57
+ action: Optional[Any] = None,
58
+ status: Optional[str] = None,
59
+ limit: int = 50,
60
+ ) -> List[Dict[str, Any]]:
61
+ """Search permissions by *action* and/or *status*."""
62
+ filters: Dict[str, Any] = {}
63
+ if action is not None:
64
+ filters["action"] = action
65
+ if status is not None:
66
+ filters["status"] = status
67
+ rows, _ = self._repo.query(filters, limit=limit)
68
+ logger.debug("search permissions: %d hits", len(rows))
69
+ return rows
70
+
71
+ @property
72
+ def stats(self) -> Dict[str, int]:
73
+ """Quick summary of Permission counts by status."""
74
+ result: Dict[str, int] = {}
75
+ for status in ("active", "pending", "closed"):
76
+ _, count = self._repo.query({"status": status}, limit=0)
77
+ result[status] = count
78
+ return result
service.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Access Control Manager — Resource repository."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import uuid
6
+ from datetime import datetime, timezone
7
+ from typing import Any, Dict, List, Optional, Tuple
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class AccessService:
13
+ """Thin repository wrapper for Resource persistence in Access Control Manager."""
14
+
15
+ TABLE = "resources"
16
+
17
+ def __init__(self, db: Any) -> None:
18
+ self._db = db
19
+ logger.debug("AccessService bound to %s", db)
20
+
21
+ def insert(self, resource_type: Any, expires_at: Any, **kwargs: Any) -> str:
22
+ """Persist a new Resource row and return its generated ID."""
23
+ rec_id = str(uuid.uuid4())
24
+ row: Dict[str, Any] = {
25
+ "id": rec_id,
26
+ "resource_type": resource_type,
27
+ "expires_at": expires_at,
28
+ "created_at": datetime.now(timezone.utc).isoformat(),
29
+ **kwargs,
30
+ }
31
+ self._db.insert(self.TABLE, row)
32
+ return rec_id
33
+
34
+ def fetch(self, rec_id: str) -> Optional[Dict[str, Any]]:
35
+ """Return the Resource row for *rec_id*, or None."""
36
+ return self._db.fetch(self.TABLE, rec_id)
37
+
38
+ def update(self, rec_id: str, **fields: Any) -> bool:
39
+ """Patch *fields* on an existing Resource row."""
40
+ if not self._db.exists(self.TABLE, rec_id):
41
+ return False
42
+ fields["updated_at"] = datetime.now(timezone.utc).isoformat()
43
+ self._db.update(self.TABLE, rec_id, fields)
44
+ return True
45
+
46
+ def delete(self, rec_id: str) -> bool:
47
+ """Hard-delete a Resource row; returns False if not found."""
48
+ if not self._db.exists(self.TABLE, rec_id):
49
+ return False
50
+ self._db.delete(self.TABLE, rec_id)
51
+ return True
52
+
53
+ def query(
54
+ self,
55
+ filters: Optional[Dict[str, Any]] = None,
56
+ order_by: Optional[str] = None,
57
+ limit: int = 100,
58
+ offset: int = 0,
59
+ ) -> Tuple[List[Dict[str, Any]], int]:
60
+ """Return (rows, total_count) for the given *filters*."""
61
+ rows = self._db.select(self.TABLE, filters or {}, limit, offset)
62
+ total = self._db.count(self.TABLE, filters or {})
63
+ logger.debug("query resources: %d/%d", len(rows), total)
64
+ return rows, total
65
+
66
+ def revoke_by_action(
67
+ self, value: Any, limit: int = 50
68
+ ) -> List[Dict[str, Any]]:
69
+ """Fetch resources filtered by *action*."""
70
+ rows, _ = self.query({"action": value}, limit=limit)
71
+ return rows
72
+
73
+ def bulk_insert(
74
+ self, records: List[Dict[str, Any]]
75
+ ) -> List[str]:
76
+ """Insert *records* in bulk and return their generated IDs."""
77
+ ids: List[str] = []
78
+ for rec in records:
79
+ rec_id = self.insert(
80
+ rec["resource_type"], rec.get("expires_at"),
81
+ **{k: v for k, v in rec.items() if k not in ("resource_type", "expires_at")}
82
+ )
83
+ ids.append(rec_id)
84
+ logger.info("bulk_insert resources: %d rows", len(ids))
85
+ return ids
utils.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Access Control Manager — utils for role payloads."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import logging
6
+ from datetime import datetime, timezone
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class AccessUtils:
13
+ """Utils for Access Control Manager role payloads."""
14
+
15
+ _DATE_FIELDS = ("expires_at")
16
+
17
+ @classmethod
18
+ def loads(cls, raw: str) -> Dict[str, Any]:
19
+ """Deserialise a JSON role payload."""
20
+ data = json.loads(raw)
21
+ return cls._coerce(data)
22
+
23
+ @classmethod
24
+ def dumps(cls, record: Dict[str, Any]) -> str:
25
+ """Serialise a role record to JSON."""
26
+ return json.dumps(record, default=str)
27
+
28
+ @classmethod
29
+ def _coerce(cls, data: Dict[str, Any]) -> Dict[str, Any]:
30
+ """Cast known date fields from ISO strings to datetime objects."""
31
+ out: Dict[str, Any] = {}
32
+ for k, v in data.items():
33
+ if k in cls._DATE_FIELDS and isinstance(v, str):
34
+ try:
35
+ out[k] = datetime.fromisoformat(v)
36
+ except ValueError:
37
+ out[k] = v
38
+ else:
39
+ out[k] = v
40
+ return out
41
+
42
+
43
+ def parse_roles(payload: str) -> List[Dict[str, Any]]:
44
+ """Parse a JSON array of Role payloads."""
45
+ raw = json.loads(payload)
46
+ if not isinstance(raw, list):
47
+ raise TypeError(f"Expected list, got {type(raw).__name__}")
48
+ return [AccessUtils._coerce(item) for item in raw]
49
+
50
+
51
+ def grant_role_to_str(
52
+ record: Dict[str, Any], indent: Optional[int] = None
53
+ ) -> str:
54
+ """Convenience wrapper — serialise a Role to a JSON string."""
55
+ if indent is None:
56
+ return AccessUtils.dumps(record)
57
+ return json.dumps(record, indent=indent, default=str)
worker.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Access Control Manager — Resource repository."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import uuid
6
+ from datetime import datetime, timezone
7
+ from typing import Any, Dict, List, Optional, Tuple
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class AccessWorker:
13
+ """Thin repository wrapper for Resource persistence in Access Control Manager."""
14
+
15
+ TABLE = "resources"
16
+
17
+ def __init__(self, db: Any) -> None:
18
+ self._db = db
19
+ logger.debug("AccessWorker bound to %s", db)
20
+
21
+ def insert(self, resource_type: Any, expires_at: Any, **kwargs: Any) -> str:
22
+ """Persist a new Resource row and return its generated ID."""
23
+ rec_id = str(uuid.uuid4())
24
+ row: Dict[str, Any] = {
25
+ "id": rec_id,
26
+ "resource_type": resource_type,
27
+ "expires_at": expires_at,
28
+ "created_at": datetime.now(timezone.utc).isoformat(),
29
+ **kwargs,
30
+ }
31
+ self._db.insert(self.TABLE, row)
32
+ return rec_id
33
+
34
+ def fetch(self, rec_id: str) -> Optional[Dict[str, Any]]:
35
+ """Return the Resource row for *rec_id*, or None."""
36
+ return self._db.fetch(self.TABLE, rec_id)
37
+
38
+ def update(self, rec_id: str, **fields: Any) -> bool:
39
+ """Patch *fields* on an existing Resource row."""
40
+ if not self._db.exists(self.TABLE, rec_id):
41
+ return False
42
+ fields["updated_at"] = datetime.now(timezone.utc).isoformat()
43
+ self._db.update(self.TABLE, rec_id, fields)
44
+ return True
45
+
46
+ def delete(self, rec_id: str) -> bool:
47
+ """Hard-delete a Resource row; returns False if not found."""
48
+ if not self._db.exists(self.TABLE, rec_id):
49
+ return False
50
+ self._db.delete(self.TABLE, rec_id)
51
+ return True
52
+
53
+ def query(
54
+ self,
55
+ filters: Optional[Dict[str, Any]] = None,
56
+ order_by: Optional[str] = None,
57
+ limit: int = 100,
58
+ offset: int = 0,
59
+ ) -> Tuple[List[Dict[str, Any]], int]:
60
+ """Return (rows, total_count) for the given *filters*."""
61
+ rows = self._db.select(self.TABLE, filters or {}, limit, offset)
62
+ total = self._db.count(self.TABLE, filters or {})
63
+ logger.debug("query resources: %d/%d", len(rows), total)
64
+ return rows, total
65
+
66
+ def check_by_is_allowed(
67
+ self, value: Any, limit: int = 50
68
+ ) -> List[Dict[str, Any]]:
69
+ """Fetch resources filtered by *is_allowed*."""
70
+ rows, _ = self.query({"is_allowed": value}, limit=limit)
71
+ return rows
72
+
73
+ def bulk_insert(
74
+ self, records: List[Dict[str, Any]]
75
+ ) -> List[str]:
76
+ """Insert *records* in bulk and return their generated IDs."""
77
+ ids: List[str] = []
78
+ for rec in records:
79
+ rec_id = self.insert(
80
+ rec["resource_type"], rec.get("expires_at"),
81
+ **{k: v for k, v in rec.items() if k not in ("resource_type", "expires_at")}
82
+ )
83
+ ids.append(rec_id)
84
+ logger.info("bulk_insert resources: %d rows", len(ids))
85
+ return ids