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.
Files changed (121) hide show
  1. bt_cli/__init__.py +3 -0
  2. bt_cli/cli.py +830 -0
  3. bt_cli/commands/__init__.py +1 -0
  4. bt_cli/commands/configure.py +415 -0
  5. bt_cli/commands/learn.py +229 -0
  6. bt_cli/commands/quick.py +784 -0
  7. bt_cli/core/__init__.py +1 -0
  8. bt_cli/core/auth.py +213 -0
  9. bt_cli/core/client.py +313 -0
  10. bt_cli/core/config.py +393 -0
  11. bt_cli/core/config_file.py +420 -0
  12. bt_cli/core/csv_utils.py +91 -0
  13. bt_cli/core/errors.py +247 -0
  14. bt_cli/core/output.py +205 -0
  15. bt_cli/core/prompts.py +87 -0
  16. bt_cli/core/rest_debug.py +221 -0
  17. bt_cli/data/CLAUDE.md +94 -0
  18. bt_cli/data/__init__.py +0 -0
  19. bt_cli/data/skills/bt/SKILL.md +108 -0
  20. bt_cli/data/skills/entitle/SKILL.md +170 -0
  21. bt_cli/data/skills/epmw/SKILL.md +144 -0
  22. bt_cli/data/skills/pra/SKILL.md +150 -0
  23. bt_cli/data/skills/pws/SKILL.md +198 -0
  24. bt_cli/entitle/__init__.py +1 -0
  25. bt_cli/entitle/client/__init__.py +5 -0
  26. bt_cli/entitle/client/base.py +443 -0
  27. bt_cli/entitle/commands/__init__.py +24 -0
  28. bt_cli/entitle/commands/accounts.py +53 -0
  29. bt_cli/entitle/commands/applications.py +39 -0
  30. bt_cli/entitle/commands/auth.py +68 -0
  31. bt_cli/entitle/commands/bundles.py +218 -0
  32. bt_cli/entitle/commands/integrations.py +60 -0
  33. bt_cli/entitle/commands/permissions.py +70 -0
  34. bt_cli/entitle/commands/policies.py +97 -0
  35. bt_cli/entitle/commands/resources.py +131 -0
  36. bt_cli/entitle/commands/roles.py +74 -0
  37. bt_cli/entitle/commands/users.py +123 -0
  38. bt_cli/entitle/commands/workflows.py +187 -0
  39. bt_cli/entitle/models/__init__.py +31 -0
  40. bt_cli/entitle/models/bundle.py +28 -0
  41. bt_cli/entitle/models/common.py +37 -0
  42. bt_cli/entitle/models/integration.py +30 -0
  43. bt_cli/entitle/models/permission.py +27 -0
  44. bt_cli/entitle/models/policy.py +25 -0
  45. bt_cli/entitle/models/resource.py +29 -0
  46. bt_cli/entitle/models/role.py +28 -0
  47. bt_cli/entitle/models/user.py +24 -0
  48. bt_cli/entitle/models/workflow.py +55 -0
  49. bt_cli/epmw/__init__.py +1 -0
  50. bt_cli/epmw/client/__init__.py +5 -0
  51. bt_cli/epmw/client/base.py +848 -0
  52. bt_cli/epmw/commands/__init__.py +33 -0
  53. bt_cli/epmw/commands/audits.py +250 -0
  54. bt_cli/epmw/commands/auth.py +55 -0
  55. bt_cli/epmw/commands/computers.py +140 -0
  56. bt_cli/epmw/commands/events.py +233 -0
  57. bt_cli/epmw/commands/groups.py +215 -0
  58. bt_cli/epmw/commands/policies.py +673 -0
  59. bt_cli/epmw/commands/quick.py +348 -0
  60. bt_cli/epmw/commands/requests.py +224 -0
  61. bt_cli/epmw/commands/roles.py +78 -0
  62. bt_cli/epmw/commands/tasks.py +38 -0
  63. bt_cli/epmw/commands/users.py +219 -0
  64. bt_cli/epmw/models/__init__.py +1 -0
  65. bt_cli/pra/__init__.py +1 -0
  66. bt_cli/pra/client/__init__.py +5 -0
  67. bt_cli/pra/client/base.py +618 -0
  68. bt_cli/pra/commands/__init__.py +30 -0
  69. bt_cli/pra/commands/auth.py +55 -0
  70. bt_cli/pra/commands/import_export.py +442 -0
  71. bt_cli/pra/commands/jump_clients.py +139 -0
  72. bt_cli/pra/commands/jump_groups.py +146 -0
  73. bt_cli/pra/commands/jump_items.py +638 -0
  74. bt_cli/pra/commands/jumpoints.py +95 -0
  75. bt_cli/pra/commands/policies.py +197 -0
  76. bt_cli/pra/commands/quick.py +470 -0
  77. bt_cli/pra/commands/teams.py +81 -0
  78. bt_cli/pra/commands/users.py +87 -0
  79. bt_cli/pra/commands/vault.py +564 -0
  80. bt_cli/pra/models/__init__.py +27 -0
  81. bt_cli/pra/models/common.py +12 -0
  82. bt_cli/pra/models/jump_client.py +25 -0
  83. bt_cli/pra/models/jump_group.py +15 -0
  84. bt_cli/pra/models/jump_item.py +72 -0
  85. bt_cli/pra/models/jumpoint.py +19 -0
  86. bt_cli/pra/models/team.py +14 -0
  87. bt_cli/pra/models/user.py +17 -0
  88. bt_cli/pra/models/vault.py +45 -0
  89. bt_cli/pws/__init__.py +1 -0
  90. bt_cli/pws/client/__init__.py +5 -0
  91. bt_cli/pws/client/base.py +356 -0
  92. bt_cli/pws/client/beyondinsight.py +869 -0
  93. bt_cli/pws/client/passwordsafe.py +1786 -0
  94. bt_cli/pws/commands/__init__.py +33 -0
  95. bt_cli/pws/commands/accounts.py +372 -0
  96. bt_cli/pws/commands/assets.py +311 -0
  97. bt_cli/pws/commands/auth.py +166 -0
  98. bt_cli/pws/commands/clouds.py +221 -0
  99. bt_cli/pws/commands/config.py +344 -0
  100. bt_cli/pws/commands/credentials.py +347 -0
  101. bt_cli/pws/commands/databases.py +306 -0
  102. bt_cli/pws/commands/directories.py +199 -0
  103. bt_cli/pws/commands/functional.py +298 -0
  104. bt_cli/pws/commands/import_export.py +452 -0
  105. bt_cli/pws/commands/platforms.py +118 -0
  106. bt_cli/pws/commands/quick.py +1646 -0
  107. bt_cli/pws/commands/search.py +256 -0
  108. bt_cli/pws/commands/secrets.py +1343 -0
  109. bt_cli/pws/commands/systems.py +389 -0
  110. bt_cli/pws/commands/users.py +415 -0
  111. bt_cli/pws/commands/workgroups.py +166 -0
  112. bt_cli/pws/config.py +18 -0
  113. bt_cli/pws/models/__init__.py +19 -0
  114. bt_cli/pws/models/account.py +186 -0
  115. bt_cli/pws/models/asset.py +102 -0
  116. bt_cli/pws/models/common.py +132 -0
  117. bt_cli/pws/models/system.py +121 -0
  118. bt_cli-0.4.13.dist-info/METADATA +417 -0
  119. bt_cli-0.4.13.dist-info/RECORD +121 -0
  120. bt_cli-0.4.13.dist-info/WHEEL +4 -0
  121. bt_cli-0.4.13.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,443 @@
1
+ """Entitle API client."""
2
+
3
+ import logging
4
+ from typing import Any, Optional
5
+
6
+ import httpx
7
+
8
+ from ...core.config import EntitleConfig, load_entitle_config
9
+ from ...core.auth import BearerTokenAuth
10
+ from ...core.rest_debug import get_event_hooks
11
+ from ...core.client import _warn_ssl_disabled
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class EntitleClient:
17
+ """HTTP client for BeyondTrust Entitle API.
18
+
19
+ Uses simple Bearer token authentication with API key.
20
+ """
21
+
22
+ def __init__(self, config: EntitleConfig):
23
+ """Initialize the Entitle client.
24
+
25
+ Args:
26
+ config: Configuration with API URL and API key
27
+ """
28
+ self.config = config
29
+ # Entitle API uses /public/v1 suffix
30
+ self.base_url = f"{config.api_url.rstrip('/')}/public/v1"
31
+ self._client: Optional[httpx.Client] = None
32
+ self._auth = BearerTokenAuth(config.api_key)
33
+
34
+ def __enter__(self) -> "EntitleClient":
35
+ """Context manager entry - create HTTP client."""
36
+ if not self.config.verify_ssl:
37
+ _warn_ssl_disabled()
38
+
39
+ self._client = httpx.Client(
40
+ base_url=self.base_url,
41
+ timeout=self.config.timeout,
42
+ verify=self.config.verify_ssl,
43
+ event_hooks=get_event_hooks(),
44
+ )
45
+ return self
46
+
47
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
48
+ """Context manager exit - close HTTP client."""
49
+ if self._client:
50
+ self._client.close()
51
+ self._client = None
52
+
53
+ def _ensure_client(self) -> httpx.Client:
54
+ """Ensure HTTP client is initialized."""
55
+ if self._client is None:
56
+ self._client = httpx.Client(
57
+ base_url=self.base_url,
58
+ timeout=self.config.timeout,
59
+ verify=self.config.verify_ssl,
60
+ event_hooks=get_event_hooks(),
61
+ )
62
+ return self._client
63
+
64
+ def _get_headers(self) -> dict[str, str]:
65
+ """Get request headers including auth.
66
+
67
+ Returns:
68
+ Headers dictionary
69
+ """
70
+ headers = {
71
+ "Content-Type": "application/json",
72
+ "Accept": "application/json",
73
+ }
74
+ headers.update(self._auth.get_headers())
75
+ return headers
76
+
77
+ def _request(
78
+ self,
79
+ method: str,
80
+ path: str,
81
+ params: Optional[dict[str, Any]] = None,
82
+ json: Optional[dict[str, Any]] = None,
83
+ ) -> Any:
84
+ """Make an HTTP request.
85
+
86
+ Args:
87
+ method: HTTP method
88
+ path: API endpoint path
89
+ params: Query parameters
90
+ json: JSON body
91
+
92
+ Returns:
93
+ Response data
94
+
95
+ Raises:
96
+ httpx.HTTPStatusError: If request fails
97
+ """
98
+ client = self._ensure_client()
99
+
100
+ # Filter out None params
101
+ if params:
102
+ params = {k: v for k, v in params.items() if v is not None}
103
+
104
+ response = client.request(
105
+ method=method,
106
+ url=path,
107
+ params=params,
108
+ json=json,
109
+ headers=self._get_headers(),
110
+ )
111
+ response.raise_for_status()
112
+
113
+ if response.status_code == 204 or not response.content:
114
+ return {}
115
+
116
+ return response.json()
117
+
118
+ def get(self, path: str, params: Optional[dict[str, Any]] = None) -> Any:
119
+ """Make a GET request."""
120
+ return self._request("GET", path, params=params)
121
+
122
+ def post(self, path: str, json: Optional[dict[str, Any]] = None) -> Any:
123
+ """Make a POST request."""
124
+ return self._request("POST", path, json=json)
125
+
126
+ def put(self, path: str, json: Optional[dict[str, Any]] = None) -> Any:
127
+ """Make a PUT request."""
128
+ return self._request("PUT", path, json=json)
129
+
130
+ def delete(self, path: str) -> Any:
131
+ """Make a DELETE request."""
132
+ return self._request("DELETE", path)
133
+
134
+ def paginate(
135
+ self,
136
+ path: str,
137
+ params: Optional[dict[str, Any]] = None,
138
+ page_size: int = 100,
139
+ max_pages: Optional[int] = None,
140
+ ) -> list[dict[str, Any]]:
141
+ """Paginate through all results from an endpoint.
142
+
143
+ Handles Entitle's page/perPage pagination style.
144
+
145
+ Args:
146
+ path: API endpoint path
147
+ params: Additional query parameters
148
+ page_size: Number of items per page
149
+ max_pages: Maximum pages to fetch (None for all)
150
+
151
+ Returns:
152
+ Complete list of all items
153
+ """
154
+ params = params or {}
155
+ params["perPage"] = page_size
156
+ page = 1
157
+ all_items: list[dict[str, Any]] = []
158
+
159
+ while True:
160
+ params["page"] = page
161
+ response = self.get(path, params)
162
+
163
+ # Handle different response formats
164
+ if isinstance(response, list):
165
+ items = response
166
+ total_pages = 1
167
+ elif "result" in response:
168
+ items = response["result"]
169
+ pagination = response.get("pagination", {})
170
+ total_pages = pagination.get("totalPages", 1)
171
+ elif "data" in response:
172
+ items = response["data"]
173
+ total_pages = response.get("totalPages", 1)
174
+ elif "items" in response:
175
+ items = response["items"]
176
+ total_pages = response.get("totalPages", 1)
177
+ else:
178
+ # Try to find a list in the response
179
+ items = []
180
+ for value in response.values():
181
+ if isinstance(value, list):
182
+ items = value
183
+ break
184
+ total_pages = 1
185
+
186
+ all_items.extend(items)
187
+ page += 1
188
+
189
+ if page > total_pages:
190
+ break
191
+ if max_pages and page > max_pages:
192
+ break
193
+
194
+ return all_items
195
+
196
+ # =========================================================================
197
+ # Integrations
198
+ # =========================================================================
199
+
200
+ def list_integrations(
201
+ self, search: Optional[str] = None, limit: int = 100
202
+ ) -> list[dict[str, Any]]:
203
+ """List all integrations."""
204
+ return self.paginate("/integrations", {"search": search}, page_size=limit)
205
+
206
+ def get_integration(self, integration_id: str) -> dict[str, Any]:
207
+ """Get an integration by ID."""
208
+ return self.get(f"/integrations/{integration_id}")
209
+
210
+ # =========================================================================
211
+ # Resources
212
+ # =========================================================================
213
+
214
+ def list_resources(
215
+ self,
216
+ integration_id: str,
217
+ search: Optional[str] = None,
218
+ limit: int = 100,
219
+ ) -> list[dict[str, Any]]:
220
+ """List resources for an integration.
221
+
222
+ Args:
223
+ integration_id: Integration ID (required by Entitle API)
224
+ search: Optional search filter
225
+ limit: Maximum results per page
226
+
227
+ Returns:
228
+ List of resources
229
+ """
230
+ params = {"integrationId": integration_id}
231
+ if search:
232
+ params["search"] = search
233
+ return self.paginate("/resources", params, page_size=limit)
234
+
235
+ def get_resource(self, resource_id: str) -> dict[str, Any]:
236
+ """Get a resource by ID."""
237
+ return self.get(f"/resources/{resource_id}")
238
+
239
+ def create_virtual_resource(
240
+ self,
241
+ integration_id: str,
242
+ name: str,
243
+ source_role_id: str,
244
+ role_name: str = "Start Session",
245
+ requestable: bool = True,
246
+ ) -> dict[str, Any]:
247
+ """Create a resource in a virtual integration.
248
+
249
+ Virtual integrations require at least one role that maps to a source
250
+ role from another integration.
251
+
252
+ Args:
253
+ integration_id: ID of the virtual integration
254
+ name: Name for the new resource
255
+ source_role_id: ID of the role from the source integration to link
256
+ role_name: Name for the role in the virtual integration
257
+ requestable: Whether the resource is requestable
258
+
259
+ Returns:
260
+ Created resource data
261
+ """
262
+ payload = {
263
+ "name": name,
264
+ "integration": {"id": integration_id},
265
+ "multirole": False,
266
+ "requestable": requestable,
267
+ "roles": [
268
+ {
269
+ "name": role_name,
270
+ "sourceRoleId": source_role_id,
271
+ }
272
+ ],
273
+ }
274
+ return self.post("/resources", json=payload)
275
+
276
+ def delete_resource(self, resource_id: str) -> dict[str, Any]:
277
+ """Delete a resource by ID."""
278
+ return self.delete(f"/resources/{resource_id}")
279
+
280
+ # =========================================================================
281
+ # Roles
282
+ # =========================================================================
283
+
284
+ def list_roles(
285
+ self,
286
+ resource_id: str,
287
+ search: Optional[str] = None,
288
+ limit: int = 100,
289
+ ) -> list[dict[str, Any]]:
290
+ """List roles for a resource.
291
+
292
+ Args:
293
+ resource_id: Resource ID (required by Entitle API)
294
+ search: Optional search filter
295
+ limit: Maximum results per page
296
+
297
+ Returns:
298
+ List of roles
299
+ """
300
+ params = {"resourceId": resource_id}
301
+ if search:
302
+ params["search"] = search
303
+ return self.paginate("/roles", params, page_size=limit)
304
+
305
+ def get_role(self, role_id: str) -> dict[str, Any]:
306
+ """Get a role by ID."""
307
+ return self.get(f"/roles/{role_id}")
308
+
309
+ # =========================================================================
310
+ # Bundles
311
+ # =========================================================================
312
+
313
+ def list_bundles(
314
+ self, search: Optional[str] = None, limit: int = 100
315
+ ) -> list[dict[str, Any]]:
316
+ """List all bundles."""
317
+ return self.paginate("/bundles", {"search": search}, page_size=limit)
318
+
319
+ def get_bundle(self, bundle_id: str) -> dict[str, Any]:
320
+ """Get a bundle by ID."""
321
+ return self.get(f"/bundles/{bundle_id}")
322
+
323
+ def create_bundle(self, data: dict[str, Any]) -> dict[str, Any]:
324
+ """Create a new bundle."""
325
+ return self.post("/bundles", json=data)
326
+
327
+ def update_bundle(self, bundle_id: str, data: dict[str, Any]) -> dict[str, Any]:
328
+ """Update a bundle."""
329
+ return self.put(f"/bundles/{bundle_id}", json=data)
330
+
331
+ def delete_bundle(self, bundle_id: str) -> dict[str, Any]:
332
+ """Delete a bundle."""
333
+ return self.delete(f"/bundles/{bundle_id}")
334
+
335
+ # =========================================================================
336
+ # Workflows
337
+ # =========================================================================
338
+
339
+ def list_workflows(
340
+ self, search: Optional[str] = None, limit: int = 100
341
+ ) -> list[dict[str, Any]]:
342
+ """List all workflows."""
343
+ return self.paginate("/workflows", {"search": search}, page_size=limit)
344
+
345
+ def get_workflow(self, workflow_id: str) -> dict[str, Any]:
346
+ """Get a workflow by ID."""
347
+ return self.get(f"/workflows/{workflow_id}")
348
+
349
+ # =========================================================================
350
+ # Users
351
+ # =========================================================================
352
+
353
+ def list_users(
354
+ self, search: Optional[str] = None, limit: int = 100
355
+ ) -> list[dict[str, Any]]:
356
+ """List all users."""
357
+ return self.paginate("/users", {"search": search}, page_size=limit)
358
+
359
+ def get_user(self, user_id: str) -> dict[str, Any]:
360
+ """Get a user by ID."""
361
+ return self.get(f"/users/{user_id}")
362
+
363
+ # =========================================================================
364
+ # Permissions
365
+ # =========================================================================
366
+
367
+ def list_permissions(
368
+ self,
369
+ user_id: Optional[str] = None,
370
+ integration_id: Optional[str] = None,
371
+ resource_id: Optional[str] = None,
372
+ limit: int = 100,
373
+ ) -> list[dict[str, Any]]:
374
+ """List permissions with optional filters."""
375
+ params = {
376
+ "userId": user_id,
377
+ "integrationId": integration_id,
378
+ "resourceId": resource_id,
379
+ }
380
+ return self.paginate("/permissions", params, page_size=limit)
381
+
382
+ def revoke_permission(self, permission_id: str) -> dict[str, Any]:
383
+ """Revoke a permission."""
384
+ return self.delete(f"/permissions/{permission_id}/revoke")
385
+
386
+ # =========================================================================
387
+ # Policies
388
+ # =========================================================================
389
+
390
+ def list_policies(self, limit: int = 100) -> list[dict[str, Any]]:
391
+ """List all policies."""
392
+ return self.paginate("/policies", page_size=limit)
393
+
394
+ def get_policy(self, policy_id: str) -> dict[str, Any]:
395
+ """Get a policy by ID."""
396
+ return self.get(f"/policies/{policy_id}")
397
+
398
+ # =========================================================================
399
+ # Applications
400
+ # =========================================================================
401
+
402
+ def list_applications(self, limit: int = 100) -> list[dict[str, Any]]:
403
+ """List available applications."""
404
+ return self.paginate("/applications", page_size=limit)
405
+
406
+ # =========================================================================
407
+ # Accounts
408
+ # =========================================================================
409
+
410
+ def list_accounts(
411
+ self,
412
+ integration_id: str,
413
+ search: Optional[str] = None,
414
+ limit: int = 100,
415
+ ) -> list[dict[str, Any]]:
416
+ """List accounts for an integration.
417
+
418
+ Accounts represent identities within an integration (e.g., AWS IAM users,
419
+ database accounts). Different from Entitle users.
420
+
421
+ Args:
422
+ integration_id: Integration ID (required by Entitle API)
423
+ search: Optional search filter
424
+ limit: Maximum results per page
425
+
426
+ Returns:
427
+ List of accounts
428
+ """
429
+ params = {"integrationId": integration_id}
430
+ if search:
431
+ params["search"] = search
432
+ return self.paginate("/accounts", params, page_size=limit)
433
+
434
+
435
+ # =========================================================================
436
+ def get_client() -> EntitleClient:
437
+ """Create a configured Entitle client.
438
+
439
+ Returns:
440
+ EntitleClient instance
441
+ """
442
+ config = load_entitle_config()
443
+ return EntitleClient(config)
@@ -0,0 +1,24 @@
1
+ """Entitle CLI commands."""
2
+
3
+ import typer
4
+
5
+ app = typer.Typer(
6
+ name="entitle",
7
+ help="Entitle management commands",
8
+ no_args_is_help=True,
9
+ )
10
+
11
+ # Import and register command groups
12
+ from . import auth, integrations, resources, roles, bundles
13
+ from . import workflows, users, permissions, policies, accounts
14
+
15
+ app.add_typer(auth.app, name="auth", help="Authentication commands")
16
+ app.add_typer(integrations.app, name="integrations", help="Manage integrations")
17
+ app.add_typer(resources.app, name="resources", help="Manage resources")
18
+ app.add_typer(roles.app, name="roles", help="Manage roles")
19
+ app.add_typer(bundles.app, name="bundles", help="Manage bundles")
20
+ app.add_typer(workflows.app, name="workflows", help="Manage workflows")
21
+ app.add_typer(users.app, name="users", help="Manage users")
22
+ app.add_typer(permissions.app, name="permissions", help="Manage permissions")
23
+ app.add_typer(policies.app, name="policies", help="Manage policies")
24
+ app.add_typer(accounts.app, name="accounts", help="Manage accounts")
@@ -0,0 +1,53 @@
1
+ """Account commands for Entitle."""
2
+
3
+ from typing import Optional
4
+
5
+ import httpx
6
+ import typer
7
+
8
+ from ..client.base import get_client
9
+ from ...core.output import console, print_table, print_json, print_error, print_api_error
10
+
11
+ app = typer.Typer(no_args_is_help=True, help="Manage accounts")
12
+
13
+
14
+ @app.command("list")
15
+ def list_accounts(
16
+ integration_id: str = typer.Option(..., "--integration", "-i", help="Integration ID (required)"),
17
+ search: Optional[str] = typer.Option(None, "--search", "-s", help="Search filter"),
18
+ limit: int = typer.Option(100, "--limit", "-l", help="Maximum results to return"),
19
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table, json"),
20
+ ) -> None:
21
+ """List accounts for an integration.
22
+
23
+ Accounts represent identities within an integration (e.g., AWS IAM users,
24
+ database accounts, service accounts). Different from Entitle users.
25
+
26
+ The Entitle API requires an integration ID to list accounts.
27
+ Use 'bt entitle integrations list' to find integration IDs.
28
+
29
+ Examples:
30
+ bt entitle accounts list -i <integration_id>
31
+ bt entitle accounts list -i <integration_id> -s "admin"
32
+ """
33
+ try:
34
+ with get_client() as client:
35
+ data = client.list_accounts(integration_id=integration_id, search=search, limit=limit)
36
+
37
+ if output == "json":
38
+ print_json(data)
39
+ else:
40
+ print_table(
41
+ data,
42
+ [("ID", "id"), ("Name", "name"), ("Email", "email")],
43
+ title="Accounts",
44
+ )
45
+ except httpx.HTTPStatusError as e:
46
+ print_api_error(e, "list accounts")
47
+ raise typer.Exit(1)
48
+ except httpx.RequestError as e:
49
+ print_api_error(e, "list accounts")
50
+ raise typer.Exit(1)
51
+ except Exception as e:
52
+ print_api_error(e, "list accounts")
53
+ raise typer.Exit(1)
@@ -0,0 +1,39 @@
1
+ """Application commands for Entitle."""
2
+
3
+ from typing import Optional
4
+
5
+ import httpx
6
+ import typer
7
+
8
+ from ..client.base import get_client
9
+ from ...core.output import console, print_table, print_json, print_error, print_api_error
10
+
11
+ app = typer.Typer(no_args_is_help=True, help="List available applications")
12
+
13
+
14
+ @app.command("list")
15
+ def list_applications(
16
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table, json"),
17
+ ) -> None:
18
+ """List all available applications."""
19
+ try:
20
+ with get_client() as client:
21
+ data = client.list_applications()
22
+
23
+ if output == "json":
24
+ print_json(data)
25
+ else:
26
+ print_table(
27
+ data,
28
+ [("ID", "id"), ("Name", "name")],
29
+ title="Applications",
30
+ )
31
+ except httpx.HTTPStatusError as e:
32
+ print_api_error(e, "list applications")
33
+ raise typer.Exit(1)
34
+ except httpx.RequestError as e:
35
+ print_api_error(e, "list applications")
36
+ raise typer.Exit(1)
37
+ except Exception as e:
38
+ print_api_error(e, "list applications")
39
+ raise typer.Exit(1)
@@ -0,0 +1,68 @@
1
+ """Authentication commands for Entitle."""
2
+
3
+ from typing import Optional
4
+
5
+ import httpx
6
+ import typer
7
+ from rich.console import Console
8
+
9
+ from ...core.output import print_error, print_api_error
10
+ from ..client.base import get_client
11
+ from ...core.config import load_entitle_config
12
+
13
+ app = typer.Typer(no_args_is_help=True, help="Authentication commands")
14
+ console = Console()
15
+
16
+
17
+ @app.command("test")
18
+ def test_auth() -> None:
19
+ """Test authentication with Entitle API.
20
+
21
+ Verifies your API key works by making a test API call.
22
+
23
+ Example:
24
+ bt entitle auth test
25
+ """
26
+ try:
27
+ config = load_entitle_config()
28
+ console.print(f"Testing connection to: {config.api_url}")
29
+
30
+ with get_client() as client:
31
+ # Test by fetching applications (lightweight call)
32
+ apps = client.list_applications(limit=1)
33
+ console.print("[green]Authentication successful![/green]")
34
+ console.print(f"API is responding ({len(apps)} application(s) found)")
35
+
36
+ except ValueError as e:
37
+ print_error(f"Configuration error: {e}")
38
+ raise typer.Exit(1)
39
+ except httpx.HTTPStatusError as e:
40
+ print_api_error(e, "test authentication")
41
+ raise typer.Exit(1)
42
+ except httpx.RequestError as e:
43
+ print_api_error(e, "test authentication")
44
+ raise typer.Exit(1)
45
+ except Exception as e:
46
+ print_api_error(e, "test authentication")
47
+ raise typer.Exit(1)
48
+
49
+
50
+ @app.command("status")
51
+ def auth_status() -> None:
52
+ """Show current authentication configuration.
53
+
54
+ Displays the configured API URL and key (masked).
55
+
56
+ Example:
57
+ bt entitle auth status
58
+ """
59
+ try:
60
+ config = load_entitle_config()
61
+ console.print(f"[dim]API URL:[/dim] {config.api_url}")
62
+ masked_key = config.api_key[:8] + "..." if len(config.api_key) > 8 else "***"
63
+ console.print(f"[dim]API Key:[/dim] {masked_key}")
64
+ console.print(f"[dim]Timeout:[/dim] {config.timeout}s")
65
+ console.print(f"[dim]SSL Verify:[/dim] {config.verify_ssl}")
66
+ except ValueError as e:
67
+ print_error(f"Configuration error: {e}")
68
+ raise typer.Exit(1)