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 +85 -0
- decern-0.3.0/README.md +67 -0
- {decern-0.2.0 → decern-0.3.0}/decern/client.py +10 -1
- decern-0.3.0/pyproject.toml +30 -0
- {decern-0.2.0 → decern-0.3.0}/tests/test_client.py +19 -0
- decern-0.2.0/PKG-INFO +0 -56
- decern-0.2.0/README.md +0 -48
- decern-0.2.0/pyproject.toml +0 -16
- {decern-0.2.0 → decern-0.3.0}/.gitignore +0 -0
- {decern-0.2.0 → decern-0.3.0}/decern/__init__.py +0 -0
- {decern-0.2.0 → decern-0.3.0}/tests/__init__.py +0 -0
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
|
+
[](https://pypi.org/project/decern/)
|
|
22
|
+
[](https://github.com/anivar/decern/blob/main/LICENSE)
|
|
23
|
+
[](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
|
+
[](https://pypi.org/project/decern/)
|
|
4
|
+
[](https://github.com/anivar/decern/blob/main/LICENSE)
|
|
5
|
+
[](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__(
|
|
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
|
-
```
|
decern-0.2.0/pyproject.toml
DELETED
|
@@ -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
|