dev-vault 0.1.2__py3-none-any.whl
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/__init__.py +3 -0
- dev_vault/cli.py +177 -0
- dev_vault/commands/__init__.py +0 -0
- dev_vault/commands/config_cmd.py +42 -0
- dev_vault/commands/get.py +134 -0
- dev_vault/commands/inject.py +91 -0
- dev_vault/commands/item.py +122 -0
- dev_vault/commands/migrate.py +108 -0
- dev_vault/commands/run.py +117 -0
- dev_vault/commands/set_cmd.py +83 -0
- dev_vault/commands/setup.py +232 -0
- dev_vault/commands/vault.py +90 -0
- dev_vault/core/__init__.py +0 -0
- dev_vault/core/config.py +165 -0
- dev_vault/core/keyring_store.py +68 -0
- dev_vault/core/manifest.py +56 -0
- dev_vault/core/resolver.py +65 -0
- dev_vault/providers/__init__.py +7 -0
- dev_vault/providers/base.py +21 -0
- dev_vault/providers/keycloak.py +135 -0
- dev_vault/setup_path.py +93 -0
- dev_vault/ui/__init__.py +0 -0
- dev_vault/ui/console.py +6 -0
- dev_vault/utils.py +11 -0
- dev_vault-0.1.2.dist-info/METADATA +170 -0
- dev_vault-0.1.2.dist-info/RECORD +30 -0
- dev_vault-0.1.2.dist-info/WHEEL +5 -0
- dev_vault-0.1.2.dist-info/entry_points.txt +4 -0
- dev_vault-0.1.2.dist-info/licenses/LICENSE +21 -0
- dev_vault-0.1.2.dist-info/top_level.txt +1 -0
dev_vault/__init__.py
ADDED
dev_vault/cli.py
ADDED
|
@@ -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"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dv inject -- replace {{dv://...}} references in stdin.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
dv inject < template.env > .env
|
|
6
|
+
cat template.yaml | dv inject > output.yaml
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from ..core.config import ensure_config, get_item, get_default_vault
|
|
15
|
+
from ..core.keyring_store import get_secret
|
|
16
|
+
from ..core.resolver import resolve
|
|
17
|
+
from ..providers import PROVIDERS
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
_REF_PATTERN = re.compile(r"\{\{(dv://[^}]+)\}\}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run(args) -> None:
|
|
25
|
+
"""Execute the inject command."""
|
|
26
|
+
asyncio.run(_inject(args))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def _inject(args) -> None:
|
|
30
|
+
config = ensure_config()
|
|
31
|
+
default_vault = get_default_vault(config)
|
|
32
|
+
|
|
33
|
+
template = sys.stdin.read()
|
|
34
|
+
refs = _REF_PATTERN.findall(template)
|
|
35
|
+
|
|
36
|
+
if not refs:
|
|
37
|
+
print(template, end="")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
# Resolve all references
|
|
41
|
+
resolved = {}
|
|
42
|
+
for ref in set(refs):
|
|
43
|
+
known_vaults = list(config.get("vaults", {}).keys())
|
|
44
|
+
vault, item_name, field = resolve(ref, default_vault, known_vaults=known_vaults)
|
|
45
|
+
item = get_item(config, vault, item_name)
|
|
46
|
+
if not item:
|
|
47
|
+
print(f"Error: item '{item_name}' not found in vault '{vault}'.", file=sys.stderr)
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
kind = item.get("kind", "static")
|
|
51
|
+
if kind == "oidc" and field is None:
|
|
52
|
+
value = await _get_oidc_token(item, vault, item_name)
|
|
53
|
+
else:
|
|
54
|
+
resolved_field = field or _primary_field(item)
|
|
55
|
+
value = get_secret(vault, item_name, resolved_field)
|
|
56
|
+
if value is None:
|
|
57
|
+
print(f"Error: no secret for {vault}/{item_name}/{resolved_field}.", file=sys.stderr)
|
|
58
|
+
sys.exit(1)
|
|
59
|
+
|
|
60
|
+
resolved[ref] = value
|
|
61
|
+
|
|
62
|
+
# Replace all references
|
|
63
|
+
def replacer(match):
|
|
64
|
+
return resolved[match.group(1)]
|
|
65
|
+
|
|
66
|
+
output = _REF_PATTERN.sub(replacer, template)
|
|
67
|
+
print(output, end="")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def _get_oidc_token(item: dict, vault: str, item_name: str) -> str:
|
|
71
|
+
provider_name = item.get("provider", "keycloak")
|
|
72
|
+
provider = PROVIDERS.get(provider_name)
|
|
73
|
+
if not provider:
|
|
74
|
+
print(f"Error: unknown provider '{provider_name}'.", file=sys.stderr)
|
|
75
|
+
sys.exit(1)
|
|
76
|
+
|
|
77
|
+
item_config = item.get("config", {})
|
|
78
|
+
secret_field = "client_secret" if item_config.get("auth_type") == "client" else "password"
|
|
79
|
+
secret = get_secret(vault, item_name, secret_field)
|
|
80
|
+
if secret is None:
|
|
81
|
+
print(f"Error: no {secret_field} for {vault}/{item_name}.", file=sys.stderr)
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
|
|
84
|
+
return await provider.get_token(item_config, secret)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _primary_field(item: dict) -> str:
|
|
88
|
+
fields = item.get("fields", [])
|
|
89
|
+
if isinstance(fields, list) and fields:
|
|
90
|
+
return fields[0]
|
|
91
|
+
return "value"
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dv item -- manage items in vaults.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
dv item list [-v vault]
|
|
6
|
+
dv item create <name> --kind static|oidc [-v vault]
|
|
7
|
+
dv item show <name> [-v vault]
|
|
8
|
+
dv item delete <name> [-v vault]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json as json_mod
|
|
12
|
+
import logging
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from ..core.config import ensure_config, get_vault, get_item, list_all_items, save_config
|
|
16
|
+
from ..core.keyring_store import delete_secret
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run(args) -> None:
|
|
22
|
+
"""Route item subcommands."""
|
|
23
|
+
action = args.item_action
|
|
24
|
+
if action == "list":
|
|
25
|
+
_list_items(args)
|
|
26
|
+
elif action == "create":
|
|
27
|
+
_create_item(args)
|
|
28
|
+
elif action == "show":
|
|
29
|
+
_show_item(args)
|
|
30
|
+
elif action == "delete":
|
|
31
|
+
_delete_item(args)
|
|
32
|
+
else:
|
|
33
|
+
print(f"Unknown item action: {action}", file=sys.stderr)
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _list_items(args) -> None:
|
|
38
|
+
config = ensure_config()
|
|
39
|
+
vault_filter = getattr(args, "vault", None)
|
|
40
|
+
|
|
41
|
+
items = list_all_items(config)
|
|
42
|
+
if vault_filter:
|
|
43
|
+
items = [i for i in items if i["vault"] == vault_filter]
|
|
44
|
+
|
|
45
|
+
if getattr(args, "json", False):
|
|
46
|
+
print(json_mod.dumps(items, indent=2))
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
if not items:
|
|
50
|
+
print("No items found.", file=sys.stderr)
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
for i in items:
|
|
54
|
+
print(f" {i['vault']}/{i['item']} [{i['kind']}]")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _create_item(args) -> None:
|
|
58
|
+
config = ensure_config()
|
|
59
|
+
vault_name = args.vault or config.get("defaults", {}).get("vault", "default")
|
|
60
|
+
vault = get_vault(config, vault_name)
|
|
61
|
+
items = vault.setdefault("items", {})
|
|
62
|
+
|
|
63
|
+
if args.name in items:
|
|
64
|
+
print(f"Error: item '{args.name}' already exists in vault '{vault_name}'.", file=sys.stderr)
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
|
|
67
|
+
kind = getattr(args, "kind", "static") or "static"
|
|
68
|
+
items[args.name] = {"kind": kind, "fields": []}
|
|
69
|
+
save_config(config)
|
|
70
|
+
|
|
71
|
+
if getattr(args, "json", False):
|
|
72
|
+
print(json_mod.dumps({"vault": vault_name, "item": args.name, "kind": kind, "created": True}))
|
|
73
|
+
else:
|
|
74
|
+
print(f"Created: {vault_name}/{args.name} [{kind}]", file=sys.stderr)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _show_item(args) -> None:
|
|
78
|
+
config = ensure_config()
|
|
79
|
+
vault_name = args.vault or config.get("defaults", {}).get("vault", "default")
|
|
80
|
+
item = get_item(config, vault_name, args.name)
|
|
81
|
+
if not item:
|
|
82
|
+
print(f"Error: item '{args.name}' not found in vault '{vault_name}'.", file=sys.stderr)
|
|
83
|
+
sys.exit(1)
|
|
84
|
+
|
|
85
|
+
if getattr(args, "json", False):
|
|
86
|
+
print(json_mod.dumps({"vault": vault_name, "item": args.name, **item}, indent=2))
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
kind = item.get("kind", "static")
|
|
90
|
+
fields = item.get("fields", [])
|
|
91
|
+
print(f" {vault_name}/{args.name} [{kind}]")
|
|
92
|
+
print(f" Fields: {', '.join(fields) if fields else '(none)'}")
|
|
93
|
+
if kind == "oidc":
|
|
94
|
+
provider = item.get("provider", "keycloak")
|
|
95
|
+
oidc_config = item.get("config", {})
|
|
96
|
+
print(f" Provider: {provider}")
|
|
97
|
+
for k, v in oidc_config.items():
|
|
98
|
+
print(f" {k}: {v}")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _delete_item(args) -> None:
|
|
102
|
+
config = ensure_config()
|
|
103
|
+
vault_name = args.vault or config.get("defaults", {}).get("vault", "default")
|
|
104
|
+
vault_data = config.get("vaults", {}).get(vault_name, {})
|
|
105
|
+
items = vault_data.get("items", {})
|
|
106
|
+
|
|
107
|
+
if args.name not in items:
|
|
108
|
+
print(f"Error: item '{args.name}' not found in vault '{vault_name}'.", file=sys.stderr)
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
|
|
111
|
+
item = items[args.name]
|
|
112
|
+
fields = item.get("fields", [])
|
|
113
|
+
for field in fields:
|
|
114
|
+
delete_secret(vault_name, args.name, field)
|
|
115
|
+
|
|
116
|
+
del items[args.name]
|
|
117
|
+
save_config(config)
|
|
118
|
+
|
|
119
|
+
if getattr(args, "json", False):
|
|
120
|
+
print(json_mod.dumps({"vault": vault_name, "item": args.name, "deleted": True}))
|
|
121
|
+
else:
|
|
122
|
+
print(f"Deleted: {vault_name}/{args.name}", file=sys.stderr)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dv migrate sso-cli -- import config and secrets from sso-cli.
|
|
3
|
+
|
|
4
|
+
Reads ~/sso_config.yaml and copies secrets from the sso-cli keyring
|
|
5
|
+
to the dev-vault keyring.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json as json_mod
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
from ..core.config import ensure_config, save_config
|
|
16
|
+
from ..core.keyring_store import store_secret, get_secret_legacy
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run(args) -> None:
|
|
22
|
+
"""Execute the migrate command."""
|
|
23
|
+
source = getattr(args, "source", "sso-cli")
|
|
24
|
+
if source != "sso-cli":
|
|
25
|
+
print(f"Error: unknown migration source '{source}'. Only 'sso-cli' is supported.", file=sys.stderr)
|
|
26
|
+
sys.exit(1)
|
|
27
|
+
|
|
28
|
+
_migrate_sso_cli(args)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _migrate_sso_cli(args) -> None:
|
|
32
|
+
"""Import sso-cli config and secrets into dev-vault."""
|
|
33
|
+
# Find sso-cli config
|
|
34
|
+
sso_config_path = os.environ.get("SSO_CONFIG_PATH", "")
|
|
35
|
+
if not sso_config_path or not os.path.exists(sso_config_path):
|
|
36
|
+
sso_config_path = os.path.expanduser("~/sso_config.yaml")
|
|
37
|
+
if not os.path.exists(sso_config_path):
|
|
38
|
+
print(f"Error: sso-cli config not found at {sso_config_path}.", file=sys.stderr)
|
|
39
|
+
sys.exit(1)
|
|
40
|
+
|
|
41
|
+
print(f"Reading sso-cli config from: {sso_config_path}", file=sys.stderr)
|
|
42
|
+
|
|
43
|
+
with open(sso_config_path, "r", encoding="utf-8") as f:
|
|
44
|
+
raw = yaml.safe_load(f) or {}
|
|
45
|
+
|
|
46
|
+
env_cfg = raw.get("environments", raw)
|
|
47
|
+
if not isinstance(env_cfg, dict):
|
|
48
|
+
print("Error: invalid sso-cli config format.", file=sys.stderr)
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
config = ensure_config()
|
|
52
|
+
migrated = 0
|
|
53
|
+
|
|
54
|
+
_LEGACY = {"password": "user", "client_credentials": "client"}
|
|
55
|
+
|
|
56
|
+
for env_key, env_data in env_cfg.items():
|
|
57
|
+
if not isinstance(env_data, dict) or not env_data.get("sso_url"):
|
|
58
|
+
print(f" Skipping invalid environment: {env_key}", file=sys.stderr)
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
# Create vault for this environment
|
|
62
|
+
vault = config.setdefault("vaults", {}).setdefault(env_key, {"items": {}})
|
|
63
|
+
items = vault.setdefault("items", {})
|
|
64
|
+
|
|
65
|
+
users = env_data.get("users", {})
|
|
66
|
+
for user_key, user_data in users.items():
|
|
67
|
+
if not isinstance(user_data, dict):
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
auth_type = _LEGACY.get(user_data.get("auth_type", ""), user_data.get("auth_type", "user"))
|
|
71
|
+
|
|
72
|
+
# Create OIDC item
|
|
73
|
+
if auth_type == "client":
|
|
74
|
+
secret_field = "client_secret"
|
|
75
|
+
item_config = {
|
|
76
|
+
"sso_url": env_data["sso_url"],
|
|
77
|
+
"auth_type": "client",
|
|
78
|
+
"client_id": user_data.get("client_id", user_key),
|
|
79
|
+
}
|
|
80
|
+
else:
|
|
81
|
+
secret_field = "password"
|
|
82
|
+
item_config = {
|
|
83
|
+
"sso_url": env_data["sso_url"],
|
|
84
|
+
"auth_type": "user",
|
|
85
|
+
"email": user_data.get("email", user_key),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
items[user_key] = {
|
|
89
|
+
"kind": "oidc",
|
|
90
|
+
"provider": "keycloak",
|
|
91
|
+
"config": item_config,
|
|
92
|
+
"fields": [secret_field],
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Migrate secret from sso-cli keyring
|
|
96
|
+
old_secret = get_secret_legacy(env_key, user_key)
|
|
97
|
+
if old_secret:
|
|
98
|
+
store_secret(env_key, user_key, secret_field, old_secret)
|
|
99
|
+
print(f" Migrated: {env_key}/{user_key} ({auth_type})", file=sys.stderr)
|
|
100
|
+
migrated += 1
|
|
101
|
+
else:
|
|
102
|
+
print(f" Warning: no keyring secret for {env_key}/{user_key}", file=sys.stderr)
|
|
103
|
+
|
|
104
|
+
path = save_config(config)
|
|
105
|
+
print(f"\nMigrated {migrated} item(s). Config saved to: {path}", file=sys.stderr)
|
|
106
|
+
|
|
107
|
+
if getattr(args, "json", False):
|
|
108
|
+
print(json_mod.dumps({"migrated": migrated, "config_path": path}))
|