bt-cli 0.4.7__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.
- bt_cli/__init__.py +3 -0
- bt_cli/cli.py +830 -0
- bt_cli/commands/__init__.py +1 -0
- bt_cli/commands/configure.py +415 -0
- bt_cli/commands/learn.py +229 -0
- bt_cli/commands/quick.py +784 -0
- bt_cli/core/__init__.py +1 -0
- bt_cli/core/auth.py +213 -0
- bt_cli/core/client.py +313 -0
- bt_cli/core/config.py +393 -0
- bt_cli/core/config_file.py +420 -0
- bt_cli/core/csv_utils.py +91 -0
- bt_cli/core/errors.py +247 -0
- bt_cli/core/output.py +205 -0
- bt_cli/core/prompts.py +87 -0
- bt_cli/core/rest_debug.py +221 -0
- bt_cli/data/CLAUDE.md +88 -0
- bt_cli/data/__init__.py +0 -0
- bt_cli/data/skills/bt/SKILL.md +98 -0
- bt_cli/data/skills/entitle/SKILL.md +159 -0
- bt_cli/data/skills/epmw/SKILL.md +145 -0
- bt_cli/data/skills/pra/SKILL.md +149 -0
- bt_cli/data/skills/pws/SKILL.md +197 -0
- bt_cli/entitle/__init__.py +1 -0
- bt_cli/entitle/client/__init__.py +5 -0
- bt_cli/entitle/client/base.py +443 -0
- bt_cli/entitle/commands/__init__.py +24 -0
- bt_cli/entitle/commands/accounts.py +53 -0
- bt_cli/entitle/commands/applications.py +39 -0
- bt_cli/entitle/commands/auth.py +68 -0
- bt_cli/entitle/commands/bundles.py +218 -0
- bt_cli/entitle/commands/integrations.py +60 -0
- bt_cli/entitle/commands/permissions.py +70 -0
- bt_cli/entitle/commands/policies.py +97 -0
- bt_cli/entitle/commands/resources.py +131 -0
- bt_cli/entitle/commands/roles.py +74 -0
- bt_cli/entitle/commands/users.py +123 -0
- bt_cli/entitle/commands/workflows.py +187 -0
- bt_cli/entitle/models/__init__.py +31 -0
- bt_cli/entitle/models/bundle.py +28 -0
- bt_cli/entitle/models/common.py +37 -0
- bt_cli/entitle/models/integration.py +30 -0
- bt_cli/entitle/models/permission.py +27 -0
- bt_cli/entitle/models/policy.py +25 -0
- bt_cli/entitle/models/resource.py +29 -0
- bt_cli/entitle/models/role.py +28 -0
- bt_cli/entitle/models/user.py +24 -0
- bt_cli/entitle/models/workflow.py +55 -0
- bt_cli/epmw/__init__.py +1 -0
- bt_cli/epmw/client/__init__.py +5 -0
- bt_cli/epmw/client/base.py +848 -0
- bt_cli/epmw/commands/__init__.py +33 -0
- bt_cli/epmw/commands/audits.py +250 -0
- bt_cli/epmw/commands/auth.py +55 -0
- bt_cli/epmw/commands/computers.py +140 -0
- bt_cli/epmw/commands/events.py +233 -0
- bt_cli/epmw/commands/groups.py +215 -0
- bt_cli/epmw/commands/policies.py +673 -0
- bt_cli/epmw/commands/quick.py +348 -0
- bt_cli/epmw/commands/requests.py +224 -0
- bt_cli/epmw/commands/roles.py +78 -0
- bt_cli/epmw/commands/tasks.py +38 -0
- bt_cli/epmw/commands/users.py +219 -0
- bt_cli/epmw/models/__init__.py +1 -0
- bt_cli/pra/__init__.py +1 -0
- bt_cli/pra/client/__init__.py +5 -0
- bt_cli/pra/client/base.py +618 -0
- bt_cli/pra/commands/__init__.py +30 -0
- bt_cli/pra/commands/auth.py +55 -0
- bt_cli/pra/commands/import_export.py +442 -0
- bt_cli/pra/commands/jump_clients.py +139 -0
- bt_cli/pra/commands/jump_groups.py +146 -0
- bt_cli/pra/commands/jump_items.py +638 -0
- bt_cli/pra/commands/jumpoints.py +95 -0
- bt_cli/pra/commands/policies.py +197 -0
- bt_cli/pra/commands/quick.py +470 -0
- bt_cli/pra/commands/teams.py +81 -0
- bt_cli/pra/commands/users.py +87 -0
- bt_cli/pra/commands/vault.py +564 -0
- bt_cli/pra/models/__init__.py +27 -0
- bt_cli/pra/models/common.py +12 -0
- bt_cli/pra/models/jump_client.py +25 -0
- bt_cli/pra/models/jump_group.py +15 -0
- bt_cli/pra/models/jump_item.py +72 -0
- bt_cli/pra/models/jumpoint.py +19 -0
- bt_cli/pra/models/team.py +14 -0
- bt_cli/pra/models/user.py +17 -0
- bt_cli/pra/models/vault.py +45 -0
- bt_cli/pws/__init__.py +1 -0
- bt_cli/pws/client/__init__.py +5 -0
- bt_cli/pws/client/base.py +356 -0
- bt_cli/pws/client/beyondinsight.py +869 -0
- bt_cli/pws/client/passwordsafe.py +1786 -0
- bt_cli/pws/commands/__init__.py +33 -0
- bt_cli/pws/commands/accounts.py +372 -0
- bt_cli/pws/commands/assets.py +311 -0
- bt_cli/pws/commands/auth.py +166 -0
- bt_cli/pws/commands/clouds.py +221 -0
- bt_cli/pws/commands/config.py +344 -0
- bt_cli/pws/commands/credentials.py +347 -0
- bt_cli/pws/commands/databases.py +306 -0
- bt_cli/pws/commands/directories.py +199 -0
- bt_cli/pws/commands/functional.py +298 -0
- bt_cli/pws/commands/import_export.py +452 -0
- bt_cli/pws/commands/platforms.py +118 -0
- bt_cli/pws/commands/quick.py +1646 -0
- bt_cli/pws/commands/search.py +256 -0
- bt_cli/pws/commands/secrets.py +1343 -0
- bt_cli/pws/commands/systems.py +389 -0
- bt_cli/pws/commands/users.py +415 -0
- bt_cli/pws/commands/workgroups.py +166 -0
- bt_cli/pws/config.py +18 -0
- bt_cli/pws/models/__init__.py +19 -0
- bt_cli/pws/models/account.py +186 -0
- bt_cli/pws/models/asset.py +102 -0
- bt_cli/pws/models/common.py +132 -0
- bt_cli/pws/models/system.py +121 -0
- bt_cli-0.4.7.dist-info/METADATA +172 -0
- bt_cli-0.4.7.dist-info/RECORD +121 -0
- bt_cli-0.4.7.dist-info/WHEEL +4 -0
- bt_cli-0.4.7.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
"""Vault account commands."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from bt_cli.core.output import OutputFormat, print_table, print_json, print_error, print_success, print_api_error
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(no_args_is_help=True)
|
|
11
|
+
|
|
12
|
+
# Account subcommands
|
|
13
|
+
accounts_app = typer.Typer(no_args_is_help=True, help="Vault accounts")
|
|
14
|
+
app.add_typer(accounts_app, name="accounts")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@accounts_app.command("list")
|
|
18
|
+
def list_vault_accounts(
|
|
19
|
+
account_type: Optional[str] = typer.Option(
|
|
20
|
+
None, "--type", "-t",
|
|
21
|
+
help="Filter by type: username_password, ssh, ssh_ca, windows_local, windows_domain, etc."
|
|
22
|
+
),
|
|
23
|
+
name: Optional[str] = typer.Option(None, "--name", "-n", help="Filter by name"),
|
|
24
|
+
details: bool = typer.Option(False, "--details", "-d", help="Fetch full details (slower, includes username/checkout status)"),
|
|
25
|
+
output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
|
|
26
|
+
):
|
|
27
|
+
"""List Vault accounts."""
|
|
28
|
+
from bt_cli.pra.client import get_client
|
|
29
|
+
from rich.console import Console
|
|
30
|
+
|
|
31
|
+
console = Console()
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
client = get_client()
|
|
35
|
+
accounts = client.list_vault_accounts(account_type=account_type, name=name)
|
|
36
|
+
|
|
37
|
+
# If details requested, fetch full info for each account
|
|
38
|
+
if details and accounts:
|
|
39
|
+
console.print(f"[dim]Fetching details for {len(accounts)} accounts...[/dim]")
|
|
40
|
+
detailed_accounts = []
|
|
41
|
+
for acc in accounts:
|
|
42
|
+
try:
|
|
43
|
+
full = client.get_vault_account(acc["id"])
|
|
44
|
+
# Add checkout status
|
|
45
|
+
if full.get("last_checkout_timestamp"):
|
|
46
|
+
full["checkout_status"] = "Checked Out"
|
|
47
|
+
else:
|
|
48
|
+
full["checkout_status"] = "-"
|
|
49
|
+
detailed_accounts.append(full)
|
|
50
|
+
except Exception:
|
|
51
|
+
detailed_accounts.append(acc)
|
|
52
|
+
accounts = detailed_accounts
|
|
53
|
+
|
|
54
|
+
if output == OutputFormat.JSON:
|
|
55
|
+
print_json(accounts)
|
|
56
|
+
else:
|
|
57
|
+
if details:
|
|
58
|
+
columns = [
|
|
59
|
+
("ID", "id"),
|
|
60
|
+
("Name", "name"),
|
|
61
|
+
("Type", "type"),
|
|
62
|
+
("Username", "username"),
|
|
63
|
+
("Last Checkout", "last_checkout_timestamp"),
|
|
64
|
+
("Description", "description"),
|
|
65
|
+
]
|
|
66
|
+
else:
|
|
67
|
+
columns = [
|
|
68
|
+
("ID", "id"),
|
|
69
|
+
("Name", "name"),
|
|
70
|
+
("Type", "type"),
|
|
71
|
+
("Group ID", "account_group_id"),
|
|
72
|
+
("Description", "description"),
|
|
73
|
+
]
|
|
74
|
+
print_table(accounts, columns, title="Vault Accounts")
|
|
75
|
+
except httpx.HTTPStatusError as e:
|
|
76
|
+
print_api_error(e, "list vault accounts")
|
|
77
|
+
raise typer.Exit(1)
|
|
78
|
+
except httpx.RequestError as e:
|
|
79
|
+
print_api_error(e, "list vault accounts")
|
|
80
|
+
raise typer.Exit(1)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
print_api_error(e, "list vault accounts")
|
|
83
|
+
raise typer.Exit(1)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@accounts_app.command("get")
|
|
87
|
+
def get_vault_account(
|
|
88
|
+
account_id: int = typer.Argument(..., help="Vault Account ID"),
|
|
89
|
+
output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
|
|
90
|
+
):
|
|
91
|
+
"""Get Vault account details."""
|
|
92
|
+
from bt_cli.pra.client import get_client
|
|
93
|
+
from rich.console import Console
|
|
94
|
+
from rich.panel import Panel
|
|
95
|
+
|
|
96
|
+
console = Console()
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
client = get_client()
|
|
100
|
+
acc = client.get_vault_account(account_id)
|
|
101
|
+
|
|
102
|
+
if output == OutputFormat.JSON:
|
|
103
|
+
print_json(acc)
|
|
104
|
+
else:
|
|
105
|
+
name = acc.get("name", "")
|
|
106
|
+
acc_type = acc.get("type", "")
|
|
107
|
+
username = acc.get("username", "") or "-"
|
|
108
|
+
description = acc.get("description", "") or "-"
|
|
109
|
+
group_id = acc.get("account_group_id", "") or "-"
|
|
110
|
+
last_checkout = acc.get("last_checkout_timestamp", "") or "-"
|
|
111
|
+
personal = "Yes" if acc.get("personal") else "No"
|
|
112
|
+
|
|
113
|
+
console.print(Panel(
|
|
114
|
+
f"[bold]{name}[/bold]\n\n"
|
|
115
|
+
f"[dim]Type:[/dim] {acc_type}\n"
|
|
116
|
+
f"[dim]Username:[/dim] {username}\n"
|
|
117
|
+
f"[dim]Description:[/dim] {description}\n"
|
|
118
|
+
f"[dim]Group ID:[/dim] {group_id}\n"
|
|
119
|
+
f"[dim]Personal:[/dim] {personal}\n"
|
|
120
|
+
f"[dim]Last Checkout:[/dim] {last_checkout}",
|
|
121
|
+
title="Vault Account Details",
|
|
122
|
+
subtitle=f"ID: {acc.get('id', '')}",
|
|
123
|
+
))
|
|
124
|
+
except httpx.HTTPStatusError as e:
|
|
125
|
+
print_api_error(e, "get vault account")
|
|
126
|
+
raise typer.Exit(1)
|
|
127
|
+
except httpx.RequestError as e:
|
|
128
|
+
print_api_error(e, "get vault account")
|
|
129
|
+
raise typer.Exit(1)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
print_api_error(e, "get vault account")
|
|
132
|
+
raise typer.Exit(1)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@accounts_app.command("create")
|
|
136
|
+
def create_vault_account(
|
|
137
|
+
name: str = typer.Option(..., "--name", "-n", help="Account name"),
|
|
138
|
+
account_type: str = typer.Option(
|
|
139
|
+
..., "--type", "-t",
|
|
140
|
+
help="Account type: username_password, ssh, or ssh_ca"
|
|
141
|
+
),
|
|
142
|
+
username: Optional[str] = typer.Option(None, "--username", "-u", help="Username (for username_password)"),
|
|
143
|
+
password: Optional[str] = typer.Option(None, "--password", "-p", help="Password (for username_password)"),
|
|
144
|
+
private_key: Optional[str] = typer.Option(None, "--private-key", help="SSH private key (for ssh type)"),
|
|
145
|
+
private_key_file: Optional[str] = typer.Option(None, "--private-key-file", help="Path to SSH private key file"),
|
|
146
|
+
description: Optional[str] = typer.Option(None, "--description", "-d", help="Account description"),
|
|
147
|
+
personal: bool = typer.Option(False, "--personal", help="Mark as personal account"),
|
|
148
|
+
output: OutputFormat = typer.Option(OutputFormat.JSON, "--output", "-o"),
|
|
149
|
+
):
|
|
150
|
+
"""Create a Vault account.
|
|
151
|
+
|
|
152
|
+
Examples:
|
|
153
|
+
# Create username/password account
|
|
154
|
+
bt pra vault accounts create -n "db-admin" -t username_password -u "admin" -p "secret"
|
|
155
|
+
|
|
156
|
+
# Create SSH key account
|
|
157
|
+
bt pra vault accounts create -n "deploy-key" -t ssh --private-key-file ~/.ssh/id_rsa
|
|
158
|
+
|
|
159
|
+
# Create SSH CA account (for certificate-based auth)
|
|
160
|
+
bt pra vault accounts create -n "ssh-ca" -t ssh_ca --private-key-file /path/to/ca_key
|
|
161
|
+
"""
|
|
162
|
+
from bt_cli.pra.client import get_client
|
|
163
|
+
from pathlib import Path
|
|
164
|
+
|
|
165
|
+
# Validate account type
|
|
166
|
+
valid_types = ["username_password", "ssh", "ssh_ca"]
|
|
167
|
+
if account_type not in valid_types:
|
|
168
|
+
print_error(f"Invalid account type '{account_type}'. Must be one of: {', '.join(valid_types)}")
|
|
169
|
+
raise typer.Exit(1)
|
|
170
|
+
|
|
171
|
+
# Build account data
|
|
172
|
+
data = {
|
|
173
|
+
"name": name,
|
|
174
|
+
"type": account_type,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if username:
|
|
178
|
+
data["username"] = username
|
|
179
|
+
if password:
|
|
180
|
+
data["password"] = password
|
|
181
|
+
if description:
|
|
182
|
+
data["description"] = description
|
|
183
|
+
if personal:
|
|
184
|
+
data["personal"] = True
|
|
185
|
+
|
|
186
|
+
# Handle private key (from file or direct)
|
|
187
|
+
if private_key_file:
|
|
188
|
+
key_path = Path(private_key_file).expanduser()
|
|
189
|
+
if not key_path.exists():
|
|
190
|
+
print_error(f"Private key file not found: {private_key_file}")
|
|
191
|
+
raise typer.Exit(1)
|
|
192
|
+
data["private_key"] = key_path.read_text()
|
|
193
|
+
elif private_key:
|
|
194
|
+
data["private_key"] = private_key
|
|
195
|
+
|
|
196
|
+
# Validate required fields per type
|
|
197
|
+
if account_type == "username_password":
|
|
198
|
+
if not username:
|
|
199
|
+
print_error("Username is required for username_password type")
|
|
200
|
+
raise typer.Exit(1)
|
|
201
|
+
elif account_type in ["ssh", "ssh_ca"]:
|
|
202
|
+
if not data.get("private_key"):
|
|
203
|
+
print_error(f"Private key is required for {account_type} type (use --private-key or --private-key-file)")
|
|
204
|
+
raise typer.Exit(1)
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
client = get_client()
|
|
208
|
+
result = client.create_vault_account(data)
|
|
209
|
+
print_success(f"Created vault account: {result.get('name')} (ID: {result.get('id')})")
|
|
210
|
+
|
|
211
|
+
if output == OutputFormat.JSON:
|
|
212
|
+
print_json(result)
|
|
213
|
+
else:
|
|
214
|
+
typer.echo(f"ID: {result.get('id')}")
|
|
215
|
+
typer.echo(f"Name: {result.get('name')}")
|
|
216
|
+
typer.echo(f"Type: {result.get('type')}")
|
|
217
|
+
except httpx.HTTPStatusError as e:
|
|
218
|
+
print_api_error(e, "create vault account")
|
|
219
|
+
raise typer.Exit(1)
|
|
220
|
+
except httpx.RequestError as e:
|
|
221
|
+
print_api_error(e, "create vault account")
|
|
222
|
+
raise typer.Exit(1)
|
|
223
|
+
except Exception as e:
|
|
224
|
+
print_api_error(e, "create vault account")
|
|
225
|
+
raise typer.Exit(1)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@accounts_app.command("delete")
|
|
229
|
+
def delete_vault_account(
|
|
230
|
+
account_id: int = typer.Argument(..., help="Vault Account ID to delete"),
|
|
231
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
|
232
|
+
):
|
|
233
|
+
"""Delete a Vault account."""
|
|
234
|
+
from bt_cli.pra.client import get_client
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
client = get_client()
|
|
238
|
+
|
|
239
|
+
# Get account details for confirmation
|
|
240
|
+
account = client.get_vault_account(account_id)
|
|
241
|
+
account_name = account.get("name", f"ID {account_id}")
|
|
242
|
+
|
|
243
|
+
if not force:
|
|
244
|
+
typer.confirm(f"Delete vault account '{account_name}'?", abort=True)
|
|
245
|
+
|
|
246
|
+
client.delete_vault_account(account_id)
|
|
247
|
+
print_success(f"Deleted vault account: {account_name}")
|
|
248
|
+
except httpx.HTTPStatusError as e:
|
|
249
|
+
print_api_error(e, "delete vault account")
|
|
250
|
+
raise typer.Exit(1)
|
|
251
|
+
except httpx.RequestError as e:
|
|
252
|
+
print_api_error(e, "delete vault account")
|
|
253
|
+
raise typer.Exit(1)
|
|
254
|
+
except Exception as e:
|
|
255
|
+
print_api_error(e, "delete vault account")
|
|
256
|
+
raise typer.Exit(1)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@accounts_app.command("get-user-data")
|
|
260
|
+
def get_user_data(
|
|
261
|
+
account_id: int = typer.Argument(..., help="SSH CA Vault Account ID"),
|
|
262
|
+
username: str = typer.Option("ec2-admin", "--username", "-u", help="Username to create on target host"),
|
|
263
|
+
sudo: bool = typer.Option(True, "--sudo/--no-sudo", help="Grant passwordless sudo"),
|
|
264
|
+
shell: str = typer.Option("/bin/bash", "--shell", "-s", help="User shell"),
|
|
265
|
+
):
|
|
266
|
+
"""Generate EC2 user-data script for SSH CA provisioning.
|
|
267
|
+
|
|
268
|
+
Creates a complete cloud-init script that:
|
|
269
|
+
- Creates the specified user
|
|
270
|
+
- Configures SSH to trust the CA public key
|
|
271
|
+
- Optionally grants passwordless sudo
|
|
272
|
+
|
|
273
|
+
Use this output as EC2 user-data when launching instances.
|
|
274
|
+
|
|
275
|
+
Example:
|
|
276
|
+
# Generate and save to file
|
|
277
|
+
bt pra vault accounts get-user-data 31 --username ec2-admin > user-data.sh
|
|
278
|
+
|
|
279
|
+
# Use with AWS CLI
|
|
280
|
+
aws ec2 run-instances ... --user-data file://user-data.sh
|
|
281
|
+
"""
|
|
282
|
+
from bt_cli.pra.client import get_client
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
client = get_client()
|
|
286
|
+
account = client.get_vault_account(account_id)
|
|
287
|
+
|
|
288
|
+
if account.get("type") != "ssh_ca":
|
|
289
|
+
print_error(f"Account {account_id} is type '{account.get('type')}', not 'ssh_ca'")
|
|
290
|
+
raise typer.Exit(1)
|
|
291
|
+
|
|
292
|
+
public_key = account.get("public_key")
|
|
293
|
+
if not public_key:
|
|
294
|
+
print_error(f"Account {account_id} has no public_key")
|
|
295
|
+
raise typer.Exit(1)
|
|
296
|
+
|
|
297
|
+
# Generate user-data script
|
|
298
|
+
sudo_line = f'echo "{username} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/{username}' if sudo else "# sudo not configured"
|
|
299
|
+
|
|
300
|
+
script = f'''#!/bin/bash
|
|
301
|
+
# Generated by bt-cli for PRA SSH CA authentication
|
|
302
|
+
# Vault Account ID: {account_id}
|
|
303
|
+
# Account Name: {account.get("name", "unknown")}
|
|
304
|
+
|
|
305
|
+
set -e
|
|
306
|
+
|
|
307
|
+
# Create user
|
|
308
|
+
useradd -m -s {shell} {username} 2>/dev/null || true
|
|
309
|
+
mkdir -p /home/{username}/.ssh
|
|
310
|
+
chown {username}:{username} /home/{username}/.ssh
|
|
311
|
+
chmod 700 /home/{username}/.ssh
|
|
312
|
+
|
|
313
|
+
# Configure SSH CA trust
|
|
314
|
+
echo "TrustedUserCAKeys /etc/ssh/trusted-user-ca-keys.pub" >> /etc/ssh/sshd_config
|
|
315
|
+
cat > /etc/ssh/trusted-user-ca-keys.pub << 'CAKEY'
|
|
316
|
+
{public_key}
|
|
317
|
+
CAKEY
|
|
318
|
+
|
|
319
|
+
# Configure sudo
|
|
320
|
+
{sudo_line}
|
|
321
|
+
|
|
322
|
+
# Restart SSH
|
|
323
|
+
systemctl restart sshd || service sshd restart
|
|
324
|
+
|
|
325
|
+
echo "SSH CA provisioning complete for user: {username}"
|
|
326
|
+
'''
|
|
327
|
+
typer.echo(script)
|
|
328
|
+
|
|
329
|
+
except httpx.HTTPStatusError as e:
|
|
330
|
+
print_api_error(e, "get user-data")
|
|
331
|
+
raise typer.Exit(1)
|
|
332
|
+
except httpx.RequestError as e:
|
|
333
|
+
print_api_error(e, "get user-data")
|
|
334
|
+
raise typer.Exit(1)
|
|
335
|
+
except Exception as e:
|
|
336
|
+
print_api_error(e, "get user-data")
|
|
337
|
+
raise typer.Exit(1)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@accounts_app.command("get-public-key")
|
|
341
|
+
def get_public_key(
|
|
342
|
+
account_id: int = typer.Argument(..., help="SSH CA Vault Account ID"),
|
|
343
|
+
authorized_keys: bool = typer.Option(
|
|
344
|
+
True, "--authorized-keys/--raw",
|
|
345
|
+
help="Output in authorized_keys format (with cert-authority prefix)"
|
|
346
|
+
),
|
|
347
|
+
):
|
|
348
|
+
"""Get SSH CA public key for provisioning hosts.
|
|
349
|
+
|
|
350
|
+
Retrieves the public key from an SSH CA vault account, ready to add
|
|
351
|
+
to a user's authorized_keys file for certificate-based authentication.
|
|
352
|
+
|
|
353
|
+
Example usage in EC2 user-data:
|
|
354
|
+
|
|
355
|
+
PRA_PUBLIC_KEY=$(bt pra vault accounts get-public-key 3)
|
|
356
|
+
echo "$PRA_PUBLIC_KEY" >> /home/admin/.ssh/authorized_keys
|
|
357
|
+
|
|
358
|
+
The output includes the 'cert-authority' prefix required for SSH CA auth.
|
|
359
|
+
"""
|
|
360
|
+
from bt_cli.pra.client import get_client
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
client = get_client()
|
|
364
|
+
account = client.get_vault_account(account_id)
|
|
365
|
+
|
|
366
|
+
if account.get("type") != "ssh_ca":
|
|
367
|
+
print_error(f"Account {account_id} is type '{account.get('type')}', not 'ssh_ca'")
|
|
368
|
+
raise typer.Exit(1)
|
|
369
|
+
|
|
370
|
+
public_key = account.get("public_key")
|
|
371
|
+
if not public_key:
|
|
372
|
+
print_error(f"Account {account_id} has no public_key")
|
|
373
|
+
raise typer.Exit(1)
|
|
374
|
+
|
|
375
|
+
# Output just the key - ready for authorized_keys or scripting
|
|
376
|
+
typer.echo(public_key)
|
|
377
|
+
|
|
378
|
+
except httpx.HTTPStatusError as e:
|
|
379
|
+
print_api_error(e, "get public key")
|
|
380
|
+
raise typer.Exit(1)
|
|
381
|
+
except httpx.RequestError as e:
|
|
382
|
+
print_api_error(e, "get public key")
|
|
383
|
+
raise typer.Exit(1)
|
|
384
|
+
except Exception as e:
|
|
385
|
+
print_api_error(e, "get public key")
|
|
386
|
+
raise typer.Exit(1)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@accounts_app.command("checkout")
|
|
390
|
+
def checkout_vault_account(
|
|
391
|
+
account_id: int = typer.Argument(..., help="Vault Account ID"),
|
|
392
|
+
output: OutputFormat = typer.Option(OutputFormat.JSON, "--output", "-o"),
|
|
393
|
+
):
|
|
394
|
+
"""Check out a Vault account's credentials."""
|
|
395
|
+
from bt_cli.pra.client import get_client
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
client = get_client()
|
|
399
|
+
credential = client.checkout_vault_account(account_id)
|
|
400
|
+
print_success(f"Checked out vault account {account_id}")
|
|
401
|
+
|
|
402
|
+
if output == OutputFormat.JSON:
|
|
403
|
+
print_json(credential)
|
|
404
|
+
else:
|
|
405
|
+
if credential.get("password"):
|
|
406
|
+
typer.echo(f"Password: {credential['password']}")
|
|
407
|
+
if credential.get("private_key"):
|
|
408
|
+
typer.echo(f"Private Key:\n{credential['private_key']}")
|
|
409
|
+
if credential.get("passphrase"):
|
|
410
|
+
typer.echo(f"Passphrase: {credential['passphrase']}")
|
|
411
|
+
except httpx.HTTPStatusError as e:
|
|
412
|
+
print_api_error(e, "checkout vault account")
|
|
413
|
+
raise typer.Exit(1)
|
|
414
|
+
except httpx.RequestError as e:
|
|
415
|
+
print_api_error(e, "checkout vault account")
|
|
416
|
+
raise typer.Exit(1)
|
|
417
|
+
except Exception as e:
|
|
418
|
+
print_api_error(e, "checkout vault account")
|
|
419
|
+
raise typer.Exit(1)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
@accounts_app.command("checkin")
|
|
423
|
+
def checkin_vault_account(
|
|
424
|
+
account_id: int = typer.Argument(..., help="Vault Account ID"),
|
|
425
|
+
):
|
|
426
|
+
"""Check in a Vault account."""
|
|
427
|
+
from bt_cli.pra.client import get_client
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
client = get_client()
|
|
431
|
+
client.checkin_vault_account(account_id)
|
|
432
|
+
print_success(f"Checked in vault account {account_id}")
|
|
433
|
+
except httpx.HTTPStatusError as e:
|
|
434
|
+
print_api_error(e, "checkin vault account")
|
|
435
|
+
raise typer.Exit(1)
|
|
436
|
+
except httpx.RequestError as e:
|
|
437
|
+
print_api_error(e, "checkin vault account")
|
|
438
|
+
raise typer.Exit(1)
|
|
439
|
+
except Exception as e:
|
|
440
|
+
print_api_error(e, "checkin vault account")
|
|
441
|
+
raise typer.Exit(1)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@accounts_app.command("force-checkin")
|
|
445
|
+
def force_checkin_vault_account(
|
|
446
|
+
account_id: int = typer.Argument(..., help="Vault Account ID"),
|
|
447
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
|
448
|
+
):
|
|
449
|
+
"""Force check in a Vault account (admin operation)."""
|
|
450
|
+
from bt_cli.pra.client import get_client
|
|
451
|
+
|
|
452
|
+
if not force:
|
|
453
|
+
typer.confirm(f"Force checkin vault account {account_id}?", abort=True)
|
|
454
|
+
|
|
455
|
+
try:
|
|
456
|
+
client = get_client()
|
|
457
|
+
client.force_checkin_vault_account(account_id)
|
|
458
|
+
print_success(f"Force checked in vault account {account_id}")
|
|
459
|
+
except httpx.HTTPStatusError as e:
|
|
460
|
+
print_api_error(e, "force checkin vault account")
|
|
461
|
+
raise typer.Exit(1)
|
|
462
|
+
except httpx.RequestError as e:
|
|
463
|
+
print_api_error(e, "force checkin vault account")
|
|
464
|
+
raise typer.Exit(1)
|
|
465
|
+
except Exception as e:
|
|
466
|
+
print_api_error(e, "force checkin vault account")
|
|
467
|
+
raise typer.Exit(1)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
@accounts_app.command("rotate")
|
|
471
|
+
def rotate_vault_account(
|
|
472
|
+
account_id: int = typer.Argument(..., help="Vault Account ID"),
|
|
473
|
+
):
|
|
474
|
+
"""Schedule credential rotation for a Vault account."""
|
|
475
|
+
from bt_cli.pra.client import get_client
|
|
476
|
+
|
|
477
|
+
try:
|
|
478
|
+
client = get_client()
|
|
479
|
+
client.rotate_vault_account(account_id)
|
|
480
|
+
print_success(f"Scheduled rotation for vault account {account_id}")
|
|
481
|
+
except httpx.HTTPStatusError as e:
|
|
482
|
+
print_api_error(e, "schedule rotation")
|
|
483
|
+
raise typer.Exit(1)
|
|
484
|
+
except httpx.RequestError as e:
|
|
485
|
+
print_api_error(e, "schedule rotation")
|
|
486
|
+
raise typer.Exit(1)
|
|
487
|
+
except Exception as e:
|
|
488
|
+
print_api_error(e, "schedule rotation")
|
|
489
|
+
raise typer.Exit(1)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
# Account Groups subcommands
|
|
493
|
+
groups_app = typer.Typer(no_args_is_help=True, help="Vault account groups")
|
|
494
|
+
app.add_typer(groups_app, name="groups")
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@groups_app.command("list")
|
|
498
|
+
def list_vault_account_groups(
|
|
499
|
+
output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
|
|
500
|
+
):
|
|
501
|
+
"""List Vault account groups."""
|
|
502
|
+
from bt_cli.pra.client import get_client
|
|
503
|
+
|
|
504
|
+
try:
|
|
505
|
+
client = get_client()
|
|
506
|
+
groups = client.list_vault_account_groups()
|
|
507
|
+
|
|
508
|
+
if output == OutputFormat.JSON:
|
|
509
|
+
print_json(groups)
|
|
510
|
+
else:
|
|
511
|
+
columns = [
|
|
512
|
+
("ID", "id"),
|
|
513
|
+
("Name", "name"),
|
|
514
|
+
("Description", "description"),
|
|
515
|
+
]
|
|
516
|
+
print_table(groups, columns, title="Vault Account Groups")
|
|
517
|
+
except httpx.HTTPStatusError as e:
|
|
518
|
+
print_api_error(e, "list vault account groups")
|
|
519
|
+
raise typer.Exit(1)
|
|
520
|
+
except httpx.RequestError as e:
|
|
521
|
+
print_api_error(e, "list vault account groups")
|
|
522
|
+
raise typer.Exit(1)
|
|
523
|
+
except Exception as e:
|
|
524
|
+
print_api_error(e, "list vault account groups")
|
|
525
|
+
raise typer.Exit(1)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
@groups_app.command("get")
|
|
529
|
+
def get_vault_account_group(
|
|
530
|
+
group_id: int = typer.Argument(..., help="Vault Account Group ID"),
|
|
531
|
+
output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
|
|
532
|
+
):
|
|
533
|
+
"""Get Vault account group details."""
|
|
534
|
+
from bt_cli.pra.client import get_client
|
|
535
|
+
from rich.console import Console
|
|
536
|
+
from rich.panel import Panel
|
|
537
|
+
|
|
538
|
+
console = Console()
|
|
539
|
+
|
|
540
|
+
try:
|
|
541
|
+
client = get_client()
|
|
542
|
+
group = client.get_vault_account_group(group_id)
|
|
543
|
+
|
|
544
|
+
if output == OutputFormat.JSON:
|
|
545
|
+
print_json(group)
|
|
546
|
+
else:
|
|
547
|
+
name = group.get("name", "")
|
|
548
|
+
description = group.get("description", "") or "-"
|
|
549
|
+
|
|
550
|
+
console.print(Panel(
|
|
551
|
+
f"[bold]{name}[/bold]\n\n"
|
|
552
|
+
f"[dim]Description:[/dim] {description}",
|
|
553
|
+
title="Vault Account Group Details",
|
|
554
|
+
subtitle=f"ID: {group.get('id', '')}",
|
|
555
|
+
))
|
|
556
|
+
except httpx.HTTPStatusError as e:
|
|
557
|
+
print_api_error(e, "get vault account group")
|
|
558
|
+
raise typer.Exit(1)
|
|
559
|
+
except httpx.RequestError as e:
|
|
560
|
+
print_api_error(e, "get vault account group")
|
|
561
|
+
raise typer.Exit(1)
|
|
562
|
+
except Exception as e:
|
|
563
|
+
print_api_error(e, "get vault account group")
|
|
564
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""PRA Pydantic models."""
|
|
2
|
+
|
|
3
|
+
from .common import PRABaseModel
|
|
4
|
+
from .jumpoint import Jumpoint
|
|
5
|
+
from .jump_group import JumpGroup
|
|
6
|
+
from .jump_client import JumpClient
|
|
7
|
+
from .jump_item import ShellJump, RdpJump, VncJump, WebJump, ProtocolTunnel
|
|
8
|
+
from .vault import VaultAccount, VaultAccountGroup, VaultCredential
|
|
9
|
+
from .user import User
|
|
10
|
+
from .team import Team
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"PRABaseModel",
|
|
14
|
+
"Jumpoint",
|
|
15
|
+
"JumpGroup",
|
|
16
|
+
"JumpClient",
|
|
17
|
+
"ShellJump",
|
|
18
|
+
"RdpJump",
|
|
19
|
+
"VncJump",
|
|
20
|
+
"WebJump",
|
|
21
|
+
"ProtocolTunnel",
|
|
22
|
+
"VaultAccount",
|
|
23
|
+
"VaultAccountGroup",
|
|
24
|
+
"VaultCredential",
|
|
25
|
+
"User",
|
|
26
|
+
"Team",
|
|
27
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Jump Client model."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from .common import PRABaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class JumpClient(PRABaseModel):
|
|
9
|
+
"""Jump Client - agent installed on endpoint."""
|
|
10
|
+
|
|
11
|
+
id: int
|
|
12
|
+
name: str
|
|
13
|
+
hostname: Optional[str] = None
|
|
14
|
+
fqdn: Optional[str] = None
|
|
15
|
+
tag: Optional[str] = None
|
|
16
|
+
comments: Optional[str] = None
|
|
17
|
+
jump_group_id: Optional[int] = None
|
|
18
|
+
jump_group_type: Optional[str] = None
|
|
19
|
+
connection_type: Optional[str] = None
|
|
20
|
+
public_ip: Optional[str] = None
|
|
21
|
+
private_ip: Optional[str] = None
|
|
22
|
+
console_user: Optional[str] = None
|
|
23
|
+
os_name: Optional[str] = None
|
|
24
|
+
os_version: Optional[str] = None
|
|
25
|
+
last_connect_time: Optional[str] = None
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Jump Group model."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from .common import PRABaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class JumpGroup(PRABaseModel):
|
|
9
|
+
"""Jump Group - organizes Jump Items."""
|
|
10
|
+
|
|
11
|
+
id: int
|
|
12
|
+
name: str
|
|
13
|
+
code_name: Optional[str] = None
|
|
14
|
+
comments: Optional[str] = None
|
|
15
|
+
ecm_group_id: Optional[int] = None
|