decern 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- decern/__init__.py +7 -0
- decern/client.py +84 -0
- decern-0.1.1.dist-info/METADATA +58 -0
- decern-0.1.1.dist-info/RECORD +5 -0
- decern-0.1.1.dist-info/WHEEL +4 -0
decern/__init__.py
ADDED
decern/client.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""AuthZEN 1.0 Access Evaluation client for the decern PDP."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.request
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any, Dict, List, Optional, Union
|
|
11
|
+
|
|
12
|
+
Entity = Dict[str, Any] # {"type": ..., "id": ...}
|
|
13
|
+
Action = Union[str, Dict[str, Any]]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DecernError(Exception):
|
|
17
|
+
"""Transport failure or non-2xx response from the PDP."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Decision:
|
|
22
|
+
allowed: bool
|
|
23
|
+
reasons: List[str] = field(default_factory=list)
|
|
24
|
+
errors: List[str] = field(default_factory=list)
|
|
25
|
+
context: Optional[Dict[str, Any]] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Client:
|
|
29
|
+
"""Client for a decern PDP speaking AuthZEN 1.0 Access Evaluation."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, base_url: str = "http://127.0.0.1:8080", timeout: float = 5.0):
|
|
32
|
+
self.base_url = base_url.rstrip("/")
|
|
33
|
+
self.timeout = timeout
|
|
34
|
+
|
|
35
|
+
# --- HTTP ---------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
def _request(self, method: str, path: str, body: Optional[dict] = None) -> Any:
|
|
38
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
39
|
+
headers = {"Content-Type": "application/json"} if data else {}
|
|
40
|
+
req = urllib.request.Request(
|
|
41
|
+
self.base_url + path, data=data, headers=headers, method=method
|
|
42
|
+
)
|
|
43
|
+
try:
|
|
44
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
45
|
+
raw = resp.read().decode()
|
|
46
|
+
except urllib.error.HTTPError as e:
|
|
47
|
+
raise DecernError(f"{method} {path} -> {e.code} {e.reason}") from e
|
|
48
|
+
except urllib.error.URLError as e:
|
|
49
|
+
raise DecernError(f"{method} {path} failed: {e.reason}") from e
|
|
50
|
+
return raw
|
|
51
|
+
|
|
52
|
+
# --- API ----------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
def evaluate(
|
|
55
|
+
self,
|
|
56
|
+
subject: Entity,
|
|
57
|
+
action: Action,
|
|
58
|
+
resource: Entity,
|
|
59
|
+
context: Optional[Dict[str, Any]] = None,
|
|
60
|
+
) -> Decision:
|
|
61
|
+
act = {"name": action} if isinstance(action, str) else action
|
|
62
|
+
body: Dict[str, Any] = {"subject": subject, "action": act, "resource": resource}
|
|
63
|
+
if context is not None:
|
|
64
|
+
body["context"] = context
|
|
65
|
+
raw = self._request("POST", "/access/v1/evaluation", body)
|
|
66
|
+
resp = json.loads(raw)
|
|
67
|
+
ctx = resp.get("context") or None
|
|
68
|
+
reasons = list(ctx.get("reasons", [])) if ctx else []
|
|
69
|
+
errors = list(ctx.get("errors", [])) if ctx else []
|
|
70
|
+
return Decision(
|
|
71
|
+
allowed=bool(resp.get("decision", False)),
|
|
72
|
+
reasons=reasons,
|
|
73
|
+
errors=errors,
|
|
74
|
+
context=ctx,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def pubkey(self) -> str:
|
|
78
|
+
return json.loads(self._request("GET", "/pubkey"))["kid"]
|
|
79
|
+
|
|
80
|
+
def healthy(self) -> bool:
|
|
81
|
+
try:
|
|
82
|
+
return self._request("GET", "/healthz").strip() == "ok"
|
|
83
|
+
except DecernError:
|
|
84
|
+
return False
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: decern
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Dependency-free Python client for the decern PDP (AuthZEN 1.0 Access Evaluation).
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# decern Python SDK
|
|
10
|
+
|
|
11
|
+
Dependency-free Python client for the decern PDP, speaking
|
|
12
|
+
[AuthZEN 1.0](https://openid.net/specs/authorization-api-1_0.html) Access Evaluation.
|
|
13
|
+
Standard library only — no `requests`/`httpx`.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
pip install .
|
|
19
|
+
# or
|
|
20
|
+
uv pip install .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
Start the PDP:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
decern-serve
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from decern import Client
|
|
35
|
+
|
|
36
|
+
c = Client("http://127.0.0.1:8080")
|
|
37
|
+
|
|
38
|
+
d = c.evaluate(
|
|
39
|
+
subject={"type": "Principal", "id": "corp"},
|
|
40
|
+
action="Read", # or {"name": "Read"}
|
|
41
|
+
resource={"type": "Resource", "id": "claim1"},
|
|
42
|
+
context={"now": 100}, # optional; PDP injects `now` if omitted
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
print(d.allowed) # True / False
|
|
46
|
+
print(d.reasons) # e.g. ["policy0"] on allow
|
|
47
|
+
print(d.errors) # e.g. ["no_policy"] on deny
|
|
48
|
+
|
|
49
|
+
c.pubkey() # ed25519 public key id (hex)
|
|
50
|
+
c.healthy() # True if /healthz == "ok"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Test
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
cd sdks/python
|
|
57
|
+
python -m unittest discover -v
|
|
58
|
+
```
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
decern/__init__.py,sha256=Gpcs5x6J6daMPwQokXnKdjwmCheE9W5_4A_Q110Ka0U,239
|
|
2
|
+
decern/client.py,sha256=_K4VmOWRE_yOPDgZ8rf9K9hqXkofY_MVcL5xn60s4oY,2925
|
|
3
|
+
decern-0.1.1.dist-info/METADATA,sha256=iVoqx9WBPKpH3666SB1pFPcsFPoPNCnQISb4rWTtiHY,1155
|
|
4
|
+
decern-0.1.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
5
|
+
decern-0.1.1.dist-info/RECORD,,
|