decern 0.2.0__tar.gz → 0.3.0__tar.gz

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-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.5
2
+ Name: decern
3
+ Version: 0.3.0
4
+ Summary: Ask a decern server whether an action is allowed, and get a decision that is recorded to a tamper-evident log. Dependency-free AuthZEN 1.0 client.
5
+ Project-URL: Homepage, https://decern.anivar.net/
6
+ Project-URL: Source, https://github.com/anivar/decern
7
+ Project-URL: Issues, https://github.com/anivar/decern/issues
8
+ Project-URL: Changelog, https://github.com/anivar/decern/blob/main/CHANGELOG.md
9
+ Author: Anivar A Aravind
10
+ License-Expression: Apache-2.0
11
+ Keywords: access-control,ai-agents,audit,authorization,authzen,pdp
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Security
15
+ Classifier: Typing :: Typed
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+
19
+ # decern — Python client
20
+
21
+ [![PyPI](https://anivar.net/badge?src=pypi&name=decern)](https://pypi.org/project/decern/)
22
+ [![License](https://anivar.net/badge?label=license&value=Apache-2.0)](https://github.com/anivar/decern/blob/main/LICENSE)
23
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21848620.svg)](https://doi.org/10.5281/zenodo.21848620)
24
+
25
+ [Website](https://decern.anivar.net/) · [Repository](https://github.com/anivar/decern) ·
26
+ [Commands](https://github.com/anivar/decern/blob/main/docs/CLI.md) ·
27
+ [Issues](https://github.com/anivar/decern/issues)
28
+
29
+ Ask whether an action is allowed, and get an answer somebody can check afterwards.
30
+ Standard library only — no `requests`, no `httpx`. Python ≥ 3.11.
31
+
32
+ ```sh
33
+ uv add decern
34
+ # and a server to ask:
35
+ cargo install decern-server && decern-serve --trust-proxy
36
+ ```
37
+
38
+ ```python
39
+ from decern import Client
40
+
41
+ c = Client("http://127.0.0.1:8080") # optionally: token="…"
42
+
43
+ d = c.evaluate(
44
+ subject={"type": "Principal", "id": "corp"},
45
+ action="Read", # or {"name": "Read"}
46
+ resource={"type": "Resource", "id": "claim1"},
47
+ )
48
+
49
+ d.allowed # True / False
50
+ d.reasons # the policies that decided it, on allow
51
+ d.errors # why not, on deny
52
+ ```
53
+
54
+ When `token` is set it is sent as `Authorization: Bearer` on every request, for a
55
+ deployment that requires bearer tokens on the evaluation endpoint.
56
+
57
+ Also on the client: `c.pubkey()` (the Ed25519 key id the log is signed with) and
58
+ `c.healthy()`. A non-2xx response or transport failure raises `DecernError` with the HTTP
59
+ status and body, so a denial is distinguishable from a misconfigured endpoint. `context` is
60
+ advisory — the server overrides anything it derives itself (the clock, the accountable
61
+ owner), so a caller cannot talk its way into a decision by supplying them.
62
+
63
+ ## What the server gives you
64
+
65
+ [decern](https://github.com/anivar/decern) is an [AuthZEN
66
+ 1.0](https://openid.net/specs/authorization-api-1_0.html) authorization server whose safety
67
+ rules are machine-checked over every input, and whose decisions land in an append-only,
68
+ signed, hash-chained log **before** they are served — a decision that cannot be recorded is
69
+ refused, and a third party can verify what was decided without trusting the operator:
70
+
71
+ ```sh
72
+ decern verify --ledger <file> --pubkey <key> # the chain and every signature
73
+ decern explain --ledger <file> --seq 12 # one decision, in full
74
+ ```
75
+
76
+ Obtain the public key out of band; a key handed over by the party being audited
77
+ establishes nothing.
78
+
79
+ ## Test
80
+
81
+ ```sh
82
+ cd sdks/python && python -m unittest discover -v
83
+ ```
84
+
85
+ Apache-2.0. Published from CI by OIDC with no stored credential.
decern-0.3.0/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # decern — Python client
2
+
3
+ [![PyPI](https://anivar.net/badge?src=pypi&name=decern)](https://pypi.org/project/decern/)
4
+ [![License](https://anivar.net/badge?label=license&value=Apache-2.0)](https://github.com/anivar/decern/blob/main/LICENSE)
5
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21848620.svg)](https://doi.org/10.5281/zenodo.21848620)
6
+
7
+ [Website](https://decern.anivar.net/) · [Repository](https://github.com/anivar/decern) ·
8
+ [Commands](https://github.com/anivar/decern/blob/main/docs/CLI.md) ·
9
+ [Issues](https://github.com/anivar/decern/issues)
10
+
11
+ Ask whether an action is allowed, and get an answer somebody can check afterwards.
12
+ Standard library only — no `requests`, no `httpx`. Python ≥ 3.11.
13
+
14
+ ```sh
15
+ uv add decern
16
+ # and a server to ask:
17
+ cargo install decern-server && decern-serve --trust-proxy
18
+ ```
19
+
20
+ ```python
21
+ from decern import Client
22
+
23
+ c = Client("http://127.0.0.1:8080") # optionally: token="…"
24
+
25
+ d = c.evaluate(
26
+ subject={"type": "Principal", "id": "corp"},
27
+ action="Read", # or {"name": "Read"}
28
+ resource={"type": "Resource", "id": "claim1"},
29
+ )
30
+
31
+ d.allowed # True / False
32
+ d.reasons # the policies that decided it, on allow
33
+ d.errors # why not, on deny
34
+ ```
35
+
36
+ When `token` is set it is sent as `Authorization: Bearer` on every request, for a
37
+ deployment that requires bearer tokens on the evaluation endpoint.
38
+
39
+ Also on the client: `c.pubkey()` (the Ed25519 key id the log is signed with) and
40
+ `c.healthy()`. A non-2xx response or transport failure raises `DecernError` with the HTTP
41
+ status and body, so a denial is distinguishable from a misconfigured endpoint. `context` is
42
+ advisory — the server overrides anything it derives itself (the clock, the accountable
43
+ owner), so a caller cannot talk its way into a decision by supplying them.
44
+
45
+ ## What the server gives you
46
+
47
+ [decern](https://github.com/anivar/decern) is an [AuthZEN
48
+ 1.0](https://openid.net/specs/authorization-api-1_0.html) authorization server whose safety
49
+ rules are machine-checked over every input, and whose decisions land in an append-only,
50
+ signed, hash-chained log **before** they are served — a decision that cannot be recorded is
51
+ refused, and a third party can verify what was decided without trusting the operator:
52
+
53
+ ```sh
54
+ decern verify --ledger <file> --pubkey <key> # the chain and every signature
55
+ decern explain --ledger <file> --seq 12 # one decision, in full
56
+ ```
57
+
58
+ Obtain the public key out of band; a key handed over by the party being audited
59
+ establishes nothing.
60
+
61
+ ## Test
62
+
63
+ ```sh
64
+ cd sdks/python && python -m unittest discover -v
65
+ ```
66
+
67
+ Apache-2.0. Published from CI by OIDC with no stored credential.
@@ -48,15 +48,24 @@ class Decision:
48
48
  class Client:
49
49
  """Client for a decern PDP speaking AuthZEN 1.0 Access Evaluation."""
50
50
 
51
- def __init__(self, base_url: str = "http://127.0.0.1:8080", timeout: float = 5.0):
51
+ def __init__(
52
+ self,
53
+ base_url: str = "http://127.0.0.1:8080",
54
+ timeout: float = 5.0,
55
+ *,
56
+ token: str | None = None,
57
+ ):
52
58
  self.base_url = base_url.rstrip("/")
53
59
  self.timeout = timeout
60
+ self.token = token
54
61
 
55
62
  # --- HTTP ---------------------------------------------------------------
56
63
 
57
64
  def _request(self, method: str, path: str, body: Optional[dict] = None) -> Any:
58
65
  data = json.dumps(body).encode() if body is not None else None
59
66
  headers = {"Content-Type": "application/json"} if data else {}
67
+ if self.token is not None:
68
+ headers["Authorization"] = f"Bearer {self.token}"
60
69
  req = urllib.request.Request(
61
70
  self.base_url + path, data=data, headers=headers, method=method
62
71
  )
@@ -0,0 +1,30 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ [build-system]
3
+ requires = ["hatchling"]
4
+ build-backend = "hatchling.build"
5
+
6
+ [project]
7
+ name = "decern"
8
+ version = "0.3.0"
9
+ description = "Ask a decern server whether an action is allowed, and get a decision that is recorded to a tamper-evident log. Dependency-free AuthZEN 1.0 client."
10
+ readme = "README.md"
11
+ requires-python = ">=3.11"
12
+ license = "Apache-2.0"
13
+ dependencies = []
14
+ authors = [{ name = "Anivar A Aravind" }]
15
+ keywords = ["authorization", "authzen", "pdp", "access-control", "ai-agents", "audit"]
16
+ classifiers = [
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Security",
20
+ "Typing :: Typed",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://decern.anivar.net/"
25
+ Source = "https://github.com/anivar/decern"
26
+ Issues = "https://github.com/anivar/decern/issues"
27
+ Changelog = "https://github.com/anivar/decern/blob/main/CHANGELOG.md"
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["decern"]
@@ -103,6 +103,25 @@ class TestPubkeyHealth(unittest.TestCase):
103
103
  self.assertFalse(c.healthy())
104
104
 
105
105
 
106
+ class TestBearerToken(unittest.TestCase):
107
+ @staticmethod
108
+ def _sent_request(client):
109
+ """Run one evaluate() through urllib and return the outgoing Request."""
110
+ resp = mock.MagicMock()
111
+ resp.__enter__.return_value.read.return_value = b'{"decision": true}'
112
+ with mock.patch("urllib.request.urlopen", return_value=resp) as urlopen:
113
+ client.evaluate({"id": "a"}, "Read", {"id": "b"})
114
+ return urlopen.call_args[0][0]
115
+
116
+ def test_token_sent_as_bearer_header(self):
117
+ req = self._sent_request(Client(token="tok123"))
118
+ self.assertEqual(req.get_header("Authorization"), "Bearer tok123")
119
+
120
+ def test_no_token_no_authorization_header(self):
121
+ req = self._sent_request(Client())
122
+ self.assertFalse(req.has_header("Authorization"))
123
+
124
+
106
125
  class TestHttpError(unittest.TestCase):
107
126
  def test_http_error_attaches_status_and_body(self):
108
127
  import io
decern-0.2.0/PKG-INFO DELETED
@@ -1,56 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: decern
3
- Version: 0.2.0
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
- uv pip install .
19
- ```
20
-
21
- ## Usage
22
-
23
- Start the PDP:
24
-
25
- ```sh
26
- decern-serve
27
- ```
28
-
29
- Then:
30
-
31
- ```python
32
- from decern import Client
33
-
34
- c = Client("http://127.0.0.1:8080")
35
-
36
- d = c.evaluate(
37
- subject={"type": "Principal", "id": "corp"},
38
- action="Read", # or {"name": "Read"}
39
- resource={"type": "Resource", "id": "claim1"},
40
- context={"now": 100}, # optional; PDP injects `now` if omitted
41
- )
42
-
43
- print(d.allowed) # True / False
44
- print(d.reasons) # e.g. ["policy0"] on allow
45
- print(d.errors) # e.g. ["no_policy"] on deny
46
-
47
- c.pubkey() # ed25519 public key id (hex)
48
- c.healthy() # True if /healthz == "ok"
49
- ```
50
-
51
- ## Test
52
-
53
- ```sh
54
- cd sdks/python
55
- python -m unittest discover -v
56
- ```
decern-0.2.0/README.md DELETED
@@ -1,48 +0,0 @@
1
- # decern Python SDK
2
-
3
- Dependency-free Python client for the decern PDP, speaking
4
- [AuthZEN 1.0](https://openid.net/specs/authorization-api-1_0.html) Access Evaluation.
5
- Standard library only — no `requests`/`httpx`.
6
-
7
- ## Install
8
-
9
- ```sh
10
- uv pip install .
11
- ```
12
-
13
- ## Usage
14
-
15
- Start the PDP:
16
-
17
- ```sh
18
- decern-serve
19
- ```
20
-
21
- Then:
22
-
23
- ```python
24
- from decern import Client
25
-
26
- c = Client("http://127.0.0.1:8080")
27
-
28
- d = c.evaluate(
29
- subject={"type": "Principal", "id": "corp"},
30
- action="Read", # or {"name": "Read"}
31
- resource={"type": "Resource", "id": "claim1"},
32
- context={"now": 100}, # optional; PDP injects `now` if omitted
33
- )
34
-
35
- print(d.allowed) # True / False
36
- print(d.reasons) # e.g. ["policy0"] on allow
37
- print(d.errors) # e.g. ["no_policy"] on deny
38
-
39
- c.pubkey() # ed25519 public key id (hex)
40
- c.healthy() # True if /healthz == "ok"
41
- ```
42
-
43
- ## Test
44
-
45
- ```sh
46
- cd sdks/python
47
- python -m unittest discover -v
48
- ```
@@ -1,16 +0,0 @@
1
- # SPDX-License-Identifier: Apache-2.0
2
- [build-system]
3
- requires = ["hatchling"]
4
- build-backend = "hatchling.build"
5
-
6
- [project]
7
- name = "decern"
8
- version = "0.2.0"
9
- description = "Dependency-free Python client for the decern PDP (AuthZEN 1.0 Access Evaluation)."
10
- readme = "README.md"
11
- requires-python = ">=3.11"
12
- license = "Apache-2.0"
13
- dependencies = []
14
-
15
- [tool.hatch.build.targets.wheel]
16
- packages = ["decern"]
File without changes
File without changes
File without changes