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,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)
|