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,415 @@
|
|
|
1
|
+
"""CLI commands for managing users and user groups."""
|
|
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.table import Table
|
|
10
|
+
|
|
11
|
+
from ...core.output import print_api_error
|
|
12
|
+
from ..client.base import get_client
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(no_args_is_help=True, help="Manage users, user groups, and roles")
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def print_users_table(users: list[dict], title: str = "Users") -> None:
|
|
19
|
+
"""Print users in a formatted table."""
|
|
20
|
+
table = Table(title=title)
|
|
21
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
22
|
+
table.add_column("Username", style="green")
|
|
23
|
+
table.add_column("Name", style="yellow")
|
|
24
|
+
table.add_column("Email", style="blue")
|
|
25
|
+
table.add_column("Active", style="magenta", no_wrap=True)
|
|
26
|
+
table.add_column("Last Login", style="dim")
|
|
27
|
+
|
|
28
|
+
for user in users:
|
|
29
|
+
# Build display name from first/last
|
|
30
|
+
first = user.get("FirstName") or ""
|
|
31
|
+
last = user.get("LastName") or ""
|
|
32
|
+
display_name = f"{first} {last}".strip() or "-"
|
|
33
|
+
|
|
34
|
+
# Format last login
|
|
35
|
+
last_login = user.get("LastLoginDate")
|
|
36
|
+
if last_login:
|
|
37
|
+
# Just show date part
|
|
38
|
+
last_login = str(last_login)[:10]
|
|
39
|
+
else:
|
|
40
|
+
last_login = "-"
|
|
41
|
+
|
|
42
|
+
# Check if API user
|
|
43
|
+
is_api = "ClientID" in user and user.get("ClientID")
|
|
44
|
+
username = user.get("UserName", "")
|
|
45
|
+
if is_api:
|
|
46
|
+
username = f"{username} [dim](API)[/dim]"
|
|
47
|
+
|
|
48
|
+
table.add_row(
|
|
49
|
+
str(user.get("UserID", "")),
|
|
50
|
+
username,
|
|
51
|
+
display_name,
|
|
52
|
+
user.get("EmailAddress") or "-",
|
|
53
|
+
"Yes" if user.get("IsActive") else "No",
|
|
54
|
+
last_login,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
console.print(table)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def print_user_detail(user: dict) -> None:
|
|
61
|
+
"""Print detailed user information."""
|
|
62
|
+
console.print(f"\n[bold cyan]User: {user.get('UserName', 'Unknown')}[/bold cyan]\n")
|
|
63
|
+
|
|
64
|
+
info_table = Table(show_header=False, box=None)
|
|
65
|
+
info_table.add_column("Field", style="dim", width=25)
|
|
66
|
+
info_table.add_column("Value")
|
|
67
|
+
|
|
68
|
+
fields = [
|
|
69
|
+
("ID", "UserID"),
|
|
70
|
+
("Username", "UserName"),
|
|
71
|
+
("First Name", "FirstName"),
|
|
72
|
+
("Last Name", "LastName"),
|
|
73
|
+
("Email", "EmailAddress"),
|
|
74
|
+
("Domain", "DomainName"),
|
|
75
|
+
("Distinguished Name", "DistinguishedName"),
|
|
76
|
+
("Active", "IsActive"),
|
|
77
|
+
("Quarantined", "IsQuarantined"),
|
|
78
|
+
("Last Login", "LastLoginDate"),
|
|
79
|
+
("Auth Type", "LastLoginAuthenticationType"),
|
|
80
|
+
("SSO URL", "LastLoginSSOURL"),
|
|
81
|
+
("SAML IDP URL", "LastLoginSAMLIDPURL"),
|
|
82
|
+
("Config Name", "LastLoginConfigurationName"),
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
for label, key in fields:
|
|
86
|
+
value = user.get(key)
|
|
87
|
+
if value is not None:
|
|
88
|
+
if isinstance(value, bool):
|
|
89
|
+
value = "Yes" if value else "No"
|
|
90
|
+
info_table.add_row(label, str(value))
|
|
91
|
+
|
|
92
|
+
console.print(info_table)
|
|
93
|
+
|
|
94
|
+
# Show API user details if present
|
|
95
|
+
if user.get("ClientID"):
|
|
96
|
+
console.print("\n[bold yellow]API User Details:[/bold yellow]")
|
|
97
|
+
api_table = Table(show_header=False, box=None)
|
|
98
|
+
api_table.add_column("Field", style="dim", width=25)
|
|
99
|
+
api_table.add_column("Value")
|
|
100
|
+
api_table.add_row("Client ID", user.get("ClientID", "-"))
|
|
101
|
+
api_table.add_row("Access Policy ID", str(user.get("AccessPolicyID", "-")))
|
|
102
|
+
console.print(api_table)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def print_groups_table(groups: list[dict], title: str = "User Groups") -> None:
|
|
106
|
+
"""Print user groups in a formatted table."""
|
|
107
|
+
table = Table(title=title)
|
|
108
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
109
|
+
table.add_column("Name", style="green")
|
|
110
|
+
table.add_column("Description", style="yellow")
|
|
111
|
+
table.add_column("Type", style="blue")
|
|
112
|
+
table.add_column("Active", style="magenta", no_wrap=True)
|
|
113
|
+
|
|
114
|
+
for group in groups:
|
|
115
|
+
table.add_row(
|
|
116
|
+
str(group.get("GroupID", "")),
|
|
117
|
+
group.get("Name", ""),
|
|
118
|
+
group.get("Description") or "-",
|
|
119
|
+
group.get("GroupType") or "-",
|
|
120
|
+
"Yes" if group.get("IsActive") else "No",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
console.print(table)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def print_group_detail(group: dict) -> None:
|
|
127
|
+
"""Print detailed user group information."""
|
|
128
|
+
console.print(f"\n[bold cyan]User Group: {group.get('Name', 'Unknown')}[/bold cyan]\n")
|
|
129
|
+
|
|
130
|
+
info_table = Table(show_header=False, box=None)
|
|
131
|
+
info_table.add_column("Field", style="dim", width=30)
|
|
132
|
+
info_table.add_column("Value")
|
|
133
|
+
|
|
134
|
+
fields = [
|
|
135
|
+
("ID", "GroupID"),
|
|
136
|
+
("Name", "Name"),
|
|
137
|
+
("Description", "Description"),
|
|
138
|
+
("Type", "GroupType"),
|
|
139
|
+
("Active", "IsActive"),
|
|
140
|
+
("Role Type", "RoleType"),
|
|
141
|
+
("Distinguished Name", "DistinguishedName"),
|
|
142
|
+
("Excluded From Global Sync", "ExcludedFromGlobalSync"),
|
|
143
|
+
("Override Global Sync Settings", "OverrideGlobalSyncSettings"),
|
|
144
|
+
("Membership Attribute", "MembershipAttribute"),
|
|
145
|
+
("Account Attribute", "AccountAttribute"),
|
|
146
|
+
("Base Distinguished Name", "BaseDistinguishedName"),
|
|
147
|
+
("Application Registration IDs", "ApplicationRegistrationIDs"),
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
for label, key in fields:
|
|
151
|
+
value = group.get(key)
|
|
152
|
+
if value is not None:
|
|
153
|
+
if isinstance(value, bool):
|
|
154
|
+
value = "Yes" if value else "No"
|
|
155
|
+
info_table.add_row(label, str(value))
|
|
156
|
+
|
|
157
|
+
console.print(info_table)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def print_roles_table(roles: list[dict], title: str = "Roles") -> None:
|
|
161
|
+
"""Print roles in a formatted table."""
|
|
162
|
+
table = Table(title=title)
|
|
163
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
164
|
+
table.add_column("Name", style="green")
|
|
165
|
+
|
|
166
|
+
for role in roles:
|
|
167
|
+
table.add_row(
|
|
168
|
+
str(role.get("RoleID", "")),
|
|
169
|
+
role.get("Name", ""),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
console.print(table)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# =============================================================================
|
|
176
|
+
# Users Commands
|
|
177
|
+
# =============================================================================
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@app.command("list")
|
|
181
|
+
def list_users(
|
|
182
|
+
search: Optional[str] = typer.Option(None, "--search", "-s", help="Search by username"),
|
|
183
|
+
limit: int = typer.Option(50, "--limit", "-l", help="Maximum results (default: 50)"),
|
|
184
|
+
fetch_all: bool = typer.Option(False, "--all", help="Fetch all results (may be slow)"),
|
|
185
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
186
|
+
) -> None:
|
|
187
|
+
"""List all users.
|
|
188
|
+
|
|
189
|
+
Examples:
|
|
190
|
+
bt pws users list # First 50 users
|
|
191
|
+
bt pws users list --all # All users
|
|
192
|
+
bt pws users list -s "admin" # Search by username
|
|
193
|
+
bt pws users list -o json # JSON output
|
|
194
|
+
"""
|
|
195
|
+
try:
|
|
196
|
+
with get_client() as client:
|
|
197
|
+
client.authenticate()
|
|
198
|
+
users = client.list_users(search=search, limit=None if fetch_all else limit)
|
|
199
|
+
|
|
200
|
+
if output == "json":
|
|
201
|
+
console.print_json(json.dumps(users, default=str))
|
|
202
|
+
else:
|
|
203
|
+
if users:
|
|
204
|
+
print_users_table(users)
|
|
205
|
+
if not fetch_all and len(users) == limit:
|
|
206
|
+
console.print(
|
|
207
|
+
f"[dim]Showing {len(users)} results. Use --all to fetch all results.[/dim]"
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
console.print("[yellow]No users found.[/yellow]")
|
|
211
|
+
|
|
212
|
+
except httpx.HTTPStatusError as e:
|
|
213
|
+
print_api_error(e, "list users")
|
|
214
|
+
raise typer.Exit(1)
|
|
215
|
+
except httpx.RequestError as e:
|
|
216
|
+
print_api_error(e, "list users")
|
|
217
|
+
raise typer.Exit(1)
|
|
218
|
+
except Exception as e:
|
|
219
|
+
print_api_error(e, "list users")
|
|
220
|
+
raise typer.Exit(1)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@app.command("get")
|
|
224
|
+
def get_user(
|
|
225
|
+
user_id: int = typer.Argument(..., help="User ID"),
|
|
226
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
227
|
+
) -> None:
|
|
228
|
+
"""Get a user by ID.
|
|
229
|
+
|
|
230
|
+
Examples:
|
|
231
|
+
bt pws users get 1 # Get user ID 1
|
|
232
|
+
bt pws users get 4 -o json # JSON output
|
|
233
|
+
"""
|
|
234
|
+
try:
|
|
235
|
+
with get_client() as client:
|
|
236
|
+
client.authenticate()
|
|
237
|
+
user = client.get_user(user_id)
|
|
238
|
+
|
|
239
|
+
if output == "json":
|
|
240
|
+
console.print_json(json.dumps(user, default=str))
|
|
241
|
+
else:
|
|
242
|
+
print_user_detail(user)
|
|
243
|
+
|
|
244
|
+
except httpx.HTTPStatusError as e:
|
|
245
|
+
print_api_error(e, "get user")
|
|
246
|
+
raise typer.Exit(1)
|
|
247
|
+
except httpx.RequestError as e:
|
|
248
|
+
print_api_error(e, "get user")
|
|
249
|
+
raise typer.Exit(1)
|
|
250
|
+
except Exception as e:
|
|
251
|
+
print_api_error(e, "get user")
|
|
252
|
+
raise typer.Exit(1)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# =============================================================================
|
|
256
|
+
# User Groups Commands
|
|
257
|
+
# =============================================================================
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@app.command("groups")
|
|
261
|
+
def list_groups(
|
|
262
|
+
search: Optional[str] = typer.Option(None, "--search", "-s", help="Search by group name"),
|
|
263
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
264
|
+
) -> None:
|
|
265
|
+
"""List all user groups.
|
|
266
|
+
|
|
267
|
+
Examples:
|
|
268
|
+
bt pws users groups # All user groups
|
|
269
|
+
bt pws users groups -s "admin" # Search groups
|
|
270
|
+
bt pws users groups -o json # JSON output
|
|
271
|
+
"""
|
|
272
|
+
try:
|
|
273
|
+
with get_client() as client:
|
|
274
|
+
client.authenticate()
|
|
275
|
+
groups = client.list_user_groups(search=search)
|
|
276
|
+
|
|
277
|
+
if output == "json":
|
|
278
|
+
console.print_json(json.dumps(groups, default=str))
|
|
279
|
+
else:
|
|
280
|
+
if groups:
|
|
281
|
+
print_groups_table(groups)
|
|
282
|
+
else:
|
|
283
|
+
console.print("[yellow]No user groups found.[/yellow]")
|
|
284
|
+
|
|
285
|
+
except httpx.HTTPStatusError as e:
|
|
286
|
+
print_api_error(e, "list user groups")
|
|
287
|
+
raise typer.Exit(1)
|
|
288
|
+
except httpx.RequestError as e:
|
|
289
|
+
print_api_error(e, "list user groups")
|
|
290
|
+
raise typer.Exit(1)
|
|
291
|
+
except Exception as e:
|
|
292
|
+
print_api_error(e, "list user groups")
|
|
293
|
+
raise typer.Exit(1)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@app.command("group")
|
|
297
|
+
def get_group(
|
|
298
|
+
group_id: int = typer.Argument(..., help="User group ID"),
|
|
299
|
+
show_members: bool = typer.Option(False, "--members", "-m", help="Show group members"),
|
|
300
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
301
|
+
) -> None:
|
|
302
|
+
"""Get a user group by ID.
|
|
303
|
+
|
|
304
|
+
Examples:
|
|
305
|
+
bt pws users group 1 # Get group ID 1
|
|
306
|
+
bt pws users group 1 --members # Show group with members
|
|
307
|
+
bt pws users group 1 -o json # JSON output
|
|
308
|
+
"""
|
|
309
|
+
try:
|
|
310
|
+
with get_client() as client:
|
|
311
|
+
client.authenticate()
|
|
312
|
+
group = client.get_user_group(group_id)
|
|
313
|
+
|
|
314
|
+
members = []
|
|
315
|
+
if show_members:
|
|
316
|
+
members = client.get_user_group_members(group_id)
|
|
317
|
+
|
|
318
|
+
if output == "json":
|
|
319
|
+
result = {"group": group, "members": members} if show_members else group
|
|
320
|
+
console.print_json(json.dumps(result, default=str))
|
|
321
|
+
else:
|
|
322
|
+
print_group_detail(group)
|
|
323
|
+
if show_members:
|
|
324
|
+
console.print(f"\n[bold]Members ({len(members)}):[/bold]")
|
|
325
|
+
if members:
|
|
326
|
+
print_users_table(members, title="Group Members")
|
|
327
|
+
else:
|
|
328
|
+
console.print("[dim]No members in this group.[/dim]")
|
|
329
|
+
|
|
330
|
+
except httpx.HTTPStatusError as e:
|
|
331
|
+
print_api_error(e, "get user group")
|
|
332
|
+
raise typer.Exit(1)
|
|
333
|
+
except httpx.RequestError as e:
|
|
334
|
+
print_api_error(e, "get user group")
|
|
335
|
+
raise typer.Exit(1)
|
|
336
|
+
except Exception as e:
|
|
337
|
+
print_api_error(e, "get user group")
|
|
338
|
+
raise typer.Exit(1)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@app.command("group-members")
|
|
342
|
+
def list_group_members(
|
|
343
|
+
group_id: int = typer.Argument(..., help="User group ID"),
|
|
344
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
345
|
+
) -> None:
|
|
346
|
+
"""List members of a user group.
|
|
347
|
+
|
|
348
|
+
Examples:
|
|
349
|
+
bt pws users group-members 1 # Members of group 1
|
|
350
|
+
bt pws users group-members 1 -o json # JSON output
|
|
351
|
+
"""
|
|
352
|
+
try:
|
|
353
|
+
with get_client() as client:
|
|
354
|
+
client.authenticate()
|
|
355
|
+
group = client.get_user_group(group_id)
|
|
356
|
+
members = client.get_user_group_members(group_id)
|
|
357
|
+
|
|
358
|
+
group_name = group.get("Name", f"Group {group_id}")
|
|
359
|
+
|
|
360
|
+
if output == "json":
|
|
361
|
+
console.print_json(json.dumps(members, default=str))
|
|
362
|
+
else:
|
|
363
|
+
if members:
|
|
364
|
+
print_users_table(members, title=f"Members of '{group_name}'")
|
|
365
|
+
else:
|
|
366
|
+
console.print(f"[yellow]No members in group '{group_name}'.[/yellow]")
|
|
367
|
+
|
|
368
|
+
except httpx.HTTPStatusError as e:
|
|
369
|
+
print_api_error(e, "list group members")
|
|
370
|
+
raise typer.Exit(1)
|
|
371
|
+
except httpx.RequestError as e:
|
|
372
|
+
print_api_error(e, "list group members")
|
|
373
|
+
raise typer.Exit(1)
|
|
374
|
+
except Exception as e:
|
|
375
|
+
print_api_error(e, "list group members")
|
|
376
|
+
raise typer.Exit(1)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# =============================================================================
|
|
380
|
+
# Roles Commands
|
|
381
|
+
# =============================================================================
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@app.command("roles")
|
|
385
|
+
def list_roles(
|
|
386
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
387
|
+
) -> None:
|
|
388
|
+
"""List all available roles.
|
|
389
|
+
|
|
390
|
+
Examples:
|
|
391
|
+
bt pws users roles # All roles
|
|
392
|
+
bt pws users roles -o json # JSON output
|
|
393
|
+
"""
|
|
394
|
+
try:
|
|
395
|
+
with get_client() as client:
|
|
396
|
+
client.authenticate()
|
|
397
|
+
roles = client.list_roles()
|
|
398
|
+
|
|
399
|
+
if output == "json":
|
|
400
|
+
console.print_json(json.dumps(roles, default=str))
|
|
401
|
+
else:
|
|
402
|
+
if roles:
|
|
403
|
+
print_roles_table(roles)
|
|
404
|
+
else:
|
|
405
|
+
console.print("[yellow]No roles found.[/yellow]")
|
|
406
|
+
|
|
407
|
+
except httpx.HTTPStatusError as e:
|
|
408
|
+
print_api_error(e, "list roles")
|
|
409
|
+
raise typer.Exit(1)
|
|
410
|
+
except httpx.RequestError as e:
|
|
411
|
+
print_api_error(e, "list roles")
|
|
412
|
+
raise typer.Exit(1)
|
|
413
|
+
except Exception as e:
|
|
414
|
+
print_api_error(e, "list roles")
|
|
415
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""CLI commands for managing workgroups."""
|
|
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.table import Table
|
|
10
|
+
|
|
11
|
+
from ...core.output import print_api_error
|
|
12
|
+
from ..client.base import get_client
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(no_args_is_help=True, help="Manage workgroups in BeyondInsight")
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def print_workgroups_table(workgroups: list[dict], title: str = "Workgroups") -> None:
|
|
19
|
+
"""Print workgroups in a formatted table."""
|
|
20
|
+
table = Table(title=title)
|
|
21
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
22
|
+
table.add_column("Name", style="green")
|
|
23
|
+
table.add_column("Description", style="yellow")
|
|
24
|
+
table.add_column("Created", style="dim")
|
|
25
|
+
|
|
26
|
+
for wg in workgroups:
|
|
27
|
+
table.add_row(
|
|
28
|
+
str(wg.get("WorkgroupID", wg.get("ID", ""))),
|
|
29
|
+
wg.get("Name", ""),
|
|
30
|
+
wg.get("Description", "-"),
|
|
31
|
+
str(wg.get("CreatedDate", "-")),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
console.print(table)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.command("list")
|
|
38
|
+
def list_workgroups(
|
|
39
|
+
search: Optional[str] = typer.Option(None, "--search", "-s", help="Search filter"),
|
|
40
|
+
limit: int = typer.Option(50, "--limit", "-l", help="Maximum results (default: 50)"),
|
|
41
|
+
fetch_all: bool = typer.Option(False, "--all", help="Fetch all results (may be slow)"),
|
|
42
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
43
|
+
) -> None:
|
|
44
|
+
"""List all workgroups.
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
bt pws workgroups list # First 50 workgroups
|
|
48
|
+
bt pws workgroups list --all # All workgroups
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
with get_client() as client:
|
|
52
|
+
client.authenticate()
|
|
53
|
+
workgroups = client.list_workgroups(search=search, limit=None if fetch_all else limit)
|
|
54
|
+
|
|
55
|
+
if output == "json":
|
|
56
|
+
console.print_json(json.dumps(workgroups, default=str))
|
|
57
|
+
else:
|
|
58
|
+
if workgroups:
|
|
59
|
+
print_workgroups_table(workgroups)
|
|
60
|
+
if not fetch_all and len(workgroups) == limit:
|
|
61
|
+
console.print(f"[dim]Showing {len(workgroups)} results. Use --all to fetch all results.[/dim]")
|
|
62
|
+
else:
|
|
63
|
+
console.print("[yellow]No workgroups found.[/yellow]")
|
|
64
|
+
|
|
65
|
+
except httpx.HTTPStatusError as e:
|
|
66
|
+
print_api_error(e, "manage workgroups")
|
|
67
|
+
raise typer.Exit(1)
|
|
68
|
+
except httpx.RequestError as e:
|
|
69
|
+
print_api_error(e, "manage workgroups")
|
|
70
|
+
raise typer.Exit(1)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
print_api_error(e, "manage workgroups")
|
|
73
|
+
raise typer.Exit(1)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.command("get")
|
|
77
|
+
def get_workgroup(
|
|
78
|
+
workgroup_id: int = typer.Argument(..., help="Workgroup ID"),
|
|
79
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Get a workgroup by ID."""
|
|
82
|
+
try:
|
|
83
|
+
with get_client() as client:
|
|
84
|
+
client.authenticate()
|
|
85
|
+
workgroup = client.get_workgroup(workgroup_id)
|
|
86
|
+
|
|
87
|
+
if output == "json":
|
|
88
|
+
console.print_json(json.dumps(workgroup, default=str))
|
|
89
|
+
else:
|
|
90
|
+
console.print(f"\n[bold cyan]Workgroup: {workgroup.get('Name', 'Unknown')}[/bold cyan]\n")
|
|
91
|
+
console.print(f" ID: {workgroup.get('WorkgroupID', 'N/A')}")
|
|
92
|
+
console.print(f" Description: {workgroup.get('Description', '-')}")
|
|
93
|
+
console.print(f" Created: {workgroup.get('CreatedDate', '-')}")
|
|
94
|
+
|
|
95
|
+
except httpx.HTTPStatusError as e:
|
|
96
|
+
print_api_error(e, "manage workgroups")
|
|
97
|
+
raise typer.Exit(1)
|
|
98
|
+
except httpx.RequestError as e:
|
|
99
|
+
print_api_error(e, "manage workgroups")
|
|
100
|
+
raise typer.Exit(1)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
print_api_error(e, "manage workgroups")
|
|
103
|
+
raise typer.Exit(1)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@app.command("create")
|
|
107
|
+
def create_workgroup(
|
|
108
|
+
name: str = typer.Option(..., "--name", "-n", help="Workgroup name"),
|
|
109
|
+
description: Optional[str] = typer.Option(None, "--description", "-d", help="Description"),
|
|
110
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Create a new workgroup."""
|
|
113
|
+
try:
|
|
114
|
+
with get_client() as client:
|
|
115
|
+
client.authenticate()
|
|
116
|
+
workgroup = client.create_workgroup(name=name, description=description)
|
|
117
|
+
|
|
118
|
+
if output == "json":
|
|
119
|
+
console.print_json(json.dumps(workgroup, default=str))
|
|
120
|
+
else:
|
|
121
|
+
console.print(f"[green]Created workgroup:[/green] {workgroup.get('Name', 'Unknown')}")
|
|
122
|
+
console.print(f" ID: {workgroup.get('WorkgroupID', 'N/A')}")
|
|
123
|
+
|
|
124
|
+
except httpx.HTTPStatusError as e:
|
|
125
|
+
print_api_error(e, "manage workgroups")
|
|
126
|
+
raise typer.Exit(1)
|
|
127
|
+
except httpx.RequestError as e:
|
|
128
|
+
print_api_error(e, "manage workgroups")
|
|
129
|
+
raise typer.Exit(1)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
print_api_error(e, "manage workgroups")
|
|
132
|
+
raise typer.Exit(1)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@app.command("delete")
|
|
136
|
+
def delete_workgroup(
|
|
137
|
+
workgroup_id: int = typer.Argument(..., help="Workgroup ID to delete"),
|
|
138
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Delete a workgroup."""
|
|
141
|
+
try:
|
|
142
|
+
with get_client() as client:
|
|
143
|
+
client.authenticate()
|
|
144
|
+
|
|
145
|
+
if not force:
|
|
146
|
+
workgroup = client.get_workgroup(workgroup_id)
|
|
147
|
+
name = workgroup.get("Name", "Unknown")
|
|
148
|
+
confirm = typer.confirm(
|
|
149
|
+
f"Are you sure you want to delete workgroup '{name}' (ID: {workgroup_id})?"
|
|
150
|
+
)
|
|
151
|
+
if not confirm:
|
|
152
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
153
|
+
raise typer.Exit(0)
|
|
154
|
+
|
|
155
|
+
client.delete_workgroup(workgroup_id)
|
|
156
|
+
console.print(f"[green]Deleted workgroup ID: {workgroup_id}[/green]")
|
|
157
|
+
|
|
158
|
+
except httpx.HTTPStatusError as e:
|
|
159
|
+
print_api_error(e, "manage workgroups")
|
|
160
|
+
raise typer.Exit(1)
|
|
161
|
+
except httpx.RequestError as e:
|
|
162
|
+
print_api_error(e, "manage workgroups")
|
|
163
|
+
raise typer.Exit(1)
|
|
164
|
+
except Exception as e:
|
|
165
|
+
print_api_error(e, "manage workgroups")
|
|
166
|
+
raise typer.Exit(1)
|
bt_cli/pws/config.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Password Safe configuration wrapper.
|
|
2
|
+
|
|
3
|
+
Provides backward-compatible get_config() for command modules.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from ..core.config import PWSConfig, load_pws_config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_config() -> PWSConfig:
|
|
10
|
+
"""Get Password Safe configuration.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
PWSConfig instance loaded from environment
|
|
14
|
+
"""
|
|
15
|
+
return load_pws_config()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ["PWSConfig", "get_config", "load_pws_config"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Pydantic models for Password Safe API responses."""
|
|
2
|
+
|
|
3
|
+
from .common import (
|
|
4
|
+
BaseAPIModel,
|
|
5
|
+
EntityRef,
|
|
6
|
+
PaginatedResponse,
|
|
7
|
+
ApiError,
|
|
8
|
+
Timestamp,
|
|
9
|
+
normalize_api_response,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"BaseAPIModel",
|
|
14
|
+
"EntityRef",
|
|
15
|
+
"PaginatedResponse",
|
|
16
|
+
"ApiError",
|
|
17
|
+
"Timestamp",
|
|
18
|
+
"normalize_api_response",
|
|
19
|
+
]
|