hestia-keyring 1.0.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.
- hestia_keyring-1.0.0/.gitignore +43 -0
- hestia_keyring-1.0.0/PKG-INFO +53 -0
- hestia_keyring-1.0.0/README.md +44 -0
- hestia_keyring-1.0.0/azure-pipelines.yml +21 -0
- hestia_keyring-1.0.0/pyproject.toml +30 -0
- hestia_keyring-1.0.0/src/hestia_keyring/__init__.py +52 -0
- hestia_keyring-1.0.0/tests/__init__.py +0 -0
- hestia_keyring-1.0.0/tests/unit/__init__.py +0 -0
- hestia_keyring-1.0.0/tests/unit/test_backend.py +88 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
.pytest_cache/
|
|
7
|
+
.mypy_cache/
|
|
8
|
+
.ruff_cache/
|
|
9
|
+
.coverage
|
|
10
|
+
htmlcov/
|
|
11
|
+
|
|
12
|
+
# uv
|
|
13
|
+
.venv/
|
|
14
|
+
|
|
15
|
+
# IDE
|
|
16
|
+
.vscode/
|
|
17
|
+
.idea/
|
|
18
|
+
*.swp
|
|
19
|
+
|
|
20
|
+
# Templates: include scaffold-emitted .vscode (they ship recommended editor settings).
|
|
21
|
+
!packages/hestia-templates/**/.vscode/
|
|
22
|
+
!packages/hestia-templates/**/.vscode/**
|
|
23
|
+
|
|
24
|
+
# OS
|
|
25
|
+
.DS_Store
|
|
26
|
+
Thumbs.db
|
|
27
|
+
|
|
28
|
+
# Local dev
|
|
29
|
+
.env
|
|
30
|
+
.env.local
|
|
31
|
+
.hestia/
|
|
32
|
+
docker-compose.override.yaml
|
|
33
|
+
|
|
34
|
+
# Parallel sub-plan worktrees
|
|
35
|
+
.worktrees/
|
|
36
|
+
|
|
37
|
+
# mkdocs build output
|
|
38
|
+
site/
|
|
39
|
+
|
|
40
|
+
# Local Claude Code skill cache (machine-specific, not project state)
|
|
41
|
+
.claude/
|
|
42
|
+
.others/
|
|
43
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hestia-keyring
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Keyring backend for HES Azure Artifacts — enables uv sync without PATs
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.13
|
|
7
|
+
Requires-Dist: keyring>=24.0
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# hestia-keyring
|
|
11
|
+
|
|
12
|
+
A [keyring](https://pypi.org/project/keyring/) backend that authenticates `uv sync` against
|
|
13
|
+
the HES Azure Artifacts feed using your active `az` CLI session — no Personal Access Tokens
|
|
14
|
+
required.
|
|
15
|
+
|
|
16
|
+
## Install once per machine
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv tool install keyring --with hestia-keyring
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## What it does
|
|
23
|
+
|
|
24
|
+
When `uv` calls `keyring get https://pkgs.dev.azure.com/... VssSessionToken`, this backend
|
|
25
|
+
runs `az account get-access-token --resource https://app.vssps.visualstudio.com` and returns
|
|
26
|
+
the resulting bearer token. Your `az login` session is the only credential you need.
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- Python 3.13+
|
|
31
|
+
- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) installed and
|
|
32
|
+
signed in (`az login`)
|
|
33
|
+
|
|
34
|
+
## Consumer app configuration
|
|
35
|
+
|
|
36
|
+
Add to your app's `pyproject.toml`:
|
|
37
|
+
|
|
38
|
+
```toml
|
|
39
|
+
[[tool.uv.index]]
|
|
40
|
+
name = "hes-internal"
|
|
41
|
+
url = "https://VssSessionToken@pkgs.dev.azure.com/hmc-heerema/HES%20Internal/_packaging/HES/pypi/simple/"
|
|
42
|
+
default = false
|
|
43
|
+
explicit = true
|
|
44
|
+
|
|
45
|
+
[tool.uv.sources]
|
|
46
|
+
hestia = { index = "hes-internal" }
|
|
47
|
+
|
|
48
|
+
[tool.uv]
|
|
49
|
+
keyring-provider = "subprocess"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The `VssSessionToken@` prefix in the URL is required — uv passes the URL username to
|
|
53
|
+
`keyring get`, and `VssSessionToken` is the username this backend recognises.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# hestia-keyring
|
|
2
|
+
|
|
3
|
+
A [keyring](https://pypi.org/project/keyring/) backend that authenticates `uv sync` against
|
|
4
|
+
the HES Azure Artifacts feed using your active `az` CLI session — no Personal Access Tokens
|
|
5
|
+
required.
|
|
6
|
+
|
|
7
|
+
## Install once per machine
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv tool install keyring --with hestia-keyring
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## What it does
|
|
14
|
+
|
|
15
|
+
When `uv` calls `keyring get https://pkgs.dev.azure.com/... VssSessionToken`, this backend
|
|
16
|
+
runs `az account get-access-token --resource https://app.vssps.visualstudio.com` and returns
|
|
17
|
+
the resulting bearer token. Your `az login` session is the only credential you need.
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- Python 3.13+
|
|
22
|
+
- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) installed and
|
|
23
|
+
signed in (`az login`)
|
|
24
|
+
|
|
25
|
+
## Consumer app configuration
|
|
26
|
+
|
|
27
|
+
Add to your app's `pyproject.toml`:
|
|
28
|
+
|
|
29
|
+
```toml
|
|
30
|
+
[[tool.uv.index]]
|
|
31
|
+
name = "hes-internal"
|
|
32
|
+
url = "https://VssSessionToken@pkgs.dev.azure.com/hmc-heerema/HES%20Internal/_packaging/HES/pypi/simple/"
|
|
33
|
+
default = false
|
|
34
|
+
explicit = true
|
|
35
|
+
|
|
36
|
+
[tool.uv.sources]
|
|
37
|
+
hestia = { index = "hes-internal" }
|
|
38
|
+
|
|
39
|
+
[tool.uv]
|
|
40
|
+
keyring-provider = "subprocess"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The `VssSessionToken@` prefix in the URL is required — uv passes the URL username to
|
|
44
|
+
`keyring get`, and `VssSessionToken` is the username this backend recognises.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# packages/hestia-keyring/azure-pipelines.yml
|
|
2
|
+
#
|
|
3
|
+
# CI for hestia-keyring. Triggers on:
|
|
4
|
+
# - Pushes to main (runs Install + Verify; no Release)
|
|
5
|
+
# - Tags matching hestia-keyring@X.Y.Z (runs full pipeline including PyPI publish)
|
|
6
|
+
#
|
|
7
|
+
# IMPORTANT: This pipeline publishes to PUBLIC PyPI, not ADO Artifacts.
|
|
8
|
+
# Do NOT change the template reference to uv-python-package.yml — that
|
|
9
|
+
# would publish to ADO Artifacts. This package must remain on public PyPI.
|
|
10
|
+
|
|
11
|
+
trigger:
|
|
12
|
+
branches: { include: [main] }
|
|
13
|
+
tags: { include: ['hestia-keyring@*'] }
|
|
14
|
+
|
|
15
|
+
pr:
|
|
16
|
+
branches: { include: [main] }
|
|
17
|
+
|
|
18
|
+
extends:
|
|
19
|
+
template: ../hestia-pipelines/templates/uv-python-package-pypi.yml
|
|
20
|
+
parameters:
|
|
21
|
+
packageName: hestia-keyring
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hestia-keyring"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Keyring backend for HES Azure Artifacts — enables uv sync without PATs"
|
|
5
|
+
requires-python = ">=3.13"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
dependencies = [
|
|
9
|
+
"keyring>=24.0",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.entry-points."keyring.backends"]
|
|
13
|
+
hestia = "hestia_keyring:HestiaArtifactsKeyring"
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["hatchling"]
|
|
17
|
+
build-backend = "hatchling.build"
|
|
18
|
+
|
|
19
|
+
[tool.hatch.build.targets.wheel]
|
|
20
|
+
packages = ["src/hestia_keyring"]
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = [
|
|
24
|
+
"pytest>=8.3",
|
|
25
|
+
"pytest-cov>=5.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[tool.pytest.ini_options]
|
|
29
|
+
testpaths = ["tests"]
|
|
30
|
+
addopts = "-ra --strict-markers"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import subprocess
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
from keyring.backend import KeyringBackend
|
|
6
|
+
from keyring.credentials import SimpleCredential
|
|
7
|
+
|
|
8
|
+
_AZ = shutil.which("az") or "az"
|
|
9
|
+
_FEED_HOST = "pkgs.dev.azure.com"
|
|
10
|
+
_RESOURCE = "https://app.vssps.visualstudio.com"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HestiaArtifactsKeyring(KeyringBackend):
|
|
14
|
+
"""Keyring backend for HES Azure Artifacts feeds.
|
|
15
|
+
|
|
16
|
+
Uses the active az CLI session — no PATs, no tenant discovery.
|
|
17
|
+
Install once: uv tool install keyring --with hestia-keyring
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
priority = 9
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def viable(cls) -> bool:
|
|
24
|
+
return shutil.which("az") is not None
|
|
25
|
+
|
|
26
|
+
def get_password(self, service: str, username: str) -> str | None:
|
|
27
|
+
cred = self.get_credential(service, username)
|
|
28
|
+
return cred.password if cred else None
|
|
29
|
+
|
|
30
|
+
def get_credential(self, service: str, username: str) -> SimpleCredential | None:
|
|
31
|
+
host = (urlparse(service).hostname or "").lower().rstrip(".")
|
|
32
|
+
if host != _FEED_HOST:
|
|
33
|
+
return None
|
|
34
|
+
try:
|
|
35
|
+
token = subprocess.check_output(
|
|
36
|
+
[_AZ, "account", "get-access-token",
|
|
37
|
+
"--resource", _RESOURCE,
|
|
38
|
+
"--query", "accessToken", "-o", "tsv"],
|
|
39
|
+
stderr=subprocess.DEVNULL,
|
|
40
|
+
timeout=30,
|
|
41
|
+
).decode().strip()
|
|
42
|
+
if token:
|
|
43
|
+
return SimpleCredential("VssSessionToken", token)
|
|
44
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
|
|
45
|
+
pass
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
def set_password(self, service: str, username: str, password: str) -> None:
|
|
49
|
+
raise NotImplementedError
|
|
50
|
+
|
|
51
|
+
def delete_password(self, service: str, username: str) -> None:
|
|
52
|
+
raise NotImplementedError
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from hestia_keyring import HestiaArtifactsKeyring
|
|
7
|
+
|
|
8
|
+
_FEED = "https://pkgs.dev.azure.com/hmc-heerema/HES%20Internal/_packaging/HES/pypi/simple/"
|
|
9
|
+
_OTHER = "https://pypi.org/simple/"
|
|
10
|
+
_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.test"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_priority():
|
|
14
|
+
assert HestiaArtifactsKeyring.priority == 9
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_viable_true_when_az_found():
|
|
18
|
+
with patch("hestia_keyring.shutil.which", return_value=r"C:\az.cmd"):
|
|
19
|
+
assert HestiaArtifactsKeyring.viable() is True
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_viable_false_when_az_not_found():
|
|
23
|
+
with patch("hestia_keyring.shutil.which", return_value=None):
|
|
24
|
+
assert HestiaArtifactsKeyring.viable() is False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_get_credential_ignores_non_feed_url():
|
|
28
|
+
assert HestiaArtifactsKeyring().get_credential(_OTHER, "token") is None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_get_credential_rejects_url_with_feed_host_in_path():
|
|
32
|
+
# Substring bypass: attacker URL contains pkgs.dev.azure.com in path, not hostname.
|
|
33
|
+
# Must be rejected — only exact hostname match is valid.
|
|
34
|
+
evil = "https://evil.com/pkgs.dev.azure.com/steal-token"
|
|
35
|
+
assert HestiaArtifactsKeyring().get_credential(evil, "VssSessionToken") is None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_get_credential_returns_credential_for_feed_url():
|
|
39
|
+
with patch("hestia_keyring.subprocess.check_output", return_value=f"{_TOKEN}\n".encode()):
|
|
40
|
+
cred = HestiaArtifactsKeyring().get_credential(_FEED, "VssSessionToken")
|
|
41
|
+
assert cred is not None
|
|
42
|
+
assert cred.username == "VssSessionToken"
|
|
43
|
+
assert cred.password == _TOKEN
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_get_credential_returns_none_on_called_process_error():
|
|
47
|
+
with patch(
|
|
48
|
+
"hestia_keyring.subprocess.check_output",
|
|
49
|
+
side_effect=subprocess.CalledProcessError(1, "az"),
|
|
50
|
+
):
|
|
51
|
+
assert HestiaArtifactsKeyring().get_credential(_FEED, "VssSessionToken") is None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_get_credential_returns_none_on_timeout():
|
|
55
|
+
with patch(
|
|
56
|
+
"hestia_keyring.subprocess.check_output",
|
|
57
|
+
side_effect=subprocess.TimeoutExpired("az", 30),
|
|
58
|
+
):
|
|
59
|
+
assert HestiaArtifactsKeyring().get_credential(_FEED, "VssSessionToken") is None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_get_credential_returns_none_when_az_not_on_path():
|
|
63
|
+
with patch("hestia_keyring.subprocess.check_output", side_effect=FileNotFoundError):
|
|
64
|
+
assert HestiaArtifactsKeyring().get_credential(_FEED, "VssSessionToken") is None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_get_credential_returns_none_on_empty_token():
|
|
68
|
+
with patch("hestia_keyring.subprocess.check_output", return_value=b"\n"):
|
|
69
|
+
assert HestiaArtifactsKeyring().get_credential(_FEED, "VssSessionToken") is None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_get_password_returns_token_string():
|
|
73
|
+
with patch("hestia_keyring.subprocess.check_output", return_value=f"{_TOKEN}\n".encode()):
|
|
74
|
+
assert HestiaArtifactsKeyring().get_password(_FEED, "VssSessionToken") == _TOKEN
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_get_password_returns_none_for_non_feed_url():
|
|
78
|
+
assert HestiaArtifactsKeyring().get_password(_OTHER, "VssSessionToken") is None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_set_password_raises():
|
|
82
|
+
with pytest.raises(NotImplementedError):
|
|
83
|
+
HestiaArtifactsKeyring().set_password("svc", "user", "pass")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_delete_password_raises():
|
|
87
|
+
with pytest.raises(NotImplementedError):
|
|
88
|
+
HestiaArtifactsKeyring().delete_password("svc", "user")
|