dev-vault 0.1.2__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.
- dev_vault-0.1.2/LICENSE +21 -0
- dev_vault-0.1.2/PKG-INFO +170 -0
- dev_vault-0.1.2/README.md +138 -0
- dev_vault-0.1.2/dev_vault/__init__.py +3 -0
- dev_vault-0.1.2/dev_vault/cli.py +177 -0
- dev_vault-0.1.2/dev_vault/commands/__init__.py +0 -0
- dev_vault-0.1.2/dev_vault/commands/config_cmd.py +42 -0
- dev_vault-0.1.2/dev_vault/commands/get.py +134 -0
- dev_vault-0.1.2/dev_vault/commands/inject.py +91 -0
- dev_vault-0.1.2/dev_vault/commands/item.py +122 -0
- dev_vault-0.1.2/dev_vault/commands/migrate.py +108 -0
- dev_vault-0.1.2/dev_vault/commands/run.py +117 -0
- dev_vault-0.1.2/dev_vault/commands/set_cmd.py +83 -0
- dev_vault-0.1.2/dev_vault/commands/setup.py +232 -0
- dev_vault-0.1.2/dev_vault/commands/vault.py +90 -0
- dev_vault-0.1.2/dev_vault/core/__init__.py +0 -0
- dev_vault-0.1.2/dev_vault/core/config.py +165 -0
- dev_vault-0.1.2/dev_vault/core/keyring_store.py +68 -0
- dev_vault-0.1.2/dev_vault/core/manifest.py +56 -0
- dev_vault-0.1.2/dev_vault/core/resolver.py +65 -0
- dev_vault-0.1.2/dev_vault/providers/__init__.py +7 -0
- dev_vault-0.1.2/dev_vault/providers/base.py +21 -0
- dev_vault-0.1.2/dev_vault/providers/keycloak.py +135 -0
- dev_vault-0.1.2/dev_vault/setup_path.py +93 -0
- dev_vault-0.1.2/dev_vault/ui/__init__.py +0 -0
- dev_vault-0.1.2/dev_vault/ui/console.py +6 -0
- dev_vault-0.1.2/dev_vault/utils.py +11 -0
- dev_vault-0.1.2/dev_vault.egg-info/PKG-INFO +170 -0
- dev_vault-0.1.2/dev_vault.egg-info/SOURCES.txt +33 -0
- dev_vault-0.1.2/dev_vault.egg-info/dependency_links.txt +1 -0
- dev_vault-0.1.2/dev_vault.egg-info/entry_points.txt +4 -0
- dev_vault-0.1.2/dev_vault.egg-info/requires.txt +6 -0
- dev_vault-0.1.2/dev_vault.egg-info/top_level.txt +1 -0
- dev_vault-0.1.2/pyproject.toml +52 -0
- dev_vault-0.1.2/setup.cfg +4 -0
dev_vault-0.1.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 caetanominuzzo
|
|
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.
|
dev_vault-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dev-vault
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Developer secret vault + OIDC token provider — for developers, scripts, and AI agents
|
|
5
|
+
Author: Caetano Minuzzo
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/caetanominuzzo/dev-vault
|
|
8
|
+
Project-URL: Repository, https://github.com/caetanominuzzo/dev-vault
|
|
9
|
+
Project-URL: Issues, https://github.com/caetanominuzzo/dev-vault/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/caetanominuzzo/dev-vault/releases
|
|
11
|
+
Keywords: vault,secrets,keyring,cli,oidc,keycloak,token,oauth,ai-agent
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Topic :: Security
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Requires-Python: >=3.7
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: httpx>=0.24.0
|
|
26
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
27
|
+
Requires-Dist: pyperclip>=1.8.2
|
|
28
|
+
Requires-Dist: rich>=13.0.0
|
|
29
|
+
Requires-Dist: inquirer>=3.0.0
|
|
30
|
+
Requires-Dist: keyring>=24.0.0
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# dev-vault
|
|
34
|
+
|
|
35
|
+
**All your secrets in one command.** Developer secret vault + OIDC token provider for developers, scripts, and AI agents.
|
|
36
|
+
|
|
37
|
+
## Why dev-vault?
|
|
38
|
+
|
|
39
|
+
AI agents need secrets (API keys, bearer tokens) but can't safely read `.env` files, and hardcoding secrets in prompts is a security risk. dev-vault stores secrets in your OS keyring and exposes them via a simple CLI -- the secret never appears in conversation context, only where it's needed.
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install dev-vault
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Store a secret
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
dv set datadog api_key # prompts for value (masked)
|
|
51
|
+
dv set datadog api_key "abc123" # inline (for scripts)
|
|
52
|
+
dv set datadog app_key "def456" # multiple fields per item
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Retrieve a secret
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
dv get datadog # primary field -> stdout
|
|
59
|
+
dv get datadog api_key # specific field (planned: prefix matching)
|
|
60
|
+
dv get default datadog api_key # explicit vault
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Use with AI agents
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Agent prompt: "use dv to get the bearer for the endpoint /api/motorcycles"
|
|
67
|
+
curl -H "Authorization: Bearer $(dv get prod caetano)" https://api.mottu.com/api/motorcycles
|
|
68
|
+
|
|
69
|
+
# Agent prompt: "query datadog for error rates"
|
|
70
|
+
DD_API_KEY=$(dv get datadog) python check_errors.py
|
|
71
|
+
|
|
72
|
+
# Or with dv run -- agent just says "run the script"
|
|
73
|
+
dv run -- python check_errors.py # secrets injected from .dv.yaml
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Run commands with secrets injected
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Explicit mapping
|
|
80
|
+
dv run -s DD_API_KEY=datadog/api_key -s DD_APP_KEY=datadog/app_key -- python app.py
|
|
81
|
+
|
|
82
|
+
# Using .dv.yaml manifest (checked into git, no secrets)
|
|
83
|
+
dv run -- python app.py
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Project manifest (`.dv.yaml`)
|
|
87
|
+
|
|
88
|
+
Place in your project root. Maps environment variables to secret references:
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
secrets:
|
|
92
|
+
DD_API_KEY: datadog/api_key
|
|
93
|
+
DD_APP_KEY: datadog/app_key
|
|
94
|
+
BEARER_TOKEN: prod/admin@example.com # OIDC -> fresh token
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Template injection
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
echo 'KEY={{dv://default/datadog/api_key}}' | dv inject
|
|
101
|
+
# Output: KEY=abc123
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## OIDC Token Provider
|
|
105
|
+
|
|
106
|
+
dev-vault can fetch fresh OIDC tokens from Keycloak (with more providers planned):
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
dv setup # interactive wizard
|
|
110
|
+
dv get prod admin@example.com # returns a fresh access_token
|
|
111
|
+
dv get prod api-client # client_credentials flow
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Migrating from sso-cli
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pip install dev-vault
|
|
118
|
+
dv migrate sso-cli # imports config + keyring secrets
|
|
119
|
+
dv get prod admin@example.com # same token, new tool
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Commands
|
|
123
|
+
|
|
124
|
+
| Command | Description |
|
|
125
|
+
|---------|-------------|
|
|
126
|
+
| `dv get [vault] <item> [field]` | Retrieve secret or OIDC token |
|
|
127
|
+
| `dv set [vault] <item> <field> [value]` | Store a secret |
|
|
128
|
+
| `dv run [-s KEY=ref] -- <cmd>` | Run command with secrets as env vars |
|
|
129
|
+
| `dv inject` | Replace `{{dv://...}}` refs in stdin |
|
|
130
|
+
| `dv item list\|create\|show\|delete` | Manage items |
|
|
131
|
+
| `dv vault list\|create\|delete` | Manage vaults |
|
|
132
|
+
| `dv setup [--reset]` | Interactive setup wizard |
|
|
133
|
+
| `dv migrate sso-cli` | Import from sso-cli |
|
|
134
|
+
| `dv config show` | Display current config |
|
|
135
|
+
|
|
136
|
+
All commands support `--json` for programmatic output and `-v` for debug logging.
|
|
137
|
+
|
|
138
|
+
## Security
|
|
139
|
+
|
|
140
|
+
dev-vault is built with a strict security-first approach:
|
|
141
|
+
|
|
142
|
+
- **Secrets never touch disk.** All secret values are stored exclusively in the OS keyring (macOS Keychain, Linux Secret Service, Windows Credential Manager). The config YAML only contains metadata (vault names, item names, field names, OIDC provider URLs).
|
|
143
|
+
- **No secrets in logs or output.** Debug/verbose mode (`-v`) never logs secret values. Human-friendly output goes to stderr; only raw secret values go to stdout (for `$(dv get ...)` substitution).
|
|
144
|
+
- **Masked input.** Interactive secret entry uses `getpass` (no terminal echo).
|
|
145
|
+
- **Subprocess isolation.** `dv run` injects secrets as environment variables only into the child process -- they don't leak into the parent shell or shell history.
|
|
146
|
+
- **No network calls for static secrets.** Only OIDC items make network requests, and only to the configured SSO endpoint.
|
|
147
|
+
- **Config file permissions.** The config directory (`~/.config/dev-vault/`) inherits your user's default umask. No world-readable files.
|
|
148
|
+
- **No telemetry.** dev-vault makes zero calls home. No analytics, no crash reporting.
|
|
149
|
+
|
|
150
|
+
### Supply chain
|
|
151
|
+
|
|
152
|
+
- Minimal dependencies: `httpx`, `pyyaml`, `keyring`, `rich`, `inquirer`, `pyperclip` -- all well-established, actively maintained packages.
|
|
153
|
+
- Published to PyPI with standard setuptools build.
|
|
154
|
+
- Source available on GitHub for audit.
|
|
155
|
+
|
|
156
|
+
## How It Works
|
|
157
|
+
|
|
158
|
+
- **Config** location: `~/.config/dev-vault/config.yaml` (XDG-compliant), fallback `~/.dv.yaml`. Override with `DV_CONFIG` env var.
|
|
159
|
+
- **OIDC items** fetch fresh tokens on every call (no caching, no stale tokens). Static items return stored keyring values.
|
|
160
|
+
- **Secret references** use `dv://vault/item/field` URIs or shorthand (`item/field`, `vault/item`).
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
PyPI package: https://pypi.org/project/dev-vault/
|
|
165
|
+
|
|
166
|
+
## See Also
|
|
167
|
+
|
|
168
|
+
- [Agent State](https://agentstate.tech/) -- Persistent memory and tools for AI agents
|
|
169
|
+
- [sso-cli](https://pypi.org/project/sso-cli/) -- Single Sign-On token CLI (the ancestor of dev-vault)
|
|
170
|
+
- [terminal-to-here](https://github.com/caetanominuzzo/terminal-to-here) -- VS Code extension to open terminal at any folder
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# dev-vault
|
|
2
|
+
|
|
3
|
+
**All your secrets in one command.** Developer secret vault + OIDC token provider for developers, scripts, and AI agents.
|
|
4
|
+
|
|
5
|
+
## Why dev-vault?
|
|
6
|
+
|
|
7
|
+
AI agents need secrets (API keys, bearer tokens) but can't safely read `.env` files, and hardcoding secrets in prompts is a security risk. dev-vault stores secrets in your OS keyring and exposes them via a simple CLI -- the secret never appears in conversation context, only where it's needed.
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install dev-vault
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Store a secret
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
dv set datadog api_key # prompts for value (masked)
|
|
19
|
+
dv set datadog api_key "abc123" # inline (for scripts)
|
|
20
|
+
dv set datadog app_key "def456" # multiple fields per item
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Retrieve a secret
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
dv get datadog # primary field -> stdout
|
|
27
|
+
dv get datadog api_key # specific field (planned: prefix matching)
|
|
28
|
+
dv get default datadog api_key # explicit vault
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Use with AI agents
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Agent prompt: "use dv to get the bearer for the endpoint /api/motorcycles"
|
|
35
|
+
curl -H "Authorization: Bearer $(dv get prod caetano)" https://api.mottu.com/api/motorcycles
|
|
36
|
+
|
|
37
|
+
# Agent prompt: "query datadog for error rates"
|
|
38
|
+
DD_API_KEY=$(dv get datadog) python check_errors.py
|
|
39
|
+
|
|
40
|
+
# Or with dv run -- agent just says "run the script"
|
|
41
|
+
dv run -- python check_errors.py # secrets injected from .dv.yaml
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Run commands with secrets injected
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Explicit mapping
|
|
48
|
+
dv run -s DD_API_KEY=datadog/api_key -s DD_APP_KEY=datadog/app_key -- python app.py
|
|
49
|
+
|
|
50
|
+
# Using .dv.yaml manifest (checked into git, no secrets)
|
|
51
|
+
dv run -- python app.py
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Project manifest (`.dv.yaml`)
|
|
55
|
+
|
|
56
|
+
Place in your project root. Maps environment variables to secret references:
|
|
57
|
+
|
|
58
|
+
```yaml
|
|
59
|
+
secrets:
|
|
60
|
+
DD_API_KEY: datadog/api_key
|
|
61
|
+
DD_APP_KEY: datadog/app_key
|
|
62
|
+
BEARER_TOKEN: prod/admin@example.com # OIDC -> fresh token
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Template injection
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
echo 'KEY={{dv://default/datadog/api_key}}' | dv inject
|
|
69
|
+
# Output: KEY=abc123
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## OIDC Token Provider
|
|
73
|
+
|
|
74
|
+
dev-vault can fetch fresh OIDC tokens from Keycloak (with more providers planned):
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
dv setup # interactive wizard
|
|
78
|
+
dv get prod admin@example.com # returns a fresh access_token
|
|
79
|
+
dv get prod api-client # client_credentials flow
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Migrating from sso-cli
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install dev-vault
|
|
86
|
+
dv migrate sso-cli # imports config + keyring secrets
|
|
87
|
+
dv get prod admin@example.com # same token, new tool
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Commands
|
|
91
|
+
|
|
92
|
+
| Command | Description |
|
|
93
|
+
|---------|-------------|
|
|
94
|
+
| `dv get [vault] <item> [field]` | Retrieve secret or OIDC token |
|
|
95
|
+
| `dv set [vault] <item> <field> [value]` | Store a secret |
|
|
96
|
+
| `dv run [-s KEY=ref] -- <cmd>` | Run command with secrets as env vars |
|
|
97
|
+
| `dv inject` | Replace `{{dv://...}}` refs in stdin |
|
|
98
|
+
| `dv item list\|create\|show\|delete` | Manage items |
|
|
99
|
+
| `dv vault list\|create\|delete` | Manage vaults |
|
|
100
|
+
| `dv setup [--reset]` | Interactive setup wizard |
|
|
101
|
+
| `dv migrate sso-cli` | Import from sso-cli |
|
|
102
|
+
| `dv config show` | Display current config |
|
|
103
|
+
|
|
104
|
+
All commands support `--json` for programmatic output and `-v` for debug logging.
|
|
105
|
+
|
|
106
|
+
## Security
|
|
107
|
+
|
|
108
|
+
dev-vault is built with a strict security-first approach:
|
|
109
|
+
|
|
110
|
+
- **Secrets never touch disk.** All secret values are stored exclusively in the OS keyring (macOS Keychain, Linux Secret Service, Windows Credential Manager). The config YAML only contains metadata (vault names, item names, field names, OIDC provider URLs).
|
|
111
|
+
- **No secrets in logs or output.** Debug/verbose mode (`-v`) never logs secret values. Human-friendly output goes to stderr; only raw secret values go to stdout (for `$(dv get ...)` substitution).
|
|
112
|
+
- **Masked input.** Interactive secret entry uses `getpass` (no terminal echo).
|
|
113
|
+
- **Subprocess isolation.** `dv run` injects secrets as environment variables only into the child process -- they don't leak into the parent shell or shell history.
|
|
114
|
+
- **No network calls for static secrets.** Only OIDC items make network requests, and only to the configured SSO endpoint.
|
|
115
|
+
- **Config file permissions.** The config directory (`~/.config/dev-vault/`) inherits your user's default umask. No world-readable files.
|
|
116
|
+
- **No telemetry.** dev-vault makes zero calls home. No analytics, no crash reporting.
|
|
117
|
+
|
|
118
|
+
### Supply chain
|
|
119
|
+
|
|
120
|
+
- Minimal dependencies: `httpx`, `pyyaml`, `keyring`, `rich`, `inquirer`, `pyperclip` -- all well-established, actively maintained packages.
|
|
121
|
+
- Published to PyPI with standard setuptools build.
|
|
122
|
+
- Source available on GitHub for audit.
|
|
123
|
+
|
|
124
|
+
## How It Works
|
|
125
|
+
|
|
126
|
+
- **Config** location: `~/.config/dev-vault/config.yaml` (XDG-compliant), fallback `~/.dv.yaml`. Override with `DV_CONFIG` env var.
|
|
127
|
+
- **OIDC items** fetch fresh tokens on every call (no caching, no stale tokens). Static items return stored keyring values.
|
|
128
|
+
- **Secret references** use `dv://vault/item/field` URIs or shorthand (`item/field`, `vault/item`).
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
PyPI package: https://pypi.org/project/dev-vault/
|
|
133
|
+
|
|
134
|
+
## See Also
|
|
135
|
+
|
|
136
|
+
- [Agent State](https://agentstate.tech/) -- Persistent memory and tools for AI agents
|
|
137
|
+
- [sso-cli](https://pypi.org/project/sso-cli/) -- Single Sign-On token CLI (the ancestor of dev-vault)
|
|
138
|
+
- [terminal-to-here](https://github.com/caetanominuzzo/terminal-to-here) -- VS Code extension to open terminal at any folder
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dev-vault CLI entry point.
|
|
3
|
+
|
|
4
|
+
Routes subcommands to their respective handlers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.logging import RichHandler
|
|
14
|
+
|
|
15
|
+
from . import __version__
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
logger = logging.getLogger("dev_vault")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _setup_logging(verbose: bool) -> None:
|
|
22
|
+
enabled = verbose or os.environ.get("DV_DEBUG", "").strip() in ("1", "true", "yes")
|
|
23
|
+
if not enabled:
|
|
24
|
+
return
|
|
25
|
+
logging.basicConfig(
|
|
26
|
+
level=logging.DEBUG,
|
|
27
|
+
format="%(message)s",
|
|
28
|
+
datefmt="[%X]",
|
|
29
|
+
handlers=[RichHandler(
|
|
30
|
+
console=Console(stderr=True),
|
|
31
|
+
rich_tracebacks=True,
|
|
32
|
+
show_path=False,
|
|
33
|
+
)],
|
|
34
|
+
)
|
|
35
|
+
logger.debug("Verbose mode enabled")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
39
|
+
parser = argparse.ArgumentParser(
|
|
40
|
+
prog="dv",
|
|
41
|
+
description="dev-vault -- Developer secret vault + OIDC token provider",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument("--version", action="version", version=f"dev-vault {__version__}")
|
|
44
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="enable debug output")
|
|
45
|
+
parser.add_argument("--json", action="store_true", help="JSON output")
|
|
46
|
+
|
|
47
|
+
sub = parser.add_subparsers(dest="command")
|
|
48
|
+
|
|
49
|
+
# dv get -- supports both:
|
|
50
|
+
# dv get prod caetano (positional: vault item)
|
|
51
|
+
# dv get prod caetano password (positional: vault item field)
|
|
52
|
+
# dv get caetano (just item, default vault)
|
|
53
|
+
# dv get prod/caetano (slash shorthand still works)
|
|
54
|
+
p_get = sub.add_parser("get", help="retrieve a secret or OIDC token")
|
|
55
|
+
p_get.add_argument("args", nargs="+", help="[vault] item [field] or vault/item[/field]")
|
|
56
|
+
|
|
57
|
+
# dv set -- supports:
|
|
58
|
+
# dv set item field [value]
|
|
59
|
+
# dv set vault item field [value]
|
|
60
|
+
p_set = sub.add_parser("set", help="store a secret")
|
|
61
|
+
p_set.add_argument("args", nargs="+", help="[vault] item field [value]")
|
|
62
|
+
|
|
63
|
+
# dv run
|
|
64
|
+
p_run = sub.add_parser("run", help="run command with secrets injected as env vars")
|
|
65
|
+
p_run.add_argument("-s", "--secret", action="append", help="KEY=ref mapping")
|
|
66
|
+
p_run.add_argument("run_command", nargs=argparse.REMAINDER, help="command to run (after --)")
|
|
67
|
+
|
|
68
|
+
# dv inject
|
|
69
|
+
sub.add_parser("inject", help="replace {{dv://...}} refs in stdin")
|
|
70
|
+
|
|
71
|
+
# dv item
|
|
72
|
+
p_item = sub.add_parser("item", help="manage items")
|
|
73
|
+
item_sub = p_item.add_subparsers(dest="item_action")
|
|
74
|
+
|
|
75
|
+
item_sub.add_parser("list", help="list items")
|
|
76
|
+
p_ic = item_sub.add_parser("create", help="create item")
|
|
77
|
+
p_ic.add_argument("name", help="item name")
|
|
78
|
+
p_ic.add_argument("--kind", choices=["static", "oidc"], default="static")
|
|
79
|
+
p_ic.add_argument("--vault", help="override vault")
|
|
80
|
+
|
|
81
|
+
p_is = item_sub.add_parser("show", help="show item details")
|
|
82
|
+
p_is.add_argument("name", help="item name")
|
|
83
|
+
p_is.add_argument("--vault", help="override vault")
|
|
84
|
+
|
|
85
|
+
p_id = item_sub.add_parser("delete", help="delete item")
|
|
86
|
+
p_id.add_argument("name", help="item name")
|
|
87
|
+
p_id.add_argument("--vault", help="override vault")
|
|
88
|
+
|
|
89
|
+
# dv vault
|
|
90
|
+
p_vault = sub.add_parser("vault", help="manage vaults")
|
|
91
|
+
vault_sub = p_vault.add_subparsers(dest="vault_action")
|
|
92
|
+
vault_sub.add_parser("list", help="list vaults")
|
|
93
|
+
vc = vault_sub.add_parser("create", help="create vault")
|
|
94
|
+
vc.add_argument("name", help="vault name")
|
|
95
|
+
vd = vault_sub.add_parser("delete", help="delete vault")
|
|
96
|
+
vd.add_argument("name", help="vault name")
|
|
97
|
+
|
|
98
|
+
# dv setup
|
|
99
|
+
p_setup = sub.add_parser("setup", help="interactive setup wizard")
|
|
100
|
+
p_setup.add_argument("--reset", action="store_true", help="backup config and start fresh")
|
|
101
|
+
|
|
102
|
+
# dv migrate
|
|
103
|
+
p_migrate = sub.add_parser("migrate", help="import from another tool")
|
|
104
|
+
p_migrate.add_argument("source", nargs="?", default="sso-cli", help="source tool (default: sso-cli)")
|
|
105
|
+
|
|
106
|
+
# dv config
|
|
107
|
+
p_config = sub.add_parser("config", help="show configuration")
|
|
108
|
+
config_sub = p_config.add_subparsers(dest="config_action")
|
|
109
|
+
config_sub.add_parser("show", help="display current config")
|
|
110
|
+
|
|
111
|
+
return parser
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def cli() -> None:
|
|
115
|
+
parser = _build_parser()
|
|
116
|
+
args = parser.parse_args()
|
|
117
|
+
|
|
118
|
+
_setup_logging(args.verbose)
|
|
119
|
+
|
|
120
|
+
# Strip leading '--' from run command
|
|
121
|
+
if args.command == "run" and hasattr(args, "run_command"):
|
|
122
|
+
cmd = getattr(args, "run_command", [])
|
|
123
|
+
if cmd and cmd[0] == "--":
|
|
124
|
+
args.run_command = cmd[1:]
|
|
125
|
+
|
|
126
|
+
if not args.command:
|
|
127
|
+
parser.print_help()
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
# Propagate --json to args
|
|
131
|
+
try:
|
|
132
|
+
_dispatch(args)
|
|
133
|
+
except KeyboardInterrupt:
|
|
134
|
+
print("\nInterrupted.", file=sys.stderr)
|
|
135
|
+
sys.exit(130)
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.debug("Error: %s", e, exc_info=True)
|
|
138
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _dispatch(args) -> None:
|
|
143
|
+
cmd = args.command
|
|
144
|
+
if cmd == "get":
|
|
145
|
+
from .commands.get import run
|
|
146
|
+
run(args)
|
|
147
|
+
elif cmd == "set":
|
|
148
|
+
from .commands.set_cmd import run
|
|
149
|
+
run(args)
|
|
150
|
+
elif cmd == "run":
|
|
151
|
+
from .commands.run import run as run_cmd
|
|
152
|
+
run_cmd(args)
|
|
153
|
+
elif cmd == "inject":
|
|
154
|
+
from .commands.inject import run
|
|
155
|
+
run(args)
|
|
156
|
+
elif cmd == "item":
|
|
157
|
+
from .commands.item import run
|
|
158
|
+
run(args)
|
|
159
|
+
elif cmd == "vault":
|
|
160
|
+
from .commands.vault import run
|
|
161
|
+
run(args)
|
|
162
|
+
elif cmd == "setup":
|
|
163
|
+
from .commands.setup import run
|
|
164
|
+
run(args)
|
|
165
|
+
elif cmd == "migrate":
|
|
166
|
+
from .commands.migrate import run
|
|
167
|
+
run(args)
|
|
168
|
+
elif cmd == "config":
|
|
169
|
+
from .commands.config_cmd import run
|
|
170
|
+
run(args)
|
|
171
|
+
else:
|
|
172
|
+
print(f"Unknown command: {cmd}", file=sys.stderr)
|
|
173
|
+
sys.exit(1)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
cli()
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dv config show -- display current configuration.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
dv config show
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json as json_mod
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
from ..core.config import find_config_path, load_config
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run(args) -> None:
|
|
20
|
+
"""Execute the config command."""
|
|
21
|
+
action = getattr(args, "config_action", "show")
|
|
22
|
+
if action == "show":
|
|
23
|
+
_show(args)
|
|
24
|
+
else:
|
|
25
|
+
print(f"Unknown config action: {action}", file=sys.stderr)
|
|
26
|
+
sys.exit(1)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _show(args) -> None:
|
|
30
|
+
config_path = find_config_path()
|
|
31
|
+
try:
|
|
32
|
+
config = load_config()
|
|
33
|
+
except FileNotFoundError:
|
|
34
|
+
print(f"No config found at: {config_path}", file=sys.stderr)
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
|
|
37
|
+
if getattr(args, "json", False):
|
|
38
|
+
print(json_mod.dumps({"path": config_path, "config": config}, indent=2))
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
print(f"Config: {config_path}\n")
|
|
42
|
+
print(yaml.dump(config, default_flow_style=False, sort_keys=False), end="")
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dv get -- retrieve a secret or OIDC token.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
dv get <item> # default vault, primary field
|
|
6
|
+
dv get <vault> <item> # specific vault
|
|
7
|
+
dv get <vault> <item> <field> # specific vault + field
|
|
8
|
+
dv get vault/item/field # slash shorthand
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import json as json_mod
|
|
13
|
+
import logging
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from ..core.config import ensure_config, get_item, get_default_vault, list_all_items
|
|
17
|
+
from ..core.keyring_store import get_secret
|
|
18
|
+
from ..core.resolver import resolve, resolve_prefix
|
|
19
|
+
from ..providers import PROVIDERS
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run(args) -> None:
|
|
25
|
+
asyncio.run(_get(args))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse_args(args) -> tuple:
|
|
29
|
+
"""Parse positional args into (vault, item, field).
|
|
30
|
+
|
|
31
|
+
Supports:
|
|
32
|
+
["item"] -> (default, item, None)
|
|
33
|
+
["vault", "item"] -> (vault, item, None)
|
|
34
|
+
["vault", "item", "f"] -> (vault, item, f)
|
|
35
|
+
["vault/item"] -> resolve via slash
|
|
36
|
+
["vault/item/field"] -> resolve via slash
|
|
37
|
+
"""
|
|
38
|
+
parts = args.args
|
|
39
|
+
config = ensure_config()
|
|
40
|
+
default_vault = get_default_vault(config)
|
|
41
|
+
|
|
42
|
+
# If single arg with slashes, use resolver
|
|
43
|
+
if len(parts) == 1 and "/" in parts[0]:
|
|
44
|
+
return resolve(parts[0], default_vault)
|
|
45
|
+
|
|
46
|
+
if len(parts) == 1:
|
|
47
|
+
# Could be just an item name (default vault)
|
|
48
|
+
# Or could be a vault name if it matches a vault
|
|
49
|
+
vaults = list(config.get("vaults", {}).keys())
|
|
50
|
+
if parts[0] in vaults:
|
|
51
|
+
# Ambiguous: is it a vault or an item in default vault?
|
|
52
|
+
# Check if it's also an item in default vault
|
|
53
|
+
default_items = config.get("vaults", {}).get(default_vault, {}).get("items", {})
|
|
54
|
+
if parts[0] in default_items:
|
|
55
|
+
return default_vault, parts[0], None
|
|
56
|
+
# It's a vault name but no item specified
|
|
57
|
+
print(f"Error: vault '{parts[0]}' specified but no item name.", file=sys.stderr)
|
|
58
|
+
print(f"Usage: dv get {parts[0]} <item>", file=sys.stderr)
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
return default_vault, parts[0], None
|
|
61
|
+
elif len(parts) == 2:
|
|
62
|
+
return parts[0], parts[1], None
|
|
63
|
+
elif len(parts) == 3:
|
|
64
|
+
return parts[0], parts[1], parts[2]
|
|
65
|
+
else:
|
|
66
|
+
print("Error: too many arguments. Usage: dv get [vault] <item> [field]", file=sys.stderr)
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def _get(args) -> None:
|
|
71
|
+
config = ensure_config()
|
|
72
|
+
vault, item_name, field = _parse_args(args)
|
|
73
|
+
logger.debug("Resolved: vault=%s item=%s field=%s", vault, item_name, field)
|
|
74
|
+
|
|
75
|
+
item = get_item(config, vault, item_name)
|
|
76
|
+
if not item:
|
|
77
|
+
print(f"Error: item '{item_name}' not found in vault '{vault}'.", file=sys.stderr)
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
|
|
80
|
+
kind = item.get("kind", "static")
|
|
81
|
+
|
|
82
|
+
if kind == "oidc" and field is None:
|
|
83
|
+
value = await _get_oidc_token(item, vault, item_name)
|
|
84
|
+
elif kind == "oidc" and field == "access_token":
|
|
85
|
+
value = await _get_oidc_token(item, vault, item_name)
|
|
86
|
+
else:
|
|
87
|
+
resolved_field = field or _primary_field(item)
|
|
88
|
+
if not resolved_field:
|
|
89
|
+
print(f"Error: item '{item_name}' has no fields.", file=sys.stderr)
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
value = get_secret(vault, item_name, resolved_field)
|
|
92
|
+
if value is None:
|
|
93
|
+
print(
|
|
94
|
+
f"Error: no secret for {vault}/{item_name}/{resolved_field}. "
|
|
95
|
+
"Run 'dv set' to store it.",
|
|
96
|
+
file=sys.stderr,
|
|
97
|
+
)
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
|
|
100
|
+
if getattr(args, "json", False):
|
|
101
|
+
print(json_mod.dumps({"vault": vault, "item": item_name, "field": field, "value": value}))
|
|
102
|
+
else:
|
|
103
|
+
print(value)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def _get_oidc_token(item: dict, vault: str, item_name: str) -> str:
|
|
107
|
+
provider_name = item.get("provider", "keycloak")
|
|
108
|
+
provider = PROVIDERS.get(provider_name)
|
|
109
|
+
if not provider:
|
|
110
|
+
print(f"Error: unknown OIDC provider '{provider_name}'.", file=sys.stderr)
|
|
111
|
+
sys.exit(1)
|
|
112
|
+
|
|
113
|
+
item_config = item.get("config", {})
|
|
114
|
+
auth_type = item_config.get("auth_type", "user")
|
|
115
|
+
secret_field = "client_secret" if auth_type == "client" else "password"
|
|
116
|
+
secret = get_secret(vault, item_name, secret_field)
|
|
117
|
+
if secret is None:
|
|
118
|
+
print(
|
|
119
|
+
f"Error: no {secret_field} in keyring for {vault}/{item_name}. "
|
|
120
|
+
"Run 'dv setup' to configure.",
|
|
121
|
+
file=sys.stderr,
|
|
122
|
+
)
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
|
|
125
|
+
return await provider.get_token(item_config, secret)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _primary_field(item: dict) -> str:
|
|
129
|
+
fields = item.get("fields", [])
|
|
130
|
+
if isinstance(fields, list) and fields:
|
|
131
|
+
return fields[0]
|
|
132
|
+
if isinstance(fields, dict) and fields:
|
|
133
|
+
return next(iter(fields))
|
|
134
|
+
return "value"
|