secrefs 0.2.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.
- secrefs-0.2.0/LICENSE +21 -0
- secrefs-0.2.0/PKG-INFO +127 -0
- secrefs-0.2.0/README.md +102 -0
- secrefs-0.2.0/pyproject.toml +56 -0
- secrefs-0.2.0/secrefs/__init__.py +110 -0
- secrefs-0.2.0/secrefs/cli.py +140 -0
- secrefs-0.2.0/secrefs/control_plane_client.py +206 -0
- secrefs-0.2.0/secrefs/envfile.py +79 -0
- secrefs-0.2.0/secrefs/parser.py +70 -0
- secrefs-0.2.0/secrefs/providers/__init__.py +16 -0
- secrefs-0.2.0/secrefs/providers/aws.py +156 -0
- secrefs-0.2.0/secrefs/providers/base.py +89 -0
- secrefs-0.2.0/secrefs/providers/bitwarden.py +257 -0
- secrefs-0.2.0/secrefs/providers/local.py +79 -0
- secrefs-0.2.0/secrefs/providers/vault.py +99 -0
- secrefs-0.2.0/secrefs/resolver.py +138 -0
- secrefs-0.2.0/secrefs/ttl_cache.py +109 -0
secrefs-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Armistice Group
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
secrefs-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: secrefs
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: BYOV secret reference engine - expand sec:// references in memory at runtime
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: SecRefs
|
|
8
|
+
Author-email: hello@secrefs.com
|
|
9
|
+
Requires-Python: >=3.10,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Provides-Extra: bitwarden
|
|
18
|
+
Requires-Dist: bitwarden-sdk (>=2.1,<3.0) ; extra == "bitwarden"
|
|
19
|
+
Requires-Dist: boto3 (>=1.34,<2.0)
|
|
20
|
+
Requires-Dist: hvac (>=2.1,<3.0)
|
|
21
|
+
Project-URL: Homepage, https://secrefs.com
|
|
22
|
+
Project-URL: Repository, https://github.com/secrefs/secrefs
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# secrefs (Python)
|
|
26
|
+
|
|
27
|
+
Python SDK/CLI parity with `@secrefs/node`. See the repo root README for the
|
|
28
|
+
full `sec://` reference spec and provider list.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install secrefs
|
|
34
|
+
# Bitwarden Secrets Manager support is an extra - see "Providers" below:
|
|
35
|
+
pip install 'secrefs[bitwarden]'
|
|
36
|
+
# or, in this monorepo:
|
|
37
|
+
poetry install
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Library usage
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import asyncio
|
|
44
|
+
import os
|
|
45
|
+
from secrefs import sec_refs
|
|
46
|
+
|
|
47
|
+
async def main():
|
|
48
|
+
await sec_refs.init() # expands sec:// values in os.environ, in place
|
|
49
|
+
print(os.environ["DB_PASSWORD"])
|
|
50
|
+
|
|
51
|
+
asyncio.run(main())
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Two ways to expand, and when each is wrong
|
|
55
|
+
|
|
56
|
+
A `sec://` reference is a **stable name for a value that changes**. Which of
|
|
57
|
+
these you use decides whether that actually holds:
|
|
58
|
+
|
|
59
|
+
**At use (recommended).** Expand where the secret is consumed. Every call
|
|
60
|
+
re-fetches, so rotating the value at the source reaches this consumer with
|
|
61
|
+
no redeploy:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
async def call_vendor_api():
|
|
65
|
+
key = await sec_refs.expand_string("sec://aws/hackerone#api_key")
|
|
66
|
+
...
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**At load (`secrefs-py run`).** Expands once at startup and bakes plain
|
|
70
|
+
strings into the child's environment. Convenient, and the right choice for
|
|
71
|
+
short-lived processes like a CI job — but an environment variable is a
|
|
72
|
+
static string, so **a long-running process keeps the pre-rotation value
|
|
73
|
+
until it restarts**.
|
|
74
|
+
|
|
75
|
+
Network-backed providers re-fetch on every expansion by default. Concurrent
|
|
76
|
+
expansions of the same reference still share one request, and `cache_ttl_ms`
|
|
77
|
+
trades a bounded window of staleness for fewer round trips if you need it:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
AWSSecretsManagerProvider(cache_ttl_ms=30_000) # rotation lands within 30s
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`LocalProvider` re-reads its JSON file per fetch for the same reason, so
|
|
84
|
+
editing `.secrefs.local.json` mid-session takes effect; `cache_file=True`
|
|
85
|
+
restores the old behavior.
|
|
86
|
+
|
|
87
|
+
## Providers
|
|
88
|
+
|
|
89
|
+
| Alias | Backend | Ambient auth |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `aws` | AWS Secrets Manager | boto3's default credential chain |
|
|
92
|
+
| `vault` | HashiCorp Vault (KV v1/v2) | `VAULT_ADDR` / `VAULT_TOKEN` |
|
|
93
|
+
| `bitwarden` | Bitwarden Secrets Manager | `BWS_ACCESS_TOKEN` / `BWS_ORGANIZATION_ID` |
|
|
94
|
+
| `local` | gitignored JSON file | n/a - development only |
|
|
95
|
+
|
|
96
|
+
`bitwarden` needs the `secrefs[bitwarden]` extra: Bitwarden secrets are
|
|
97
|
+
end-to-end encrypted, so only Bitwarden's own SDK can decrypt them, and it
|
|
98
|
+
ships as prebuilt native wheels for a fixed set of platforms. It's an extra
|
|
99
|
+
rather than a hard dependency so a platform it doesn't build for can still
|
|
100
|
+
install SecRefs.
|
|
101
|
+
|
|
102
|
+
## Control-plane-sourced credentials
|
|
103
|
+
|
|
104
|
+
`AWSSecretsManagerProvider` and `BitwardenProvider` can source credentials
|
|
105
|
+
per request from a running control plane instead of the ambient environment
|
|
106
|
+
(see `docs/control-plane-design.md`), so each fetch is authenticated,
|
|
107
|
+
RBAC-checked and audited on the control plane's side:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from secrefs import AWSSecretsManagerProvider, ControlPlaneCredentialSource
|
|
111
|
+
|
|
112
|
+
AWSSecretsManagerProvider(
|
|
113
|
+
control_plane=ControlPlaneCredentialSource(
|
|
114
|
+
base_url=os.environ["SECREFS_CONTROL_PLANE_URL"],
|
|
115
|
+
token=os.environ["SECREFS_CONTROL_PLANE_TOKEN"],
|
|
116
|
+
alias="aws-prod",
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## CLI usage
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
secrefs-py run -- python app.py
|
|
125
|
+
secrefs-py check
|
|
126
|
+
```
|
|
127
|
+
|
secrefs-0.2.0/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# secrefs (Python)
|
|
2
|
+
|
|
3
|
+
Python SDK/CLI parity with `@secrefs/node`. See the repo root README for the
|
|
4
|
+
full `sec://` reference spec and provider list.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install secrefs
|
|
10
|
+
# Bitwarden Secrets Manager support is an extra - see "Providers" below:
|
|
11
|
+
pip install 'secrefs[bitwarden]'
|
|
12
|
+
# or, in this monorepo:
|
|
13
|
+
poetry install
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Library usage
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import asyncio
|
|
20
|
+
import os
|
|
21
|
+
from secrefs import sec_refs
|
|
22
|
+
|
|
23
|
+
async def main():
|
|
24
|
+
await sec_refs.init() # expands sec:// values in os.environ, in place
|
|
25
|
+
print(os.environ["DB_PASSWORD"])
|
|
26
|
+
|
|
27
|
+
asyncio.run(main())
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Two ways to expand, and when each is wrong
|
|
31
|
+
|
|
32
|
+
A `sec://` reference is a **stable name for a value that changes**. Which of
|
|
33
|
+
these you use decides whether that actually holds:
|
|
34
|
+
|
|
35
|
+
**At use (recommended).** Expand where the secret is consumed. Every call
|
|
36
|
+
re-fetches, so rotating the value at the source reaches this consumer with
|
|
37
|
+
no redeploy:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
async def call_vendor_api():
|
|
41
|
+
key = await sec_refs.expand_string("sec://aws/hackerone#api_key")
|
|
42
|
+
...
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**At load (`secrefs-py run`).** Expands once at startup and bakes plain
|
|
46
|
+
strings into the child's environment. Convenient, and the right choice for
|
|
47
|
+
short-lived processes like a CI job — but an environment variable is a
|
|
48
|
+
static string, so **a long-running process keeps the pre-rotation value
|
|
49
|
+
until it restarts**.
|
|
50
|
+
|
|
51
|
+
Network-backed providers re-fetch on every expansion by default. Concurrent
|
|
52
|
+
expansions of the same reference still share one request, and `cache_ttl_ms`
|
|
53
|
+
trades a bounded window of staleness for fewer round trips if you need it:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
AWSSecretsManagerProvider(cache_ttl_ms=30_000) # rotation lands within 30s
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`LocalProvider` re-reads its JSON file per fetch for the same reason, so
|
|
60
|
+
editing `.secrefs.local.json` mid-session takes effect; `cache_file=True`
|
|
61
|
+
restores the old behavior.
|
|
62
|
+
|
|
63
|
+
## Providers
|
|
64
|
+
|
|
65
|
+
| Alias | Backend | Ambient auth |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `aws` | AWS Secrets Manager | boto3's default credential chain |
|
|
68
|
+
| `vault` | HashiCorp Vault (KV v1/v2) | `VAULT_ADDR` / `VAULT_TOKEN` |
|
|
69
|
+
| `bitwarden` | Bitwarden Secrets Manager | `BWS_ACCESS_TOKEN` / `BWS_ORGANIZATION_ID` |
|
|
70
|
+
| `local` | gitignored JSON file | n/a - development only |
|
|
71
|
+
|
|
72
|
+
`bitwarden` needs the `secrefs[bitwarden]` extra: Bitwarden secrets are
|
|
73
|
+
end-to-end encrypted, so only Bitwarden's own SDK can decrypt them, and it
|
|
74
|
+
ships as prebuilt native wheels for a fixed set of platforms. It's an extra
|
|
75
|
+
rather than a hard dependency so a platform it doesn't build for can still
|
|
76
|
+
install SecRefs.
|
|
77
|
+
|
|
78
|
+
## Control-plane-sourced credentials
|
|
79
|
+
|
|
80
|
+
`AWSSecretsManagerProvider` and `BitwardenProvider` can source credentials
|
|
81
|
+
per request from a running control plane instead of the ambient environment
|
|
82
|
+
(see `docs/control-plane-design.md`), so each fetch is authenticated,
|
|
83
|
+
RBAC-checked and audited on the control plane's side:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from secrefs import AWSSecretsManagerProvider, ControlPlaneCredentialSource
|
|
87
|
+
|
|
88
|
+
AWSSecretsManagerProvider(
|
|
89
|
+
control_plane=ControlPlaneCredentialSource(
|
|
90
|
+
base_url=os.environ["SECREFS_CONTROL_PLANE_URL"],
|
|
91
|
+
token=os.environ["SECREFS_CONTROL_PLANE_TOKEN"],
|
|
92
|
+
alias="aws-prod",
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## CLI usage
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
secrefs-py run -- python app.py
|
|
101
|
+
secrefs-py check
|
|
102
|
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "secrefs"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "BYOV secret reference engine - expand sec:// references in memory at runtime"
|
|
5
|
+
authors = ["SecRefs <hello@secrefs.com>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
homepage = "https://secrefs.com"
|
|
9
|
+
repository = "https://github.com/secrefs/secrefs"
|
|
10
|
+
packages = [{ include = "secrefs" }]
|
|
11
|
+
|
|
12
|
+
[tool.poetry.dependencies]
|
|
13
|
+
python = "^3.10"
|
|
14
|
+
boto3 = "^1.34"
|
|
15
|
+
hvac = "^2.1"
|
|
16
|
+
# Optional, unlike boto3/hvac: bitwarden-sdk publishes only prebuilt native
|
|
17
|
+
# wheels for a fixed set of platform tags with no source distribution, so a
|
|
18
|
+
# hard dependency would break installing secrefs at all on any platform
|
|
19
|
+
# Bitwarden doesn't build for - including for the many users who never write
|
|
20
|
+
# a sec://bitwarden/... reference. providers/bitwarden.py imports it at
|
|
21
|
+
# first use and explains how to install it if it's absent.
|
|
22
|
+
bitwarden-sdk = { version = "^2.1", optional = true }
|
|
23
|
+
|
|
24
|
+
[tool.poetry.extras]
|
|
25
|
+
bitwarden = ["bitwarden-sdk"]
|
|
26
|
+
|
|
27
|
+
[tool.poetry.group.dev.dependencies]
|
|
28
|
+
pytest = "^8.2"
|
|
29
|
+
pytest-asyncio = "^0.23"
|
|
30
|
+
mypy = "^1.10"
|
|
31
|
+
|
|
32
|
+
[tool.poetry.scripts]
|
|
33
|
+
secrefs-py = "secrefs.cli:main"
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
asyncio_mode = "auto"
|
|
37
|
+
testpaths = ["tests"]
|
|
38
|
+
|
|
39
|
+
[tool.mypy]
|
|
40
|
+
python_version = "3.10"
|
|
41
|
+
warn_unused_configs = true
|
|
42
|
+
strict = true
|
|
43
|
+
|
|
44
|
+
# boto3/hvac/bitwarden_sdk ship without inline type stubs (py.typed) or a
|
|
45
|
+
# matching stub-only package pinned in this project; suppress the import
|
|
46
|
+
# warning for just these rather than pulling in a heavy generated-stub
|
|
47
|
+
# dependency (e.g. boto3-stubs) not called for in the project spec.
|
|
48
|
+
# bitwarden_sdk is additionally an optional extra, so it may legitimately
|
|
49
|
+
# not be installed in the environment mypy runs in.
|
|
50
|
+
[[tool.mypy.overrides]]
|
|
51
|
+
module = ["boto3.*", "hvac.*", "bitwarden_sdk.*"]
|
|
52
|
+
ignore_missing_imports = true
|
|
53
|
+
|
|
54
|
+
[build-system]
|
|
55
|
+
requires = ["poetry-core"]
|
|
56
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
from .control_plane_client import (
|
|
5
|
+
ControlPlaneClient,
|
|
6
|
+
ControlPlaneCredentialSource,
|
|
7
|
+
ControlPlaneRequestError,
|
|
8
|
+
MintCredentialResponse,
|
|
9
|
+
MintedAWSCredentials,
|
|
10
|
+
MintedBitwardenCredentials,
|
|
11
|
+
)
|
|
12
|
+
from .parser import (
|
|
13
|
+
ParsedSecretRef,
|
|
14
|
+
SecRefParseError,
|
|
15
|
+
is_secret_ref,
|
|
16
|
+
parse_secret_ref,
|
|
17
|
+
try_parse_secret_ref,
|
|
18
|
+
)
|
|
19
|
+
from .providers.aws import AWSSecretsManagerProvider
|
|
20
|
+
from .providers.base import ProviderHealth, SecretFetchError, SecretFetchRequest, SecretProvider
|
|
21
|
+
from .providers.bitwarden import BitwardenProvider
|
|
22
|
+
from .providers.local import LocalProvider
|
|
23
|
+
from .providers.vault import VaultProvider
|
|
24
|
+
from .resolver import (
|
|
25
|
+
CheckResult,
|
|
26
|
+
ProviderRegistry,
|
|
27
|
+
ResolutionFailure,
|
|
28
|
+
SecRefsResolutionError,
|
|
29
|
+
check_references,
|
|
30
|
+
expand_environ,
|
|
31
|
+
expand_key_value_map,
|
|
32
|
+
)
|
|
33
|
+
from .ttl_cache import TtlCache
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create_default_providers() -> Dict[str, SecretProvider]:
|
|
37
|
+
"""Builds the default provider registry: aws, vault, local, bitwarden."""
|
|
38
|
+
return {
|
|
39
|
+
"aws": AWSSecretsManagerProvider(),
|
|
40
|
+
"vault": VaultProvider(),
|
|
41
|
+
"local": LocalProvider(),
|
|
42
|
+
"bitwarden": BitwardenProvider(),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SecRefs:
|
|
47
|
+
"""Mirrors the Node SDK's `secRefs.init()` / `expandEnv()` / `expandString()` API."""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
providers: Optional[Dict[str, SecretProvider]] = None,
|
|
52
|
+
strict: bool = True,
|
|
53
|
+
) -> None:
|
|
54
|
+
self.providers: Dict[str, SecretProvider] = providers or create_default_providers()
|
|
55
|
+
self.strict = strict
|
|
56
|
+
|
|
57
|
+
async def init(self) -> List[str]:
|
|
58
|
+
"""Expands sec:// values found in os.environ, mutating it in place."""
|
|
59
|
+
return await expand_environ(self.providers, strict=self.strict)
|
|
60
|
+
|
|
61
|
+
async def expand_env(self, env: Dict[str, Optional[str]]) -> Dict[str, str]:
|
|
62
|
+
"""Expands sec:// values in an arbitrary key/value map without touching os.environ."""
|
|
63
|
+
return await expand_key_value_map(env, self.providers, strict=self.strict)
|
|
64
|
+
|
|
65
|
+
async def expand_string(self, value: str) -> str:
|
|
66
|
+
"""Expands a single string if it's a sec:// reference; otherwise returns it unchanged."""
|
|
67
|
+
if not is_secret_ref(value):
|
|
68
|
+
return value
|
|
69
|
+
result = await expand_key_value_map({"__value__": value}, self.providers, strict=True)
|
|
70
|
+
return result["__value__"]
|
|
71
|
+
|
|
72
|
+
async def check(self, env: Optional[Dict[str, Optional[str]]] = None) -> List[CheckResult]:
|
|
73
|
+
"""Dry-run validation of every sec:// reference in `env` (defaults to os.environ)."""
|
|
74
|
+
return await check_references(env if env is not None else dict(os.environ), self.providers)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
sec_refs = SecRefs()
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
"SecRefs",
|
|
81
|
+
"sec_refs",
|
|
82
|
+
"create_default_providers",
|
|
83
|
+
"ParsedSecretRef",
|
|
84
|
+
"SecRefParseError",
|
|
85
|
+
"is_secret_ref",
|
|
86
|
+
"parse_secret_ref",
|
|
87
|
+
"try_parse_secret_ref",
|
|
88
|
+
"AWSSecretsManagerProvider",
|
|
89
|
+
"VaultProvider",
|
|
90
|
+
"LocalProvider",
|
|
91
|
+
"BitwardenProvider",
|
|
92
|
+
"SecretProvider",
|
|
93
|
+
"SecretFetchRequest",
|
|
94
|
+
"SecretFetchError",
|
|
95
|
+
"ProviderHealth",
|
|
96
|
+
"TtlCache",
|
|
97
|
+
"ControlPlaneClient",
|
|
98
|
+
"ControlPlaneCredentialSource",
|
|
99
|
+
"ControlPlaneRequestError",
|
|
100
|
+
"MintCredentialResponse",
|
|
101
|
+
"MintedAWSCredentials",
|
|
102
|
+
"MintedBitwardenCredentials",
|
|
103
|
+
"SecRefsResolutionError",
|
|
104
|
+
"ResolutionFailure",
|
|
105
|
+
"CheckResult",
|
|
106
|
+
"ProviderRegistry",
|
|
107
|
+
"expand_environ",
|
|
108
|
+
"expand_key_value_map",
|
|
109
|
+
"check_references",
|
|
110
|
+
]
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
import signal
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List
|
|
11
|
+
|
|
12
|
+
from . import SecRefs, SecRefsResolutionError
|
|
13
|
+
from .envfile import parse_env_file_text
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _load_dotenv(path: Path) -> None:
|
|
17
|
+
"""Loads a `.env` file into `os.environ`, correctly preserving
|
|
18
|
+
`sec://...#field` fragments (see `envfile.py`). Mirrors dotenv.config()'s
|
|
19
|
+
default precedence: never overrides a value already present in
|
|
20
|
+
`os.environ` (e.g. set explicitly by the shell/CI)."""
|
|
21
|
+
if not path.exists():
|
|
22
|
+
return
|
|
23
|
+
parsed = parse_env_file_text(path.read_text(encoding="utf-8"))
|
|
24
|
+
for key, value in parsed.items():
|
|
25
|
+
os.environ.setdefault(key, value)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
29
|
+
parser = argparse.ArgumentParser(
|
|
30
|
+
prog="secrefs-py",
|
|
31
|
+
description="BYOV secret reference engine - expand sec:// references in memory at runtime",
|
|
32
|
+
)
|
|
33
|
+
subparsers = parser.add_subparsers(dest="subcommand", required=True)
|
|
34
|
+
|
|
35
|
+
run_parser = subparsers.add_parser(
|
|
36
|
+
"run", help="Resolve sec:// references and spawn a child process"
|
|
37
|
+
)
|
|
38
|
+
run_parser.add_argument("--env-file", default=".env", help="path to a .env file to load first")
|
|
39
|
+
run_parser.add_argument(
|
|
40
|
+
"--no-env-file", action="store_true", help="skip loading a .env file"
|
|
41
|
+
)
|
|
42
|
+
run_parser.add_argument(
|
|
43
|
+
"command", nargs=argparse.REMAINDER, help="command to run, e.g. -- python app.py"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
check_parser = subparsers.add_parser(
|
|
47
|
+
"check", help="Validate sec:// references and provider reachability"
|
|
48
|
+
)
|
|
49
|
+
check_parser.add_argument("--env-file", default=".env", help="path to a .env file to load first")
|
|
50
|
+
check_parser.add_argument(
|
|
51
|
+
"--no-env-file", action="store_true", help="skip loading a .env file"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return parser
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def _run(args: argparse.Namespace) -> int:
|
|
58
|
+
command: List[str] = [c for c in args.command if c != "--"]
|
|
59
|
+
if not command:
|
|
60
|
+
print(
|
|
61
|
+
"secrefs-py run: no command given. Usage: secrefs-py run -- <command> [args...]",
|
|
62
|
+
file=sys.stderr,
|
|
63
|
+
)
|
|
64
|
+
return 1
|
|
65
|
+
|
|
66
|
+
if not args.no_env_file:
|
|
67
|
+
_load_dotenv(Path(args.env_file))
|
|
68
|
+
|
|
69
|
+
instance = SecRefs()
|
|
70
|
+
try:
|
|
71
|
+
changed = await instance.init()
|
|
72
|
+
if changed:
|
|
73
|
+
print(
|
|
74
|
+
f"secrefs-py: resolved {len(changed)} secret reference(s): {', '.join(changed)}",
|
|
75
|
+
file=sys.stderr,
|
|
76
|
+
)
|
|
77
|
+
except SecRefsResolutionError as exc:
|
|
78
|
+
print("secrefs-py: failed to resolve one or more secret references:", file=sys.stderr)
|
|
79
|
+
print(str(exc), file=sys.stderr)
|
|
80
|
+
return 1
|
|
81
|
+
|
|
82
|
+
process = subprocess.Popen(command, env=os.environ.copy())
|
|
83
|
+
|
|
84
|
+
forwarded_signals = [
|
|
85
|
+
s for s in (signal.SIGINT, signal.SIGTERM, getattr(signal, "SIGHUP", None)) if s is not None
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
def _forward(sig: int, _frame: object) -> None:
|
|
89
|
+
if process.poll() is None:
|
|
90
|
+
process.send_signal(sig)
|
|
91
|
+
|
|
92
|
+
original_handlers = {sig: signal.getsignal(sig) for sig in forwarded_signals}
|
|
93
|
+
for sig in forwarded_signals:
|
|
94
|
+
signal.signal(sig, _forward)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
return process.wait()
|
|
98
|
+
finally:
|
|
99
|
+
for sig, handler in original_handlers.items():
|
|
100
|
+
signal.signal(sig, handler)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def _check(args: argparse.Namespace) -> int:
|
|
104
|
+
if not args.no_env_file:
|
|
105
|
+
_load_dotenv(Path(args.env_file))
|
|
106
|
+
|
|
107
|
+
instance = SecRefs()
|
|
108
|
+
results = await instance.check()
|
|
109
|
+
|
|
110
|
+
if not results:
|
|
111
|
+
print("secrefs-py check: no sec:// references found in the environment.")
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
ok_count = 0
|
|
115
|
+
for result in results:
|
|
116
|
+
icon = "✓" if result.ok else "✗"
|
|
117
|
+
print(f"{icon} {result.key} -> {result.ref}")
|
|
118
|
+
if result.ok:
|
|
119
|
+
ok_count += 1
|
|
120
|
+
else:
|
|
121
|
+
print(f" {result.message}")
|
|
122
|
+
|
|
123
|
+
print(f"\n{ok_count}/{len(results)} reference(s) resolved successfully.")
|
|
124
|
+
return 0 if ok_count == len(results) else 1
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main() -> None:
|
|
128
|
+
parser = _build_parser()
|
|
129
|
+
args = parser.parse_args()
|
|
130
|
+
|
|
131
|
+
if args.subcommand == "run":
|
|
132
|
+
exit_code = asyncio.run(_run(args))
|
|
133
|
+
else:
|
|
134
|
+
exit_code = asyncio.run(_check(args))
|
|
135
|
+
|
|
136
|
+
sys.exit(exit_code)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
main()
|