bt-cli 0.4.13__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 +94 -0
- bt_cli/data/__init__.py +0 -0
- bt_cli/data/skills/bt/SKILL.md +108 -0
- bt_cli/data/skills/entitle/SKILL.md +170 -0
- bt_cli/data/skills/epmw/SKILL.md +144 -0
- bt_cli/data/skills/pra/SKILL.md +150 -0
- bt_cli/data/skills/pws/SKILL.md +198 -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.13.dist-info/METADATA +417 -0
- bt_cli-0.4.13.dist-info/RECORD +121 -0
- bt_cli-0.4.13.dist-info/WHEEL +4 -0
- bt_cli-0.4.13.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,1646 @@
|
|
|
1
|
+
"""Quick commands for Password Safe - combine multiple API calls into single operations."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from ...core.output import print_api_error, print_error, print_success, print_warning
|
|
13
|
+
from ...core.prompts import prompt_if_missing, prompt_from_list, prompt_choice
|
|
14
|
+
from ..client.base import get_client
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(no_args_is_help=True, help="Quick commands - common multi-step operations in one command")
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command("checkout")
|
|
21
|
+
def quick_checkout(
|
|
22
|
+
system: Optional[str] = typer.Option(None, "--system", "-s", help="System name (partial match supported)"),
|
|
23
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Account name"),
|
|
24
|
+
duration: int = typer.Option(60, "--duration", "-d", help="Duration in minutes"),
|
|
25
|
+
reason: Optional[str] = typer.Option(None, "--reason", "-r", help="Reason for checkout"),
|
|
26
|
+
raw: bool = typer.Option(False, "--raw", help="Output only the password (for scripts)"),
|
|
27
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Checkout credentials and show password in one step.
|
|
30
|
+
|
|
31
|
+
Combines: find system -> find account -> checkout -> show password
|
|
32
|
+
|
|
33
|
+
If system or account not provided, prompts interactively.
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
bt pws quick checkout # Interactive mode
|
|
37
|
+
bt pws quick checkout -s "axion-finapp-01" -a "root"
|
|
38
|
+
bt pws quick checkout -s axion -a root --duration 30
|
|
39
|
+
PASSWORD=$(bt pws quick checkout -s server -a admin --raw)
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
with get_client() as client:
|
|
43
|
+
client.authenticate()
|
|
44
|
+
|
|
45
|
+
# Interactive prompt for system if not provided
|
|
46
|
+
if not system:
|
|
47
|
+
system = prompt_if_missing(system, "System name (or partial match)")
|
|
48
|
+
|
|
49
|
+
# Step 1: Find the system
|
|
50
|
+
console.print(f"[dim]Finding system '{system}'...[/dim]") if not raw else None
|
|
51
|
+
systems = client.list_managed_systems(search=system)
|
|
52
|
+
|
|
53
|
+
if not systems:
|
|
54
|
+
print_error(f"No system found matching '{system}'")
|
|
55
|
+
raise typer.Exit(1)
|
|
56
|
+
|
|
57
|
+
# Try exact match first, then partial
|
|
58
|
+
matched_system = None
|
|
59
|
+
for s in systems:
|
|
60
|
+
if s.get("SystemName", "").lower() == system.lower():
|
|
61
|
+
matched_system = s
|
|
62
|
+
break
|
|
63
|
+
|
|
64
|
+
if not matched_system:
|
|
65
|
+
# Use first partial match
|
|
66
|
+
matched_system = systems[0]
|
|
67
|
+
if len(systems) > 1 and not raw:
|
|
68
|
+
console.print(f"[yellow]Multiple matches found, using: {matched_system.get('SystemName')}[/yellow]")
|
|
69
|
+
|
|
70
|
+
system_id = matched_system.get("ManagedSystemID")
|
|
71
|
+
system_name = matched_system.get("SystemName")
|
|
72
|
+
|
|
73
|
+
# Step 2: Find the account
|
|
74
|
+
accounts = client.list_managed_accounts(system_id=system_id)
|
|
75
|
+
|
|
76
|
+
# Interactive prompt for account if not provided
|
|
77
|
+
if not account:
|
|
78
|
+
if not accounts:
|
|
79
|
+
print_error(f"No accounts found on system '{system_name}'")
|
|
80
|
+
raise typer.Exit(1)
|
|
81
|
+
account = prompt_from_list(
|
|
82
|
+
accounts, "Account name", "AccountName", "AccountName",
|
|
83
|
+
f"Accounts on {system_name}", str
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
console.print(f"[dim]Finding account '{account}' on {system_name}...[/dim]") if not raw else None
|
|
87
|
+
|
|
88
|
+
matched_account = None
|
|
89
|
+
for acc in accounts:
|
|
90
|
+
if acc.get("AccountName", "").lower() == account.lower():
|
|
91
|
+
matched_account = acc
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
if not matched_account:
|
|
95
|
+
print_error(f"Account '{account}' not found on system '{system_name}'")
|
|
96
|
+
# Show available accounts
|
|
97
|
+
if accounts and not raw:
|
|
98
|
+
console.print("[dim]Available accounts:[/dim]")
|
|
99
|
+
for acc in accounts[:10]:
|
|
100
|
+
console.print(f" - {acc.get('AccountName')}")
|
|
101
|
+
raise typer.Exit(1)
|
|
102
|
+
|
|
103
|
+
account_id = matched_account.get("AccountId", matched_account.get("ManagedAccountID"))
|
|
104
|
+
account_name = matched_account.get("AccountName")
|
|
105
|
+
|
|
106
|
+
# Step 3: Checkout
|
|
107
|
+
console.print(f"[dim]Checking out {account_name}@{system_name}...[/dim]") if not raw else None
|
|
108
|
+
request = client.create_request(
|
|
109
|
+
account_id=account_id,
|
|
110
|
+
system_id=system_id,
|
|
111
|
+
duration_minutes=duration,
|
|
112
|
+
reason=reason,
|
|
113
|
+
access_type="View",
|
|
114
|
+
)
|
|
115
|
+
request_id = request.get("RequestID")
|
|
116
|
+
|
|
117
|
+
# Step 4: Get the credential
|
|
118
|
+
credential = client.get_credential(request_id)
|
|
119
|
+
password = credential.get("Password", "")
|
|
120
|
+
|
|
121
|
+
# Output
|
|
122
|
+
if raw:
|
|
123
|
+
print(password, end="")
|
|
124
|
+
elif output == "json":
|
|
125
|
+
result = {
|
|
126
|
+
"request_id": request_id,
|
|
127
|
+
"system": system_name,
|
|
128
|
+
"system_id": system_id,
|
|
129
|
+
"account": account_name,
|
|
130
|
+
"account_id": account_id,
|
|
131
|
+
"password": password,
|
|
132
|
+
"duration_minutes": duration,
|
|
133
|
+
}
|
|
134
|
+
console.print_json(json.dumps(result))
|
|
135
|
+
else:
|
|
136
|
+
console.print(Panel(
|
|
137
|
+
f"[green]Credential checked out successfully![/green]\n\n"
|
|
138
|
+
f"System: [cyan]{system_name}[/cyan] (ID: {system_id})\n"
|
|
139
|
+
f"Account: [cyan]{account_name}[/cyan] (ID: {account_id})\n"
|
|
140
|
+
f"Request ID: [bold yellow]{request_id}[/bold yellow]\n"
|
|
141
|
+
f"Duration: {duration} minutes\n\n"
|
|
142
|
+
f"Password: [bold green]{password}[/bold green]\n\n"
|
|
143
|
+
f"[dim]Checkin: bt pws credentials checkin {request_id}[/dim]",
|
|
144
|
+
title="Quick Checkout",
|
|
145
|
+
))
|
|
146
|
+
|
|
147
|
+
except httpx.HTTPStatusError as e:
|
|
148
|
+
print_api_error(e, "quick checkout")
|
|
149
|
+
raise typer.Exit(1)
|
|
150
|
+
except httpx.RequestError as e:
|
|
151
|
+
print_api_error(e, "quick checkout")
|
|
152
|
+
raise typer.Exit(1)
|
|
153
|
+
except typer.Exit:
|
|
154
|
+
raise
|
|
155
|
+
except Exception as e:
|
|
156
|
+
print_api_error(e, "quick checkout")
|
|
157
|
+
raise typer.Exit(1)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@app.command("checkin")
|
|
161
|
+
def quick_checkin(
|
|
162
|
+
request_id: int = typer.Argument(..., help="Request ID to check in"),
|
|
163
|
+
rotate: bool = typer.Option(False, "--rotate", "-r", help="Rotate password after checkin"),
|
|
164
|
+
) -> None:
|
|
165
|
+
"""Check in a credential with optional password rotation.
|
|
166
|
+
|
|
167
|
+
Examples:
|
|
168
|
+
bt pws quick checkin 17
|
|
169
|
+
bt pws quick checkin 17 --rotate
|
|
170
|
+
"""
|
|
171
|
+
try:
|
|
172
|
+
with get_client() as client:
|
|
173
|
+
client.authenticate()
|
|
174
|
+
|
|
175
|
+
if rotate:
|
|
176
|
+
console.print("[dim]Scheduling password rotation...[/dim]")
|
|
177
|
+
client.rotate_on_checkin(request_id)
|
|
178
|
+
|
|
179
|
+
console.print("[dim]Checking in credential...[/dim]")
|
|
180
|
+
client.checkin_request(request_id)
|
|
181
|
+
|
|
182
|
+
print_success(f"Credential {request_id} checked in successfully!")
|
|
183
|
+
if rotate:
|
|
184
|
+
print_warning("Password rotation scheduled - new password will be generated.")
|
|
185
|
+
|
|
186
|
+
except httpx.HTTPStatusError as e:
|
|
187
|
+
print_api_error(e, "quick checkin")
|
|
188
|
+
raise typer.Exit(1)
|
|
189
|
+
except httpx.RequestError as e:
|
|
190
|
+
print_api_error(e, "quick checkin")
|
|
191
|
+
raise typer.Exit(1)
|
|
192
|
+
except Exception as e:
|
|
193
|
+
print_api_error(e, "quick checkin")
|
|
194
|
+
raise typer.Exit(1)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@app.command("search")
|
|
198
|
+
def quick_search(
|
|
199
|
+
query: str = typer.Argument(..., help="Search term (searches systems and accounts)"),
|
|
200
|
+
limit: int = typer.Option(20, "--limit", "-l", help="Maximum results per category"),
|
|
201
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Search across systems and accounts in one command.
|
|
204
|
+
|
|
205
|
+
Examples:
|
|
206
|
+
bt pws quick search axion
|
|
207
|
+
bt pws quick search root
|
|
208
|
+
bt pws quick search database -o json
|
|
209
|
+
"""
|
|
210
|
+
try:
|
|
211
|
+
with get_client() as client:
|
|
212
|
+
client.authenticate()
|
|
213
|
+
|
|
214
|
+
# Search systems (filter client-side since API doesn't support search well)
|
|
215
|
+
console.print(f"[dim]Searching systems for '{query}'...[/dim]")
|
|
216
|
+
all_systems = client.list_managed_systems()
|
|
217
|
+
query_lower = query.lower()
|
|
218
|
+
systems = [
|
|
219
|
+
s for s in all_systems
|
|
220
|
+
if query_lower in s.get("SystemName", "").lower()
|
|
221
|
+
][:limit]
|
|
222
|
+
|
|
223
|
+
# Search accounts
|
|
224
|
+
console.print(f"[dim]Searching accounts for '{query}'...[/dim]")
|
|
225
|
+
accounts = client.list_managed_accounts(account_name=query, limit=limit)
|
|
226
|
+
|
|
227
|
+
if output == "json":
|
|
228
|
+
result = {
|
|
229
|
+
"query": query,
|
|
230
|
+
"systems": systems,
|
|
231
|
+
"accounts": accounts,
|
|
232
|
+
}
|
|
233
|
+
console.print_json(json.dumps(result, default=str))
|
|
234
|
+
else:
|
|
235
|
+
# Show systems
|
|
236
|
+
if systems:
|
|
237
|
+
table = Table(title=f"Systems matching '{query}'")
|
|
238
|
+
table.add_column("ID", style="cyan")
|
|
239
|
+
table.add_column("Name", style="green")
|
|
240
|
+
table.add_column("Platform", style="yellow")
|
|
241
|
+
table.add_column("Workgroup", style="magenta")
|
|
242
|
+
|
|
243
|
+
for s in systems[:limit]:
|
|
244
|
+
table.add_row(
|
|
245
|
+
str(s.get("ManagedSystemID", "")),
|
|
246
|
+
s.get("SystemName", ""),
|
|
247
|
+
str(s.get("PlatformID", "")),
|
|
248
|
+
str(s.get("WorkgroupID", "")),
|
|
249
|
+
)
|
|
250
|
+
console.print(table)
|
|
251
|
+
else:
|
|
252
|
+
console.print(f"[yellow]No systems found matching '{query}'[/yellow]")
|
|
253
|
+
|
|
254
|
+
console.print()
|
|
255
|
+
|
|
256
|
+
# Show accounts
|
|
257
|
+
if accounts:
|
|
258
|
+
table = Table(title=f"Accounts matching '{query}'")
|
|
259
|
+
table.add_column("Acct ID", style="cyan")
|
|
260
|
+
table.add_column("Account", style="green")
|
|
261
|
+
table.add_column("Sys ID", style="blue")
|
|
262
|
+
table.add_column("System", style="magenta")
|
|
263
|
+
|
|
264
|
+
for a in accounts[:limit]:
|
|
265
|
+
table.add_row(
|
|
266
|
+
str(a.get("AccountId", a.get("ManagedAccountID", ""))),
|
|
267
|
+
a.get("AccountName", ""),
|
|
268
|
+
str(a.get("SystemId", a.get("ManagedSystemID", ""))),
|
|
269
|
+
a.get("SystemName", ""),
|
|
270
|
+
)
|
|
271
|
+
console.print(table)
|
|
272
|
+
else:
|
|
273
|
+
console.print(f"[yellow]No accounts found matching '{query}'[/yellow]")
|
|
274
|
+
|
|
275
|
+
except httpx.HTTPStatusError as e:
|
|
276
|
+
print_api_error(e, "quick search")
|
|
277
|
+
raise typer.Exit(1)
|
|
278
|
+
except httpx.RequestError as e:
|
|
279
|
+
print_api_error(e, "quick search")
|
|
280
|
+
raise typer.Exit(1)
|
|
281
|
+
except Exception as e:
|
|
282
|
+
print_api_error(e, "quick search")
|
|
283
|
+
raise typer.Exit(1)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@app.command("password")
|
|
287
|
+
def quick_password(
|
|
288
|
+
system: Optional[str] = typer.Option(None, "--system", "-s", help="System name"),
|
|
289
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Account name"),
|
|
290
|
+
duration: int = typer.Option(5, "--duration", "-d", help="Duration in minutes (default: 5)"),
|
|
291
|
+
auto_checkin: bool = typer.Option(True, "--auto-checkin/--no-auto-checkin", help="Auto checkin after showing password"),
|
|
292
|
+
) -> None:
|
|
293
|
+
"""Get a password quickly - checkout, show, and optionally auto-checkin.
|
|
294
|
+
|
|
295
|
+
Ideal for quick lookups where you just need to see/copy the password.
|
|
296
|
+
If system or account not provided, prompts interactively.
|
|
297
|
+
|
|
298
|
+
Examples:
|
|
299
|
+
bt pws quick password # Interactive mode
|
|
300
|
+
bt pws quick password -s server -a root
|
|
301
|
+
bt pws quick password -s db-server -a admin --no-auto-checkin
|
|
302
|
+
"""
|
|
303
|
+
try:
|
|
304
|
+
with get_client() as client:
|
|
305
|
+
client.authenticate()
|
|
306
|
+
|
|
307
|
+
# Interactive prompt for system if not provided
|
|
308
|
+
if not system:
|
|
309
|
+
system = prompt_if_missing(system, "System name (or partial match)")
|
|
310
|
+
|
|
311
|
+
# Find system
|
|
312
|
+
systems = client.list_managed_systems(search=system)
|
|
313
|
+
if not systems:
|
|
314
|
+
print_error(f"No system found matching '{system}'")
|
|
315
|
+
raise typer.Exit(1)
|
|
316
|
+
|
|
317
|
+
matched_system = None
|
|
318
|
+
for s in systems:
|
|
319
|
+
if s.get("SystemName", "").lower() == system.lower():
|
|
320
|
+
matched_system = s
|
|
321
|
+
break
|
|
322
|
+
if not matched_system:
|
|
323
|
+
matched_system = systems[0]
|
|
324
|
+
|
|
325
|
+
system_id = matched_system.get("ManagedSystemID")
|
|
326
|
+
system_name = matched_system.get("SystemName")
|
|
327
|
+
|
|
328
|
+
# Find account
|
|
329
|
+
accounts = client.list_managed_accounts(system_id=system_id)
|
|
330
|
+
|
|
331
|
+
# Interactive prompt for account if not provided
|
|
332
|
+
if not account:
|
|
333
|
+
if not accounts:
|
|
334
|
+
print_error(f"No accounts found on system '{system_name}'")
|
|
335
|
+
raise typer.Exit(1)
|
|
336
|
+
account = prompt_from_list(
|
|
337
|
+
accounts, "Account name", "AccountName", "AccountName",
|
|
338
|
+
f"Accounts on {system_name}", str
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
matched_account = None
|
|
342
|
+
for acc in accounts:
|
|
343
|
+
if acc.get("AccountName", "").lower() == account.lower():
|
|
344
|
+
matched_account = acc
|
|
345
|
+
break
|
|
346
|
+
|
|
347
|
+
if not matched_account:
|
|
348
|
+
print_error(f"Account '{account}' not found on '{system_name}'")
|
|
349
|
+
raise typer.Exit(1)
|
|
350
|
+
|
|
351
|
+
account_id = matched_account.get("AccountId", matched_account.get("ManagedAccountID"))
|
|
352
|
+
account_name = matched_account.get("AccountName")
|
|
353
|
+
|
|
354
|
+
# Checkout
|
|
355
|
+
console.print(f"[dim]Checking out {account_name}@{system_name}...[/dim]")
|
|
356
|
+
request = client.create_request(
|
|
357
|
+
account_id=account_id,
|
|
358
|
+
system_id=system_id,
|
|
359
|
+
duration_minutes=duration,
|
|
360
|
+
access_type="View",
|
|
361
|
+
)
|
|
362
|
+
request_id = request.get("RequestID")
|
|
363
|
+
|
|
364
|
+
# Get password
|
|
365
|
+
credential = client.get_credential(request_id)
|
|
366
|
+
password = credential.get("Password", "")
|
|
367
|
+
|
|
368
|
+
# Show password
|
|
369
|
+
console.print(f"\n[bold green]{password}[/bold green]\n")
|
|
370
|
+
console.print(f"[dim]{account_name}@{system_name} (Request: {request_id})[/dim]")
|
|
371
|
+
|
|
372
|
+
# Auto checkin
|
|
373
|
+
if auto_checkin:
|
|
374
|
+
console.print("[dim]Checking in...[/dim]")
|
|
375
|
+
client.checkin_request(request_id)
|
|
376
|
+
console.print("[green]Auto checked in.[/green]")
|
|
377
|
+
else:
|
|
378
|
+
console.print(f"\n[yellow]Remember to checkin: bt pws credentials checkin {request_id}[/yellow]")
|
|
379
|
+
|
|
380
|
+
except httpx.HTTPStatusError as e:
|
|
381
|
+
print_api_error(e, "quick password")
|
|
382
|
+
raise typer.Exit(1)
|
|
383
|
+
except httpx.RequestError as e:
|
|
384
|
+
print_api_error(e, "quick password")
|
|
385
|
+
raise typer.Exit(1)
|
|
386
|
+
except typer.Exit:
|
|
387
|
+
raise
|
|
388
|
+
except Exception as e:
|
|
389
|
+
print_api_error(e, "quick password")
|
|
390
|
+
raise typer.Exit(1)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@app.command("rotate")
|
|
394
|
+
def quick_rotate(
|
|
395
|
+
system: str = typer.Option(..., "--system", "-s", help="System name (partial match supported)"),
|
|
396
|
+
account: str = typer.Option(..., "--account", "-a", help="Account name"),
|
|
397
|
+
) -> None:
|
|
398
|
+
"""Find an account and trigger password rotation.
|
|
399
|
+
|
|
400
|
+
Combines: find system -> find account -> trigger rotation
|
|
401
|
+
|
|
402
|
+
Examples:
|
|
403
|
+
bt pws quick rotate -s "axion-finapp-01" -a "root"
|
|
404
|
+
bt pws quick rotate -s axion -a svc-backup
|
|
405
|
+
"""
|
|
406
|
+
try:
|
|
407
|
+
with get_client() as client:
|
|
408
|
+
client.authenticate()
|
|
409
|
+
|
|
410
|
+
# Step 1: Find the system
|
|
411
|
+
console.print(f"[dim]Finding system '{system}'...[/dim]")
|
|
412
|
+
systems = client.list_managed_systems(search=system)
|
|
413
|
+
|
|
414
|
+
if not systems:
|
|
415
|
+
print_error(f"No system found matching '{system}'")
|
|
416
|
+
raise typer.Exit(1)
|
|
417
|
+
|
|
418
|
+
# Try exact match first, then partial
|
|
419
|
+
matched_system = None
|
|
420
|
+
for s in systems:
|
|
421
|
+
if s.get("SystemName", "").lower() == system.lower():
|
|
422
|
+
matched_system = s
|
|
423
|
+
break
|
|
424
|
+
|
|
425
|
+
if not matched_system:
|
|
426
|
+
matched_system = systems[0]
|
|
427
|
+
if len(systems) > 1:
|
|
428
|
+
console.print(f"[yellow]Multiple matches found, using: {matched_system.get('SystemName')}[/yellow]")
|
|
429
|
+
|
|
430
|
+
system_id = matched_system.get("ManagedSystemID")
|
|
431
|
+
system_name = matched_system.get("SystemName")
|
|
432
|
+
|
|
433
|
+
# Step 2: Find the account
|
|
434
|
+
console.print(f"[dim]Finding account '{account}' on {system_name}...[/dim]")
|
|
435
|
+
accounts = client.list_managed_accounts(system_id=system_id)
|
|
436
|
+
|
|
437
|
+
matched_account = None
|
|
438
|
+
for acc in accounts:
|
|
439
|
+
if acc.get("AccountName", "").lower() == account.lower():
|
|
440
|
+
matched_account = acc
|
|
441
|
+
break
|
|
442
|
+
|
|
443
|
+
if not matched_account:
|
|
444
|
+
print_error(f"Account '{account}' not found on system '{system_name}'")
|
|
445
|
+
if accounts:
|
|
446
|
+
console.print("[dim]Available accounts:[/dim]")
|
|
447
|
+
for acc in accounts[:10]:
|
|
448
|
+
console.print(f" - {acc.get('AccountName')}")
|
|
449
|
+
raise typer.Exit(1)
|
|
450
|
+
|
|
451
|
+
account_id = matched_account.get("AccountId", matched_account.get("ManagedAccountID"))
|
|
452
|
+
account_name = matched_account.get("AccountName")
|
|
453
|
+
|
|
454
|
+
# Step 3: Trigger rotation
|
|
455
|
+
console.print(f"[dim]Triggering password rotation for {account_name}@{system_name}...[/dim]")
|
|
456
|
+
client.change_managed_account_password(account_id)
|
|
457
|
+
|
|
458
|
+
print_success(f"Password rotation initiated for {account_name}@{system_name}")
|
|
459
|
+
console.print("[dim]Note: The new password will be generated using the configured password rule.[/dim]")
|
|
460
|
+
|
|
461
|
+
except httpx.HTTPStatusError as e:
|
|
462
|
+
print_api_error(e, "quick rotate")
|
|
463
|
+
raise typer.Exit(1)
|
|
464
|
+
except httpx.RequestError as e:
|
|
465
|
+
print_api_error(e, "quick rotate")
|
|
466
|
+
raise typer.Exit(1)
|
|
467
|
+
except typer.Exit:
|
|
468
|
+
raise
|
|
469
|
+
except Exception as e:
|
|
470
|
+
print_api_error(e, "quick rotate")
|
|
471
|
+
raise typer.Exit(1)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@app.command("find-secret")
|
|
475
|
+
def quick_find_secret(
|
|
476
|
+
query: str = typer.Argument(..., help="Search term (searches folders and secrets)"),
|
|
477
|
+
limit: int = typer.Option(20, "--limit", "-l", help="Maximum results per category"),
|
|
478
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
479
|
+
) -> None:
|
|
480
|
+
"""Search Secrets Safe folders and secrets.
|
|
481
|
+
|
|
482
|
+
Examples:
|
|
483
|
+
bt pws quick find-secret database
|
|
484
|
+
bt pws quick find-secret admin
|
|
485
|
+
bt pws quick find-secret api-key -o json
|
|
486
|
+
"""
|
|
487
|
+
try:
|
|
488
|
+
with get_client() as client:
|
|
489
|
+
client.authenticate()
|
|
490
|
+
query_lower = query.lower()
|
|
491
|
+
|
|
492
|
+
# Search folders
|
|
493
|
+
console.print(f"[dim]Searching folders for '{query}'...[/dim]")
|
|
494
|
+
all_folders = client.list_folders()
|
|
495
|
+
folders = [
|
|
496
|
+
f for f in all_folders
|
|
497
|
+
if query_lower in f.get("Name", "").lower()
|
|
498
|
+
or query_lower in (f.get("Description") or "").lower()
|
|
499
|
+
][:limit]
|
|
500
|
+
|
|
501
|
+
# Search secrets
|
|
502
|
+
console.print(f"[dim]Searching secrets for '{query}'...[/dim]")
|
|
503
|
+
all_secrets = client.list_secrets()
|
|
504
|
+
secrets = [
|
|
505
|
+
s for s in all_secrets
|
|
506
|
+
if query_lower in s.get("Title", "").lower()
|
|
507
|
+
or query_lower in (s.get("Username") or "").lower()
|
|
508
|
+
or query_lower in (s.get("Description") or "").lower()
|
|
509
|
+
][:limit]
|
|
510
|
+
|
|
511
|
+
if output == "json":
|
|
512
|
+
result = {
|
|
513
|
+
"query": query,
|
|
514
|
+
"folders": folders,
|
|
515
|
+
"secrets": secrets,
|
|
516
|
+
}
|
|
517
|
+
console.print_json(json.dumps(result, default=str))
|
|
518
|
+
else:
|
|
519
|
+
# Show folders
|
|
520
|
+
if folders:
|
|
521
|
+
table = Table(title=f"Folders matching '{query}'")
|
|
522
|
+
table.add_column("ID", style="cyan")
|
|
523
|
+
table.add_column("Name", style="green")
|
|
524
|
+
table.add_column("Path", style="yellow")
|
|
525
|
+
table.add_column("Description", style="dim")
|
|
526
|
+
|
|
527
|
+
for f in folders:
|
|
528
|
+
table.add_row(
|
|
529
|
+
str(f.get("Id", "")),
|
|
530
|
+
f.get("Name", ""),
|
|
531
|
+
f.get("FolderPath", "") or "-",
|
|
532
|
+
(f.get("Description") or "-")[:40],
|
|
533
|
+
)
|
|
534
|
+
console.print(table)
|
|
535
|
+
else:
|
|
536
|
+
console.print(f"[yellow]No folders found matching '{query}'[/yellow]")
|
|
537
|
+
|
|
538
|
+
console.print()
|
|
539
|
+
|
|
540
|
+
# Show secrets
|
|
541
|
+
if secrets:
|
|
542
|
+
table = Table(title=f"Secrets matching '{query}'")
|
|
543
|
+
table.add_column("ID", style="cyan")
|
|
544
|
+
table.add_column("Title", style="green")
|
|
545
|
+
table.add_column("Username", style="yellow")
|
|
546
|
+
table.add_column("Folder", style="magenta")
|
|
547
|
+
|
|
548
|
+
for s in secrets:
|
|
549
|
+
table.add_row(
|
|
550
|
+
str(s.get("Id", "")),
|
|
551
|
+
s.get("Title", ""),
|
|
552
|
+
s.get("Username", "") or "-",
|
|
553
|
+
s.get("FolderName", "") or "-",
|
|
554
|
+
)
|
|
555
|
+
console.print(table)
|
|
556
|
+
console.print(f"\n[dim]To get secret value: bt pws secrets secrets get <id>[/dim]")
|
|
557
|
+
else:
|
|
558
|
+
console.print(f"[yellow]No secrets found matching '{query}'[/yellow]")
|
|
559
|
+
|
|
560
|
+
except httpx.HTTPStatusError as e:
|
|
561
|
+
print_api_error(e, "quick find-secret")
|
|
562
|
+
raise typer.Exit(1)
|
|
563
|
+
except httpx.RequestError as e:
|
|
564
|
+
print_api_error(e, "quick find-secret")
|
|
565
|
+
raise typer.Exit(1)
|
|
566
|
+
except Exception as e:
|
|
567
|
+
print_api_error(e, "quick find-secret")
|
|
568
|
+
raise typer.Exit(1)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
@app.command("offboard")
|
|
572
|
+
def quick_offboard(
|
|
573
|
+
system: str = typer.Option(..., "--system", "-s", help="System name (partial match supported)"),
|
|
574
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
|
575
|
+
keep_asset: bool = typer.Option(False, "--keep-asset", help="Don't delete the asset"),
|
|
576
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
577
|
+
) -> None:
|
|
578
|
+
"""Offboard a system: delete all accounts, system, and asset.
|
|
579
|
+
|
|
580
|
+
Performs cascading delete in order:
|
|
581
|
+
1. Delete all managed accounts on the system
|
|
582
|
+
2. Delete the managed system
|
|
583
|
+
3. Delete the asset (unless --keep-asset)
|
|
584
|
+
|
|
585
|
+
Examples:
|
|
586
|
+
bt pws quick offboard -s "my-server"
|
|
587
|
+
bt pws quick offboard -s "web-01" --force
|
|
588
|
+
bt pws quick offboard -s "db-server" --keep-asset
|
|
589
|
+
"""
|
|
590
|
+
try:
|
|
591
|
+
with get_client() as client:
|
|
592
|
+
client.authenticate()
|
|
593
|
+
|
|
594
|
+
# Step 1: Find the system
|
|
595
|
+
console.print(f"[dim]Finding system '{system}'...[/dim]")
|
|
596
|
+
systems = client.list_managed_systems(search=system)
|
|
597
|
+
|
|
598
|
+
if not systems:
|
|
599
|
+
print_error(f"No system found matching '{system}'")
|
|
600
|
+
raise typer.Exit(1)
|
|
601
|
+
|
|
602
|
+
# Try exact match first, then partial
|
|
603
|
+
matched_system = None
|
|
604
|
+
for s in systems:
|
|
605
|
+
if s.get("SystemName", "").lower() == system.lower():
|
|
606
|
+
matched_system = s
|
|
607
|
+
break
|
|
608
|
+
|
|
609
|
+
if not matched_system:
|
|
610
|
+
matched_system = systems[0]
|
|
611
|
+
if len(systems) > 1:
|
|
612
|
+
console.print(f"[yellow]Multiple matches found, using: {matched_system.get('SystemName')}[/yellow]")
|
|
613
|
+
|
|
614
|
+
system_id = matched_system.get("ManagedSystemID")
|
|
615
|
+
system_name = matched_system.get("SystemName")
|
|
616
|
+
asset_id = matched_system.get("AssetID")
|
|
617
|
+
|
|
618
|
+
# Get full system details if asset_id not in list response
|
|
619
|
+
if not asset_id:
|
|
620
|
+
full_system = client.get_managed_system(system_id)
|
|
621
|
+
asset_id = full_system.get("AssetID")
|
|
622
|
+
|
|
623
|
+
# Step 2: List accounts on this system
|
|
624
|
+
console.print(f"[dim]Finding accounts on {system_name}...[/dim]")
|
|
625
|
+
accounts = client.list_managed_accounts(system_id=system_id)
|
|
626
|
+
|
|
627
|
+
# Show what will be deleted
|
|
628
|
+
console.print(f"\n[bold]Will delete:[/bold]")
|
|
629
|
+
console.print(f" System: [cyan]{system_name}[/cyan] (ID: {system_id})")
|
|
630
|
+
if accounts:
|
|
631
|
+
console.print(f" Accounts ({len(accounts)}):")
|
|
632
|
+
for acc in accounts:
|
|
633
|
+
acc_id = acc.get("AccountId", acc.get("ManagedAccountID", ""))
|
|
634
|
+
console.print(f" - {acc.get('AccountName')} (ID: {acc_id})")
|
|
635
|
+
else:
|
|
636
|
+
console.print(" Accounts: None")
|
|
637
|
+
if asset_id and not keep_asset:
|
|
638
|
+
console.print(f" Asset ID: [yellow]{asset_id}[/yellow]")
|
|
639
|
+
elif keep_asset:
|
|
640
|
+
console.print(f" Asset ID: {asset_id} [dim](keeping)[/dim]")
|
|
641
|
+
|
|
642
|
+
# Confirm
|
|
643
|
+
if not force:
|
|
644
|
+
confirm = typer.confirm("\nProceed with deletion?")
|
|
645
|
+
if not confirm:
|
|
646
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
647
|
+
raise typer.Exit(0)
|
|
648
|
+
|
|
649
|
+
deleted = {"accounts": [], "system": None, "asset": None}
|
|
650
|
+
|
|
651
|
+
# Step 3: Delete accounts
|
|
652
|
+
for acc in accounts:
|
|
653
|
+
acc_id = acc.get("AccountId", acc.get("ManagedAccountID"))
|
|
654
|
+
acc_name = acc.get("AccountName")
|
|
655
|
+
console.print(f"[dim]Deleting account {acc_name} (ID: {acc_id})...[/dim]")
|
|
656
|
+
client.delete_managed_account(acc_id)
|
|
657
|
+
deleted["accounts"].append({"id": acc_id, "name": acc_name})
|
|
658
|
+
console.print(f" [green]Deleted account: {acc_name}[/green]")
|
|
659
|
+
|
|
660
|
+
# Step 4: Delete system
|
|
661
|
+
console.print(f"[dim]Deleting system {system_name} (ID: {system_id})...[/dim]")
|
|
662
|
+
client.delete_managed_system(system_id)
|
|
663
|
+
deleted["system"] = {"id": system_id, "name": system_name}
|
|
664
|
+
console.print(f" [green]Deleted system: {system_name}[/green]")
|
|
665
|
+
|
|
666
|
+
# Step 5: Delete asset (unless --keep-asset)
|
|
667
|
+
if asset_id and not keep_asset:
|
|
668
|
+
console.print(f"[dim]Deleting asset (ID: {asset_id})...[/dim]")
|
|
669
|
+
client.delete_asset(asset_id)
|
|
670
|
+
deleted["asset"] = {"id": asset_id}
|
|
671
|
+
console.print(f" [green]Deleted asset ID: {asset_id}[/green]")
|
|
672
|
+
|
|
673
|
+
# Output
|
|
674
|
+
if output == "json":
|
|
675
|
+
console.print_json(json.dumps(deleted))
|
|
676
|
+
else:
|
|
677
|
+
console.print(f"\n[bold green]System '{system_name}' offboarded successfully![/bold green]")
|
|
678
|
+
console.print(f" Accounts deleted: {len(deleted['accounts'])}")
|
|
679
|
+
console.print(f" System deleted: {system_name}")
|
|
680
|
+
if deleted["asset"]:
|
|
681
|
+
console.print(f" Asset deleted: {deleted['asset']['id']}")
|
|
682
|
+
|
|
683
|
+
except httpx.HTTPStatusError as e:
|
|
684
|
+
print_api_error(e, "quick offboard")
|
|
685
|
+
raise typer.Exit(1)
|
|
686
|
+
except httpx.RequestError as e:
|
|
687
|
+
print_api_error(e, "quick offboard")
|
|
688
|
+
raise typer.Exit(1)
|
|
689
|
+
except typer.Exit:
|
|
690
|
+
raise
|
|
691
|
+
except Exception as e:
|
|
692
|
+
print_api_error(e, "quick offboard")
|
|
693
|
+
raise typer.Exit(1)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
@app.command("onboard")
|
|
697
|
+
def quick_onboard(
|
|
698
|
+
name: Optional[str] = typer.Option(None, "--name", "-n", help="System name"),
|
|
699
|
+
ip: Optional[str] = typer.Option(None, "--ip", "-i", help="IP address"),
|
|
700
|
+
dns: Optional[str] = typer.Option(None, "--dns", "-d", help="DNS name (e.g., ip-10-0-1-50.compute.internal)"),
|
|
701
|
+
workgroup: Optional[int] = typer.Option(None, "--workgroup", "-w", help="Workgroup ID"),
|
|
702
|
+
platform: Optional[int] = typer.Option(None, "--platform", "-p", help="Platform ID (1=Windows, 2=Linux)"),
|
|
703
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Account name to create"),
|
|
704
|
+
password: Optional[str] = typer.Option(None, "--password", help="Account password"),
|
|
705
|
+
functional_account: Optional[int] = typer.Option(None, "--functional-account", "-f", help="Functional account ID for auto-management"),
|
|
706
|
+
auto_manage: bool = typer.Option(True, "--auto-manage/--no-auto-manage", help="Enable auto password management"),
|
|
707
|
+
port: Optional[int] = typer.Option(None, "--port", help="Connection port"),
|
|
708
|
+
elevation: Optional[str] = typer.Option(None, "--elevation", "-e", help="Elevation command (e.g., 'sudo')"),
|
|
709
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
710
|
+
) -> None:
|
|
711
|
+
"""Onboard a system in one step: create asset, managed system, and account.
|
|
712
|
+
|
|
713
|
+
Combines: create asset -> create managed system -> create managed account
|
|
714
|
+
|
|
715
|
+
If required options are not provided, you will be prompted interactively.
|
|
716
|
+
|
|
717
|
+
Examples:
|
|
718
|
+
# Interactive mode - prompts for missing info
|
|
719
|
+
bt pws quick onboard
|
|
720
|
+
|
|
721
|
+
# Basic Linux server
|
|
722
|
+
bt pws quick onboard -n "my-server" -i "10.0.1.50" -w 3
|
|
723
|
+
|
|
724
|
+
# EC2 instance with internal DNS (recommended for AWS)
|
|
725
|
+
bt pws quick onboard -n "web-01" -i "10.0.1.100" -d "ip-10-0-1-100.compute.internal" -w 3
|
|
726
|
+
|
|
727
|
+
# With functional account for auto-management
|
|
728
|
+
bt pws quick onboard -n "web-01" -i "10.0.1.100" -w 3 -f 7 -e "sudo"
|
|
729
|
+
|
|
730
|
+
# Windows server
|
|
731
|
+
bt pws quick onboard -n "win-srv" -i "10.0.1.200" -w 2 -p 1 -a "Administrator" --port 5985
|
|
732
|
+
|
|
733
|
+
# With specific password
|
|
734
|
+
bt pws quick onboard -n "db-01" -i "10.0.1.150" -w 3 -a "postgres" --password "InitialPass123"
|
|
735
|
+
"""
|
|
736
|
+
try:
|
|
737
|
+
with get_client() as client:
|
|
738
|
+
client.authenticate()
|
|
739
|
+
|
|
740
|
+
# Interactive prompting for missing required fields
|
|
741
|
+
name = prompt_if_missing(name, "System name")
|
|
742
|
+
ip = prompt_if_missing(ip, "IP address")
|
|
743
|
+
|
|
744
|
+
if workgroup is None:
|
|
745
|
+
workgroups = client.list_workgroups()
|
|
746
|
+
workgroup = prompt_from_list(
|
|
747
|
+
workgroups, "Workgroup ID", "ID", "Name", "Available Workgroups", int
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
# Prompt for platform if not specified
|
|
751
|
+
if platform is None:
|
|
752
|
+
platform_choice = prompt_choice(
|
|
753
|
+
"Platform",
|
|
754
|
+
[
|
|
755
|
+
("linux", "Linux/Unix (SSH, port 22)"),
|
|
756
|
+
("windows", "Windows (WinRM, port 5985)"),
|
|
757
|
+
],
|
|
758
|
+
default="linux",
|
|
759
|
+
)
|
|
760
|
+
if platform_choice == "windows":
|
|
761
|
+
platform = 1
|
|
762
|
+
default_account = "Administrator"
|
|
763
|
+
default_port = 5985
|
|
764
|
+
else:
|
|
765
|
+
platform = 2
|
|
766
|
+
default_account = "root"
|
|
767
|
+
default_port = 22
|
|
768
|
+
else:
|
|
769
|
+
# Platform specified via CLI
|
|
770
|
+
if platform == 1:
|
|
771
|
+
default_account = "Administrator"
|
|
772
|
+
default_port = 5985
|
|
773
|
+
else:
|
|
774
|
+
default_account = "root"
|
|
775
|
+
default_port = 22
|
|
776
|
+
|
|
777
|
+
# Prompt for account name if not specified
|
|
778
|
+
if account is None:
|
|
779
|
+
account = typer.prompt("Account name", default=default_account)
|
|
780
|
+
|
|
781
|
+
# Set port default based on platform
|
|
782
|
+
if port is None:
|
|
783
|
+
port = default_port
|
|
784
|
+
|
|
785
|
+
# Optionally prompt for functional account
|
|
786
|
+
if functional_account is None:
|
|
787
|
+
setup_auto = typer.confirm("Configure auto-management with functional account?", default=False)
|
|
788
|
+
if setup_auto:
|
|
789
|
+
all_func_accounts = client.list_functional_accounts()
|
|
790
|
+
# Filter by platform (PlatformID must match selected platform)
|
|
791
|
+
func_accounts = [
|
|
792
|
+
fa for fa in all_func_accounts
|
|
793
|
+
if fa.get("PlatformID") == platform
|
|
794
|
+
]
|
|
795
|
+
if func_accounts:
|
|
796
|
+
# Show DisplayName for clarity
|
|
797
|
+
for fa in func_accounts:
|
|
798
|
+
fa["_display"] = f"{fa.get('DisplayName', '')} ({fa.get('AccountName', '')})"
|
|
799
|
+
functional_account = prompt_from_list(
|
|
800
|
+
func_accounts,
|
|
801
|
+
"Functional Account ID",
|
|
802
|
+
"FunctionalAccountID",
|
|
803
|
+
"_display",
|
|
804
|
+
f"Functional Accounts for {'Linux' if platform == 2 else 'Windows'}",
|
|
805
|
+
int,
|
|
806
|
+
)
|
|
807
|
+
if platform == 2 and elevation is None:
|
|
808
|
+
elevation = typer.prompt("Elevation command", default="sudo")
|
|
809
|
+
else:
|
|
810
|
+
platform_name = "Linux" if platform == 2 else "Windows"
|
|
811
|
+
console.print(f"[yellow]No functional accounts found for {platform_name}. Skipping auto-management.[/yellow]")
|
|
812
|
+
|
|
813
|
+
# Step 1: Create asset
|
|
814
|
+
console.print(f"\n[dim]Creating asset '{name}'...[/dim]")
|
|
815
|
+
asset = client.create_asset(
|
|
816
|
+
workgroup_id=workgroup,
|
|
817
|
+
ip_address=ip,
|
|
818
|
+
asset_name=name,
|
|
819
|
+
dns_name=dns,
|
|
820
|
+
)
|
|
821
|
+
asset_id = asset.get("AssetID")
|
|
822
|
+
console.print(f" [green]Created asset ID: {asset_id}[/green]")
|
|
823
|
+
|
|
824
|
+
# Step 2: Create managed system
|
|
825
|
+
console.print(f"[dim]Creating managed system...[/dim]")
|
|
826
|
+
system = client.create_managed_system(
|
|
827
|
+
system_name=name,
|
|
828
|
+
platform_id=platform,
|
|
829
|
+
asset_id=asset_id,
|
|
830
|
+
port=port,
|
|
831
|
+
functional_account_id=functional_account,
|
|
832
|
+
auto_management_flag=auto_manage if functional_account else False,
|
|
833
|
+
elevation_command=elevation,
|
|
834
|
+
)
|
|
835
|
+
system_id = system.get("ManagedSystemID")
|
|
836
|
+
console.print(f" [green]Created managed system ID: {system_id}[/green]")
|
|
837
|
+
|
|
838
|
+
# Step 3: Create managed account
|
|
839
|
+
console.print(f"[dim]Creating managed account '{account}'...[/dim]")
|
|
840
|
+
account_obj = client.create_managed_account(
|
|
841
|
+
system_id=system_id,
|
|
842
|
+
account_name=account,
|
|
843
|
+
password=password,
|
|
844
|
+
auto_management_flag=auto_manage if functional_account else False,
|
|
845
|
+
)
|
|
846
|
+
account_id = account_obj.get("ManagedAccountID")
|
|
847
|
+
console.print(f" [green]Created managed account ID: {account_id}[/green]")
|
|
848
|
+
|
|
849
|
+
# Output
|
|
850
|
+
if output == "json":
|
|
851
|
+
result = {
|
|
852
|
+
"asset_id": asset_id,
|
|
853
|
+
"system_id": system_id,
|
|
854
|
+
"system_name": name,
|
|
855
|
+
"account_id": account_id,
|
|
856
|
+
"account_name": account,
|
|
857
|
+
"workgroup_id": workgroup,
|
|
858
|
+
"platform_id": platform,
|
|
859
|
+
"dns": dns,
|
|
860
|
+
}
|
|
861
|
+
console.print_json(json.dumps(result))
|
|
862
|
+
else:
|
|
863
|
+
dns_line = f"DNS: {dns}\n" if dns else ""
|
|
864
|
+
console.print(Panel(
|
|
865
|
+
f"[green]System onboarded successfully![/green]\n\n"
|
|
866
|
+
f"Asset ID: [cyan]{asset_id}[/cyan]\n"
|
|
867
|
+
f"System ID: [cyan]{system_id}[/cyan]\n"
|
|
868
|
+
f"System Name: [bold]{name}[/bold]\n"
|
|
869
|
+
f"Account ID: [cyan]{account_id}[/cyan]\n"
|
|
870
|
+
f"Account Name: [bold]{account}[/bold]\n"
|
|
871
|
+
f"IP: {ip}\n"
|
|
872
|
+
f"{dns_line}"
|
|
873
|
+
f"Port: {port}\n\n"
|
|
874
|
+
f"[dim]Checkout: bt pws quick checkout -s \"{name}\" -a \"{account}\"[/dim]",
|
|
875
|
+
title="Quick Onboard",
|
|
876
|
+
))
|
|
877
|
+
|
|
878
|
+
except httpx.HTTPStatusError as e:
|
|
879
|
+
print_api_error(e, "quick onboard")
|
|
880
|
+
raise typer.Exit(1)
|
|
881
|
+
except httpx.RequestError as e:
|
|
882
|
+
print_api_error(e, "quick onboard")
|
|
883
|
+
raise typer.Exit(1)
|
|
884
|
+
except Exception as e:
|
|
885
|
+
print_api_error(e, "quick onboard")
|
|
886
|
+
raise typer.Exit(1)
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
@app.command("user-entitlements")
|
|
890
|
+
def quick_user_entitlements(
|
|
891
|
+
search: str = typer.Argument(..., help="User search (name or email, partial match)"),
|
|
892
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
893
|
+
) -> None:
|
|
894
|
+
"""Show user entitlements report: groups, roles, and access policies.
|
|
895
|
+
|
|
896
|
+
Searches for a user and shows:
|
|
897
|
+
- User details
|
|
898
|
+
- Groups the user belongs to
|
|
899
|
+
- Role type for each group
|
|
900
|
+
- API registration info (for API/OAuth users)
|
|
901
|
+
- Access policies where user's groups are assignees
|
|
902
|
+
|
|
903
|
+
Examples:
|
|
904
|
+
bt pws quick user-entitlements dave
|
|
905
|
+
bt pws quick user-entitlements admin@example.com
|
|
906
|
+
bt pws quick user-entitlements nhi-provision -o json
|
|
907
|
+
"""
|
|
908
|
+
try:
|
|
909
|
+
with get_client() as client:
|
|
910
|
+
client.authenticate()
|
|
911
|
+
|
|
912
|
+
# Step 1: Find the user (API search is exact, so do client-side filtering)
|
|
913
|
+
console.print(f"[dim]Searching for user '{search}'...[/dim]")
|
|
914
|
+
all_users = client.list_users()
|
|
915
|
+
search_lower = search.lower()
|
|
916
|
+
users = [
|
|
917
|
+
u for u in all_users
|
|
918
|
+
if search_lower in (u.get("UserName", "") or "").lower()
|
|
919
|
+
or search_lower in (u.get("EmailAddress", "") or "").lower()
|
|
920
|
+
or search_lower in (u.get("FirstName", "") or "").lower()
|
|
921
|
+
or search_lower in (u.get("LastName", "") or "").lower()
|
|
922
|
+
]
|
|
923
|
+
|
|
924
|
+
if not users:
|
|
925
|
+
print_error(f"No user found matching '{search}'")
|
|
926
|
+
raise typer.Exit(1)
|
|
927
|
+
|
|
928
|
+
# If multiple users found, show selection
|
|
929
|
+
if len(users) > 1:
|
|
930
|
+
console.print(f"[yellow]Found {len(users)} users matching '{search}':[/yellow]")
|
|
931
|
+
for i, u in enumerate(users[:10]):
|
|
932
|
+
name = f"{u.get('FirstName', '')} {u.get('LastName', '')}".strip() or u.get('UserName')
|
|
933
|
+
console.print(f" {i+1}. {name} ({u.get('UserName')})")
|
|
934
|
+
if len(users) > 10:
|
|
935
|
+
console.print(f" ... and {len(users) - 10} more")
|
|
936
|
+
console.print()
|
|
937
|
+
|
|
938
|
+
user = users[0]
|
|
939
|
+
user_id = user.get("UserID")
|
|
940
|
+
user_name = user.get("UserName")
|
|
941
|
+
|
|
942
|
+
# Step 2: Get all groups and check membership
|
|
943
|
+
console.print(f"[dim]Finding groups for user '{user_name}'...[/dim]")
|
|
944
|
+
all_groups = client.list_user_groups()
|
|
945
|
+
user_groups = []
|
|
946
|
+
|
|
947
|
+
# Role type mapping
|
|
948
|
+
role_types = {0: "Standard", 1: "Administrator", 2: "Auditor"}
|
|
949
|
+
|
|
950
|
+
for group in all_groups:
|
|
951
|
+
group_id = group.get("GroupID")
|
|
952
|
+
try:
|
|
953
|
+
members = client.get_user_group_members(group_id)
|
|
954
|
+
for member in members:
|
|
955
|
+
if member.get("UserID") == user_id:
|
|
956
|
+
# User is in this group - add extra info from membership
|
|
957
|
+
group["_membership"] = member
|
|
958
|
+
user_groups.append(group)
|
|
959
|
+
break
|
|
960
|
+
except Exception:
|
|
961
|
+
# Skip groups we can't access
|
|
962
|
+
pass
|
|
963
|
+
|
|
964
|
+
# Step 3: Get access policies and find which ones apply to user's groups
|
|
965
|
+
console.print(f"[dim]Finding access policies...[/dim]")
|
|
966
|
+
policies = client.list_access_policies()
|
|
967
|
+
user_group_ids = {g.get("GroupID") for g in user_groups}
|
|
968
|
+
user_policies = []
|
|
969
|
+
|
|
970
|
+
for policy in policies:
|
|
971
|
+
policy_id = policy.get("AccessPolicyID")
|
|
972
|
+
try:
|
|
973
|
+
assignees = client.get_access_policy_assignees(policy_id)
|
|
974
|
+
for assignee in assignees:
|
|
975
|
+
if assignee.get("UserGroupID") in user_group_ids:
|
|
976
|
+
user_policies.append({
|
|
977
|
+
"policy": policy,
|
|
978
|
+
"assignee": assignee,
|
|
979
|
+
})
|
|
980
|
+
except Exception:
|
|
981
|
+
pass
|
|
982
|
+
|
|
983
|
+
# Build result
|
|
984
|
+
result = {
|
|
985
|
+
"user": user,
|
|
986
|
+
"groups": user_groups,
|
|
987
|
+
"access_policies": user_policies,
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if output == "json":
|
|
991
|
+
console.print_json(json.dumps(result, default=str))
|
|
992
|
+
else:
|
|
993
|
+
# User info
|
|
994
|
+
first = user.get('FirstName') or ''
|
|
995
|
+
last = user.get('LastName') or ''
|
|
996
|
+
display_name = f"{first} {last}".strip() or user_name
|
|
997
|
+
console.print(Panel(
|
|
998
|
+
f"[bold]{display_name}[/bold]\n"
|
|
999
|
+
f"Username: {user_name}\n"
|
|
1000
|
+
f"Email: {user.get('EmailAddress') or '-'}\n"
|
|
1001
|
+
f"Active: {'Yes' if user.get('IsActive') else 'No'}\n"
|
|
1002
|
+
f"Last Login: {user.get('LastLoginDate') or 'Never'}\n"
|
|
1003
|
+
f"Auth Type: {user.get('LastLoginAuthenticationType') or '-'}",
|
|
1004
|
+
title=f"User {user_id}",
|
|
1005
|
+
))
|
|
1006
|
+
|
|
1007
|
+
# Groups table
|
|
1008
|
+
if user_groups:
|
|
1009
|
+
groups_table = Table(title="User Groups")
|
|
1010
|
+
groups_table.add_column("ID", style="cyan")
|
|
1011
|
+
groups_table.add_column("Name", style="green")
|
|
1012
|
+
groups_table.add_column("Type", style="yellow")
|
|
1013
|
+
groups_table.add_column("Role Type", style="magenta")
|
|
1014
|
+
groups_table.add_column("API Reg IDs", style="blue")
|
|
1015
|
+
groups_table.add_column("Client ID", style="dim")
|
|
1016
|
+
|
|
1017
|
+
for g in user_groups:
|
|
1018
|
+
membership = g.get("_membership", {})
|
|
1019
|
+
groups_table.add_row(
|
|
1020
|
+
str(g.get("GroupID", "")),
|
|
1021
|
+
g.get("Name", ""),
|
|
1022
|
+
g.get("GroupType", "-"),
|
|
1023
|
+
role_types.get(g.get("RoleType"), str(g.get("RoleType", "-"))),
|
|
1024
|
+
g.get("ApplicationRegistrationIDs") or "-",
|
|
1025
|
+
membership.get("ClientID") or "-",
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
console.print(groups_table)
|
|
1029
|
+
else:
|
|
1030
|
+
console.print("[yellow]User is not a member of any groups.[/yellow]")
|
|
1031
|
+
|
|
1032
|
+
# Access policies table
|
|
1033
|
+
if user_policies:
|
|
1034
|
+
console.print()
|
|
1035
|
+
policies_table = Table(title="Access Policies (via group membership)")
|
|
1036
|
+
policies_table.add_column("Policy", style="green")
|
|
1037
|
+
policies_table.add_column("Group", style="cyan")
|
|
1038
|
+
policies_table.add_column("Role", style="yellow")
|
|
1039
|
+
policies_table.add_column("Smart Rule", style="magenta")
|
|
1040
|
+
|
|
1041
|
+
for p in user_policies:
|
|
1042
|
+
policy = p["policy"]
|
|
1043
|
+
assignee = p["assignee"]
|
|
1044
|
+
policies_table.add_row(
|
|
1045
|
+
policy.get("Name", ""),
|
|
1046
|
+
assignee.get("UserGroupName", ""),
|
|
1047
|
+
assignee.get("RoleName", ""),
|
|
1048
|
+
assignee.get("SmartRuleTitle", "-"),
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
console.print(policies_table)
|
|
1052
|
+
else:
|
|
1053
|
+
console.print("\n[yellow]No access policies found for user's groups.[/yellow]")
|
|
1054
|
+
|
|
1055
|
+
except httpx.HTTPStatusError as e:
|
|
1056
|
+
print_api_error(e, "quick user-entitlements")
|
|
1057
|
+
raise typer.Exit(1)
|
|
1058
|
+
except httpx.RequestError as e:
|
|
1059
|
+
print_api_error(e, "quick user-entitlements")
|
|
1060
|
+
raise typer.Exit(1)
|
|
1061
|
+
except typer.Exit:
|
|
1062
|
+
raise
|
|
1063
|
+
except Exception as e:
|
|
1064
|
+
print_api_error(e, "quick user-entitlements")
|
|
1065
|
+
raise typer.Exit(1)
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
@app.command("functional")
|
|
1069
|
+
def quick_functional(
|
|
1070
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
1071
|
+
) -> None:
|
|
1072
|
+
"""Create a functional account interactively.
|
|
1073
|
+
|
|
1074
|
+
Guides you through creating a functional account with smart prompts
|
|
1075
|
+
based on the platform type selected.
|
|
1076
|
+
|
|
1077
|
+
Examples:
|
|
1078
|
+
bt pws quick functional
|
|
1079
|
+
"""
|
|
1080
|
+
import os
|
|
1081
|
+
|
|
1082
|
+
try:
|
|
1083
|
+
with get_client() as client:
|
|
1084
|
+
client.authenticate()
|
|
1085
|
+
|
|
1086
|
+
console.print("\n[bold cyan]Create Functional Account[/bold cyan]\n")
|
|
1087
|
+
|
|
1088
|
+
# Step 1: Choose platform
|
|
1089
|
+
platform_choice = prompt_choice(
|
|
1090
|
+
"Platform type",
|
|
1091
|
+
[
|
|
1092
|
+
("linux", "Linux/Unix (SSH)"),
|
|
1093
|
+
("windows", "Windows"),
|
|
1094
|
+
("entra", "Microsoft Entra ID (Azure AD)"),
|
|
1095
|
+
("aws", "Amazon Web Services"),
|
|
1096
|
+
("mssql", "MS SQL Server"),
|
|
1097
|
+
("mysql", "MySQL"),
|
|
1098
|
+
("postgres", "PostgreSQL"),
|
|
1099
|
+
("ad", "Active Directory"),
|
|
1100
|
+
],
|
|
1101
|
+
default="linux",
|
|
1102
|
+
)
|
|
1103
|
+
|
|
1104
|
+
# Map choice to platform ID
|
|
1105
|
+
platform_map = {
|
|
1106
|
+
"linux": 2,
|
|
1107
|
+
"windows": 1,
|
|
1108
|
+
"entra": 84,
|
|
1109
|
+
"aws": 47,
|
|
1110
|
+
"mssql": 11,
|
|
1111
|
+
"mysql": 10,
|
|
1112
|
+
"postgres": 79,
|
|
1113
|
+
"ad": 25,
|
|
1114
|
+
}
|
|
1115
|
+
platform_id = platform_map[platform_choice]
|
|
1116
|
+
|
|
1117
|
+
# Step 2: Basic info
|
|
1118
|
+
account_name = typer.prompt("Account name/username")
|
|
1119
|
+
display_name = typer.prompt("Display name", default=account_name)
|
|
1120
|
+
description = typer.prompt("Description", default="")
|
|
1121
|
+
|
|
1122
|
+
# Step 3: Platform-specific options
|
|
1123
|
+
password = None
|
|
1124
|
+
private_key = None
|
|
1125
|
+
passphrase = None
|
|
1126
|
+
elevation = None
|
|
1127
|
+
application_id = None
|
|
1128
|
+
tenant_id = None
|
|
1129
|
+
object_id = None
|
|
1130
|
+
secret = None
|
|
1131
|
+
api_key = None
|
|
1132
|
+
|
|
1133
|
+
if platform_choice == "linux":
|
|
1134
|
+
# Linux: Ask about SSH key and elevation
|
|
1135
|
+
console.print("\n[dim]Linux authentication options:[/dim]")
|
|
1136
|
+
auth_method = prompt_choice(
|
|
1137
|
+
"Authentication method",
|
|
1138
|
+
[
|
|
1139
|
+
("password", "Password"),
|
|
1140
|
+
("sshkey", "SSH Private Key"),
|
|
1141
|
+
],
|
|
1142
|
+
default="password",
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1145
|
+
if auth_method == "password":
|
|
1146
|
+
password = typer.prompt("Password", hide_input=True)
|
|
1147
|
+
else:
|
|
1148
|
+
key_path = typer.prompt("SSH private key file path", default="~/.ssh/id_rsa")
|
|
1149
|
+
key_path = os.path.expanduser(key_path)
|
|
1150
|
+
if os.path.exists(key_path):
|
|
1151
|
+
with open(key_path, "r") as f:
|
|
1152
|
+
private_key = f.read()
|
|
1153
|
+
console.print(f"[green]Loaded key from {key_path}[/green]")
|
|
1154
|
+
if typer.confirm("Is the key encrypted (has passphrase)?", default=False):
|
|
1155
|
+
passphrase = typer.prompt("Key passphrase", hide_input=True)
|
|
1156
|
+
else:
|
|
1157
|
+
console.print(f"[red]Key file not found: {key_path}[/red]")
|
|
1158
|
+
raise typer.Exit(1)
|
|
1159
|
+
|
|
1160
|
+
# Elevation command
|
|
1161
|
+
if typer.confirm("Configure elevation command (sudo)?", default=True):
|
|
1162
|
+
elevation = typer.prompt("Elevation command", default="sudo")
|
|
1163
|
+
|
|
1164
|
+
elif platform_choice == "windows":
|
|
1165
|
+
# Windows: Just password
|
|
1166
|
+
console.print("\n[dim]Windows authentication:[/dim]")
|
|
1167
|
+
password = typer.prompt("Password", hide_input=True)
|
|
1168
|
+
|
|
1169
|
+
elif platform_choice == "entra":
|
|
1170
|
+
# Entra ID: App registration details
|
|
1171
|
+
console.print("\n[dim]Entra ID (Azure AD) app registration:[/dim]")
|
|
1172
|
+
application_id = typer.prompt("Application (Client) ID")
|
|
1173
|
+
tenant_id = typer.prompt("Tenant ID")
|
|
1174
|
+
object_id = typer.prompt("Object ID")
|
|
1175
|
+
secret = typer.prompt("Client Secret", hide_input=True)
|
|
1176
|
+
|
|
1177
|
+
elif platform_choice == "aws":
|
|
1178
|
+
# AWS: Access keys
|
|
1179
|
+
console.print("\n[dim]AWS IAM credentials:[/dim]")
|
|
1180
|
+
api_key = typer.prompt("Access Key ID")
|
|
1181
|
+
secret = typer.prompt("Secret Access Key", hide_input=True)
|
|
1182
|
+
|
|
1183
|
+
elif platform_choice in ["mssql", "mysql", "postgres"]:
|
|
1184
|
+
# Databases: Just password
|
|
1185
|
+
console.print(f"\n[dim]{platform_choice.upper()} authentication:[/dim]")
|
|
1186
|
+
password = typer.prompt("Password", hide_input=True)
|
|
1187
|
+
|
|
1188
|
+
elif platform_choice == "ad":
|
|
1189
|
+
# Active Directory: Password
|
|
1190
|
+
console.print("\n[dim]Active Directory authentication:[/dim]")
|
|
1191
|
+
password = typer.prompt("Password", hide_input=True)
|
|
1192
|
+
|
|
1193
|
+
# Confirm before creating
|
|
1194
|
+
console.print("\n[bold]Summary:[/bold]")
|
|
1195
|
+
console.print(f" Platform: {platform_choice} (ID: {platform_id})")
|
|
1196
|
+
console.print(f" Account Name: {account_name}")
|
|
1197
|
+
console.print(f" Display Name: {display_name}")
|
|
1198
|
+
if description:
|
|
1199
|
+
console.print(f" Description: {description}")
|
|
1200
|
+
if elevation:
|
|
1201
|
+
console.print(f" Elevation: {elevation}")
|
|
1202
|
+
if application_id:
|
|
1203
|
+
console.print(f" App ID: {application_id}")
|
|
1204
|
+
if api_key:
|
|
1205
|
+
console.print(f" Access Key: {api_key[:8]}...")
|
|
1206
|
+
|
|
1207
|
+
if not typer.confirm("\nCreate this functional account?", default=True):
|
|
1208
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
1209
|
+
raise typer.Exit(0)
|
|
1210
|
+
|
|
1211
|
+
# Create the functional account
|
|
1212
|
+
console.print("\n[dim]Creating functional account...[/dim]")
|
|
1213
|
+
account = client.create_functional_account(
|
|
1214
|
+
account_name=account_name,
|
|
1215
|
+
platform_id=platform_id,
|
|
1216
|
+
display_name=display_name if display_name != account_name else None,
|
|
1217
|
+
description=description if description else None,
|
|
1218
|
+
elevation_command=elevation,
|
|
1219
|
+
password=password,
|
|
1220
|
+
private_key=private_key,
|
|
1221
|
+
passphrase=passphrase,
|
|
1222
|
+
application_id=application_id,
|
|
1223
|
+
tenant_id=tenant_id,
|
|
1224
|
+
object_id=object_id,
|
|
1225
|
+
secret=secret,
|
|
1226
|
+
api_key=api_key,
|
|
1227
|
+
)
|
|
1228
|
+
|
|
1229
|
+
if output == "json":
|
|
1230
|
+
console.print_json(json.dumps(account, default=str))
|
|
1231
|
+
else:
|
|
1232
|
+
console.print(Panel(
|
|
1233
|
+
f"[green]Functional account created![/green]\n\n"
|
|
1234
|
+
f"ID: [cyan]{account.get('FunctionalAccountID')}[/cyan]\n"
|
|
1235
|
+
f"Name: [bold]{account.get('DisplayName', account.get('AccountName'))}[/bold]\n"
|
|
1236
|
+
f"Platform: {account.get('PlatformID')}\n"
|
|
1237
|
+
+ (f"Elevation: {account.get('ElevationCommand')}\n" if account.get('ElevationCommand') else "")
|
|
1238
|
+
+ f"\n[dim]Use with: bt pws quick onboard -f {account.get('FunctionalAccountID')}[/dim]",
|
|
1239
|
+
title="Quick Functional Account",
|
|
1240
|
+
))
|
|
1241
|
+
|
|
1242
|
+
except httpx.HTTPStatusError as e:
|
|
1243
|
+
print_api_error(e, "quick functional")
|
|
1244
|
+
raise typer.Exit(1)
|
|
1245
|
+
except httpx.RequestError as e:
|
|
1246
|
+
print_api_error(e, "quick functional")
|
|
1247
|
+
raise typer.Exit(1)
|
|
1248
|
+
except typer.Exit:
|
|
1249
|
+
raise
|
|
1250
|
+
except Exception as e:
|
|
1251
|
+
print_api_error(e, "quick functional")
|
|
1252
|
+
raise typer.Exit(1)
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
@app.command("app-setup")
|
|
1256
|
+
def quick_app_setup(
|
|
1257
|
+
user_search: Optional[str] = typer.Option(None, "--user", "-u", help="User to search for (partial match)"),
|
|
1258
|
+
safe_name: Optional[str] = typer.Option(None, "--safe", "-s", help="Safe name to create"),
|
|
1259
|
+
folder_path: Optional[str] = typer.Option(None, "--folder", "-f", help="Folder path to create (e.g., 'Database/Production')"),
|
|
1260
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
1261
|
+
) -> None:
|
|
1262
|
+
"""Set up a safe for an application user with full permissions.
|
|
1263
|
+
|
|
1264
|
+
Interactive workflow that:
|
|
1265
|
+
1. Lists users and lets you select one (API users highlighted)
|
|
1266
|
+
2. Shows access policies for reference
|
|
1267
|
+
3. Creates a safe
|
|
1268
|
+
4. Assigns the user full permissions (Read, Create, Edit, Delete, Share, Manage)
|
|
1269
|
+
5. Optionally creates a folder path within the safe
|
|
1270
|
+
|
|
1271
|
+
Examples:
|
|
1272
|
+
bt pws quick app-setup # Fully interactive
|
|
1273
|
+
bt pws quick app-setup -u nhi-provision # Pre-select user
|
|
1274
|
+
bt pws quick app-setup -u nhi -s "MyApp Secrets" # Pre-fill user and safe
|
|
1275
|
+
bt pws quick app-setup -u nhi -s "MyApp" -f "Database/Prod"
|
|
1276
|
+
"""
|
|
1277
|
+
try:
|
|
1278
|
+
with get_client() as client:
|
|
1279
|
+
client.authenticate()
|
|
1280
|
+
|
|
1281
|
+
console.print("\n[bold cyan]Application Safe Setup[/bold cyan]\n")
|
|
1282
|
+
|
|
1283
|
+
# =================================================================
|
|
1284
|
+
# Step 1: Select user
|
|
1285
|
+
# =================================================================
|
|
1286
|
+
console.print("[bold]Step 1: Select User[/bold]")
|
|
1287
|
+
|
|
1288
|
+
all_users = client.list_users()
|
|
1289
|
+
|
|
1290
|
+
# If user search provided, filter
|
|
1291
|
+
if user_search:
|
|
1292
|
+
search_lower = user_search.lower()
|
|
1293
|
+
filtered_users = [
|
|
1294
|
+
u for u in all_users
|
|
1295
|
+
if search_lower in (u.get("UserName", "") or "").lower()
|
|
1296
|
+
or search_lower in (u.get("EmailAddress", "") or "").lower()
|
|
1297
|
+
or search_lower in (u.get("FirstName", "") or "").lower()
|
|
1298
|
+
or search_lower in (u.get("LastName", "") or "").lower()
|
|
1299
|
+
]
|
|
1300
|
+
if not filtered_users:
|
|
1301
|
+
print_error(f"No user found matching '{user_search}'")
|
|
1302
|
+
raise typer.Exit(1)
|
|
1303
|
+
else:
|
|
1304
|
+
filtered_users = all_users
|
|
1305
|
+
|
|
1306
|
+
# Show users table
|
|
1307
|
+
users_table = Table(title="Available Users")
|
|
1308
|
+
users_table.add_column("#", style="dim", width=4)
|
|
1309
|
+
users_table.add_column("ID", style="cyan", width=6)
|
|
1310
|
+
users_table.add_column("Username", style="green")
|
|
1311
|
+
users_table.add_column("Name", style="yellow")
|
|
1312
|
+
users_table.add_column("Type", style="magenta", width=8)
|
|
1313
|
+
|
|
1314
|
+
# Show max 20 users
|
|
1315
|
+
display_users = filtered_users[:20]
|
|
1316
|
+
for i, user in enumerate(display_users, 1):
|
|
1317
|
+
first = user.get("FirstName") or ""
|
|
1318
|
+
last = user.get("LastName") or ""
|
|
1319
|
+
display_name = f"{first} {last}".strip() or "-"
|
|
1320
|
+
is_api = "API" if user.get("ClientID") else "Human"
|
|
1321
|
+
users_table.add_row(
|
|
1322
|
+
str(i),
|
|
1323
|
+
str(user.get("UserID")),
|
|
1324
|
+
user.get("UserName", ""),
|
|
1325
|
+
display_name,
|
|
1326
|
+
is_api,
|
|
1327
|
+
)
|
|
1328
|
+
|
|
1329
|
+
console.print(users_table)
|
|
1330
|
+
|
|
1331
|
+
if len(filtered_users) > 20:
|
|
1332
|
+
console.print(f"[dim]... and {len(filtered_users) - 20} more users (use --user to filter)[/dim]")
|
|
1333
|
+
|
|
1334
|
+
# Prompt for selection
|
|
1335
|
+
selection = typer.prompt("Select user (enter # or User ID)", type=str)
|
|
1336
|
+
try:
|
|
1337
|
+
sel_int = int(selection)
|
|
1338
|
+
if 1 <= sel_int <= len(display_users):
|
|
1339
|
+
selected_user = display_users[sel_int - 1]
|
|
1340
|
+
else:
|
|
1341
|
+
# Treat as User ID
|
|
1342
|
+
selected_user = next((u for u in all_users if u.get("UserID") == sel_int), None)
|
|
1343
|
+
except ValueError:
|
|
1344
|
+
# Try matching by username
|
|
1345
|
+
selected_user = next(
|
|
1346
|
+
(u for u in all_users if u.get("UserName", "").lower() == selection.lower()),
|
|
1347
|
+
None
|
|
1348
|
+
)
|
|
1349
|
+
|
|
1350
|
+
if not selected_user:
|
|
1351
|
+
print_error(f"Invalid selection: {selection}")
|
|
1352
|
+
raise typer.Exit(1)
|
|
1353
|
+
|
|
1354
|
+
user_id = selected_user.get("UserID")
|
|
1355
|
+
user_name = selected_user.get("UserName")
|
|
1356
|
+
console.print(f"[green]Selected user:[/green] {user_name} (ID: {user_id})")
|
|
1357
|
+
|
|
1358
|
+
# =================================================================
|
|
1359
|
+
# Step 2: Show access policies (informational)
|
|
1360
|
+
# =================================================================
|
|
1361
|
+
console.print("\n[bold]Step 2: Access Policies (Reference)[/bold]")
|
|
1362
|
+
console.print("[dim]These are the available access policies in the system:[/dim]")
|
|
1363
|
+
|
|
1364
|
+
policies = client.get("/AccessPolicies")
|
|
1365
|
+
if policies:
|
|
1366
|
+
policies_table = Table(title="Access Policies")
|
|
1367
|
+
policies_table.add_column("ID", style="cyan", width=8)
|
|
1368
|
+
policies_table.add_column("Name", style="green")
|
|
1369
|
+
policies_table.add_column("Description", style="yellow")
|
|
1370
|
+
|
|
1371
|
+
for policy in policies[:10]:
|
|
1372
|
+
policies_table.add_row(
|
|
1373
|
+
str(policy.get("AccessPolicyID", "")),
|
|
1374
|
+
policy.get("Name", ""),
|
|
1375
|
+
(policy.get("Description") or "-")[:50],
|
|
1376
|
+
)
|
|
1377
|
+
console.print(policies_table)
|
|
1378
|
+
console.print("[dim]Note: Access policies are assigned via user groups, not directly to safes.[/dim]")
|
|
1379
|
+
else:
|
|
1380
|
+
console.print("[yellow]No access policies found.[/yellow]")
|
|
1381
|
+
|
|
1382
|
+
# =================================================================
|
|
1383
|
+
# Step 3: Create safe
|
|
1384
|
+
# =================================================================
|
|
1385
|
+
console.print("\n[bold]Step 3: Create Safe[/bold]")
|
|
1386
|
+
|
|
1387
|
+
if not safe_name:
|
|
1388
|
+
safe_name = typer.prompt("Safe name")
|
|
1389
|
+
|
|
1390
|
+
safe_description = typer.prompt("Safe description (optional)", default="")
|
|
1391
|
+
|
|
1392
|
+
# Confirmation before creating
|
|
1393
|
+
console.print("\n[bold]Review:[/bold]")
|
|
1394
|
+
console.print(f" User: [cyan]{user_name}[/cyan] (ID: {user_id})")
|
|
1395
|
+
console.print(f" Safe: [cyan]{safe_name}[/cyan]")
|
|
1396
|
+
if safe_description:
|
|
1397
|
+
console.print(f" Description: [dim]{safe_description}[/dim]")
|
|
1398
|
+
console.print(f" Permissions: [green]Full (Read, Create, Edit, Delete, Share, Manage)[/green]")
|
|
1399
|
+
|
|
1400
|
+
if not typer.confirm("\nProceed with setup?", default=True):
|
|
1401
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
1402
|
+
raise typer.Exit(0)
|
|
1403
|
+
|
|
1404
|
+
console.print(f"\n[dim]Creating safe '{safe_name}'...[/dim]")
|
|
1405
|
+
safe = client.create_safe(name=safe_name, description=safe_description or None)
|
|
1406
|
+
safe_id = safe.get("Id")
|
|
1407
|
+
console.print(f"[green]Created safe:[/green] {safe_name} (ID: {safe_id})")
|
|
1408
|
+
|
|
1409
|
+
# =================================================================
|
|
1410
|
+
# Step 4: Assign permissions
|
|
1411
|
+
# =================================================================
|
|
1412
|
+
console.print("\n[bold]Step 4: Assign Permissions[/bold]")
|
|
1413
|
+
|
|
1414
|
+
# Full permissions
|
|
1415
|
+
full_permissions = ["Read", "Create", "Edit", "Delete", "Share", "Manage"]
|
|
1416
|
+
|
|
1417
|
+
console.print(f"[dim]Granting full permissions to user {user_name}...[/dim]")
|
|
1418
|
+
try:
|
|
1419
|
+
client.grant_safe_permission_to_user(
|
|
1420
|
+
safe_id=safe_id,
|
|
1421
|
+
user_id=user_id,
|
|
1422
|
+
permission_flags=full_permissions,
|
|
1423
|
+
)
|
|
1424
|
+
console.print(f"[green]Granted permissions:[/green] {', '.join(full_permissions)}")
|
|
1425
|
+
except httpx.HTTPStatusError as perm_err:
|
|
1426
|
+
# Permission assignment failed - clean up the safe
|
|
1427
|
+
console.print(f"[red]Failed to assign permissions.[/red]")
|
|
1428
|
+
if "SecretsSafe" in str(perm_err.response.text) or "role" in str(perm_err.response.text).lower():
|
|
1429
|
+
console.print(f"[yellow]The user '{user_name}' may not have the SecretsSafe/WorkforcePasswords role.[/yellow]")
|
|
1430
|
+
console.print("[dim]This role must be assigned via user group membership in the BeyondInsight console.[/dim]")
|
|
1431
|
+
else:
|
|
1432
|
+
console.print(f"[dim]Error: {perm_err.response.text}[/dim]")
|
|
1433
|
+
|
|
1434
|
+
# Offer to keep or delete the safe
|
|
1435
|
+
if typer.confirm(f"\nDelete the created safe '{safe_name}'?", default=True):
|
|
1436
|
+
try:
|
|
1437
|
+
client.delete(f"/Secrets-Safe/Safes/{safe_id}")
|
|
1438
|
+
console.print(f"[yellow]Deleted safe: {safe_id}[/yellow]")
|
|
1439
|
+
except Exception:
|
|
1440
|
+
console.print(f"[red]Could not delete safe. Delete manually: bt pws secrets safes delete {safe_id}[/red]")
|
|
1441
|
+
else:
|
|
1442
|
+
console.print(f"[dim]Safe kept. You can assign permissions manually or delete with:[/dim]")
|
|
1443
|
+
console.print(f"[dim] bt pws secrets safes delete {safe_id}[/dim]")
|
|
1444
|
+
raise typer.Exit(1)
|
|
1445
|
+
|
|
1446
|
+
# =================================================================
|
|
1447
|
+
# Step 5: Create folder (optional)
|
|
1448
|
+
# =================================================================
|
|
1449
|
+
console.print("\n[bold]Step 5: Create Folder (Optional)[/bold]")
|
|
1450
|
+
|
|
1451
|
+
created_folders = []
|
|
1452
|
+
if folder_path is None:
|
|
1453
|
+
if typer.confirm("Create a folder inside the safe?", default=False):
|
|
1454
|
+
folder_path = typer.prompt("Folder path (e.g., 'Database/Production')")
|
|
1455
|
+
|
|
1456
|
+
if folder_path:
|
|
1457
|
+
# Parse path and create nested folders
|
|
1458
|
+
path_parts = [p.strip() for p in folder_path.split("/") if p.strip()]
|
|
1459
|
+
parent_id = safe_id
|
|
1460
|
+
|
|
1461
|
+
for part in path_parts:
|
|
1462
|
+
console.print(f"[dim]Creating folder '{part}'...[/dim]")
|
|
1463
|
+
folder = client.create_folder(
|
|
1464
|
+
name=part,
|
|
1465
|
+
parent_id=parent_id,
|
|
1466
|
+
description=f"Folder in {safe_name}",
|
|
1467
|
+
)
|
|
1468
|
+
folder_id = folder.get("Id")
|
|
1469
|
+
created_folders.append({"name": part, "id": folder_id})
|
|
1470
|
+
console.print(f"[green]Created folder:[/green] {part} (ID: {folder_id})")
|
|
1471
|
+
parent_id = folder_id
|
|
1472
|
+
|
|
1473
|
+
# =================================================================
|
|
1474
|
+
# Summary
|
|
1475
|
+
# =================================================================
|
|
1476
|
+
console.print()
|
|
1477
|
+
|
|
1478
|
+
if output == "json":
|
|
1479
|
+
result = {
|
|
1480
|
+
"user": selected_user,
|
|
1481
|
+
"safe": safe,
|
|
1482
|
+
"permissions": full_permissions,
|
|
1483
|
+
"folders": created_folders,
|
|
1484
|
+
}
|
|
1485
|
+
console.print_json(json.dumps(result, default=str))
|
|
1486
|
+
else:
|
|
1487
|
+
summary_lines = [
|
|
1488
|
+
f"[green]Application safe setup complete![/green]\n",
|
|
1489
|
+
f"[bold]User:[/bold] {user_name} (ID: {user_id})",
|
|
1490
|
+
f"[bold]Safe:[/bold] {safe_name} (ID: {safe_id})",
|
|
1491
|
+
f"[bold]Permissions:[/bold] {', '.join(full_permissions)}",
|
|
1492
|
+
]
|
|
1493
|
+
if created_folders:
|
|
1494
|
+
folder_display = " / ".join(f["name"] for f in created_folders)
|
|
1495
|
+
summary_lines.append(f"[bold]Folders:[/bold] {folder_display}")
|
|
1496
|
+
|
|
1497
|
+
summary_lines.append(f"\n[dim]Add secrets: bt pws secrets secrets create --folder {created_folders[-1]['id'] if created_folders else safe_id}[/dim]")
|
|
1498
|
+
|
|
1499
|
+
console.print(Panel(
|
|
1500
|
+
"\n".join(summary_lines),
|
|
1501
|
+
title="App Setup Complete",
|
|
1502
|
+
))
|
|
1503
|
+
|
|
1504
|
+
except httpx.HTTPStatusError as e:
|
|
1505
|
+
print_api_error(e, "quick app-setup")
|
|
1506
|
+
raise typer.Exit(1)
|
|
1507
|
+
except httpx.RequestError as e:
|
|
1508
|
+
print_api_error(e, "quick app-setup")
|
|
1509
|
+
raise typer.Exit(1)
|
|
1510
|
+
except typer.Exit:
|
|
1511
|
+
raise
|
|
1512
|
+
except Exception as e:
|
|
1513
|
+
print_api_error(e, "quick app-setup")
|
|
1514
|
+
raise typer.Exit(1)
|
|
1515
|
+
|
|
1516
|
+
|
|
1517
|
+
@app.command("get-secret")
|
|
1518
|
+
def quick_get_secret(
|
|
1519
|
+
path: Optional[str] = typer.Argument(None, help="Secret path (e.g., 'Safe/Folder/SecretName')"),
|
|
1520
|
+
show_password: bool = typer.Option(True, "--show-password/--hide-password", help="Show or hide password"),
|
|
1521
|
+
raw: bool = typer.Option(False, "--raw", help="Output only the password (for scripts)"),
|
|
1522
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
1523
|
+
) -> None:
|
|
1524
|
+
"""Get a secret by its path.
|
|
1525
|
+
|
|
1526
|
+
Path format: Safe/Folder/Subfolder/SecretName
|
|
1527
|
+
|
|
1528
|
+
Examples:
|
|
1529
|
+
bt pws quick get-secret "Example1/Folder1/ID-Pass"
|
|
1530
|
+
bt pws quick get-secret "PAM Demo Credentials/Service Accounts/db-admin"
|
|
1531
|
+
bt pws quick get-secret "MySafe/MySecret" --hide-password
|
|
1532
|
+
PASSWORD=$(bt pws quick get-secret "MySafe/Secret" --raw)
|
|
1533
|
+
"""
|
|
1534
|
+
try:
|
|
1535
|
+
with get_client() as client:
|
|
1536
|
+
client.authenticate()
|
|
1537
|
+
|
|
1538
|
+
# Interactive mode if no path provided
|
|
1539
|
+
if not path:
|
|
1540
|
+
console.print("\n[bold cyan]Get Secret by Path[/bold cyan]\n")
|
|
1541
|
+
|
|
1542
|
+
# Show available safes
|
|
1543
|
+
safes = client.list_safes()
|
|
1544
|
+
console.print("[bold]Available Safes:[/bold]")
|
|
1545
|
+
for s in safes:
|
|
1546
|
+
console.print(f" - {s.get('Name')}")
|
|
1547
|
+
console.print()
|
|
1548
|
+
|
|
1549
|
+
path = typer.prompt("Secret path (Safe/Folder/SecretName)")
|
|
1550
|
+
|
|
1551
|
+
# Parse path
|
|
1552
|
+
path_parts = [p.strip() for p in path.split("/") if p.strip()]
|
|
1553
|
+
if len(path_parts) < 2:
|
|
1554
|
+
print_error("Path must have at least Safe/SecretName (e.g., 'MySafe/MySecret')")
|
|
1555
|
+
raise typer.Exit(1)
|
|
1556
|
+
|
|
1557
|
+
secret_name = path_parts[-1]
|
|
1558
|
+
folder_path = "/".join(path_parts[:-1])
|
|
1559
|
+
|
|
1560
|
+
if not raw:
|
|
1561
|
+
console.print(f"[dim]Searching for secret '{secret_name}' in '{folder_path}'...[/dim]")
|
|
1562
|
+
|
|
1563
|
+
# Get all secrets and find by path
|
|
1564
|
+
secrets = client.list_secrets()
|
|
1565
|
+
matched_secret = None
|
|
1566
|
+
|
|
1567
|
+
for secret in secrets:
|
|
1568
|
+
secret_folder_path = secret.get("FolderPath", "")
|
|
1569
|
+
secret_title = secret.get("Title", "")
|
|
1570
|
+
|
|
1571
|
+
# Match by folder path and title
|
|
1572
|
+
if secret_folder_path.lower() == folder_path.lower() and secret_title.lower() == secret_name.lower():
|
|
1573
|
+
matched_secret = secret
|
|
1574
|
+
break
|
|
1575
|
+
|
|
1576
|
+
if not matched_secret:
|
|
1577
|
+
# Try partial match
|
|
1578
|
+
for secret in secrets:
|
|
1579
|
+
secret_folder_path = secret.get("FolderPath", "")
|
|
1580
|
+
secret_title = secret.get("Title", "")
|
|
1581
|
+
full_path = f"{secret_folder_path}/{secret_title}".lower()
|
|
1582
|
+
|
|
1583
|
+
if path.lower() in full_path or full_path.endswith(path.lower()):
|
|
1584
|
+
matched_secret = secret
|
|
1585
|
+
if not raw:
|
|
1586
|
+
console.print(f"[yellow]Partial match found: {secret_folder_path}/{secret_title}[/yellow]")
|
|
1587
|
+
break
|
|
1588
|
+
|
|
1589
|
+
if not matched_secret:
|
|
1590
|
+
print_error(f"No secret found at path '{path}'")
|
|
1591
|
+
|
|
1592
|
+
# Show similar paths
|
|
1593
|
+
similar = []
|
|
1594
|
+
for secret in secrets:
|
|
1595
|
+
fp = secret.get("FolderPath", "")
|
|
1596
|
+
title = secret.get("Title", "")
|
|
1597
|
+
if path_parts[0].lower() in fp.lower() or secret_name.lower() in title.lower():
|
|
1598
|
+
similar.append(f"{fp}/{title}")
|
|
1599
|
+
|
|
1600
|
+
if similar and not raw:
|
|
1601
|
+
console.print("\n[dim]Similar secrets:[/dim]")
|
|
1602
|
+
for s in similar[:5]:
|
|
1603
|
+
console.print(f" - {s}")
|
|
1604
|
+
|
|
1605
|
+
raise typer.Exit(1)
|
|
1606
|
+
|
|
1607
|
+
# Get full secret details (includes password)
|
|
1608
|
+
secret_id = matched_secret.get("Id")
|
|
1609
|
+
secret_details = client.get_secret(secret_id)
|
|
1610
|
+
|
|
1611
|
+
# Output
|
|
1612
|
+
if raw:
|
|
1613
|
+
print(secret_details.get("Password", ""), end="")
|
|
1614
|
+
elif output == "json":
|
|
1615
|
+
if not show_password:
|
|
1616
|
+
secret_details["Password"] = "********"
|
|
1617
|
+
console.print_json(json.dumps(secret_details, default=str))
|
|
1618
|
+
else:
|
|
1619
|
+
full_path = f"{secret_details.get('FolderPath', '')}/{secret_details.get('Title', '')}"
|
|
1620
|
+
password = secret_details.get("Password", "")
|
|
1621
|
+
|
|
1622
|
+
console.print(Panel(
|
|
1623
|
+
f"[bold]Path:[/bold] {full_path}\n"
|
|
1624
|
+
f"[bold]Title:[/bold] {secret_details.get('Title', '-')}\n"
|
|
1625
|
+
f"[bold]Type:[/bold] {secret_details.get('SecretType', '-')}\n"
|
|
1626
|
+
f"[bold]Username:[/bold] {secret_details.get('Username') or '-'}\n"
|
|
1627
|
+
f"[bold]Password:[/bold] {'[green]' + password + '[/green]' if show_password else '[dim]********[/dim]'}\n"
|
|
1628
|
+
+ (f"[bold]Description:[/bold] {secret_details.get('Description')}\n" if secret_details.get('Description') else "")
|
|
1629
|
+
+ (f"[bold]Notes:[/bold] {secret_details.get('Notes')}\n" if secret_details.get('Notes') else "")
|
|
1630
|
+
+ (f"[bold]URLs:[/bold] {', '.join(u.get('Url', '') for u in secret_details.get('Urls', []))}\n" if secret_details.get('Urls') else "")
|
|
1631
|
+
+ f"\n[dim]Owner: {secret_details.get('Owner', '-')}[/dim]\n"
|
|
1632
|
+
f"[dim]Modified: {secret_details.get('ModifiedOn', '-')} by {secret_details.get('ModifiedBy', '-')}[/dim]",
|
|
1633
|
+
title=f"Secret: {secret_details.get('Title')}",
|
|
1634
|
+
))
|
|
1635
|
+
|
|
1636
|
+
except httpx.HTTPStatusError as e:
|
|
1637
|
+
print_api_error(e, "quick get-secret")
|
|
1638
|
+
raise typer.Exit(1)
|
|
1639
|
+
except httpx.RequestError as e:
|
|
1640
|
+
print_api_error(e, "quick get-secret")
|
|
1641
|
+
raise typer.Exit(1)
|
|
1642
|
+
except typer.Exit:
|
|
1643
|
+
raise
|
|
1644
|
+
except Exception as e:
|
|
1645
|
+
print_api_error(e, "quick get-secret")
|
|
1646
|
+
raise typer.Exit(1)
|