lumos-identity-cli 0.0.0.dev1__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.
- lumos/__init__.py +4 -0
- lumos/__main__.py +9 -0
- lumos/cli.py +96 -0
- lumos/common/__init__.py +1 -0
- lumos/common/client.py +479 -0
- lumos/common/client_helpers.py +40 -0
- lumos/common/helpers.py +90 -0
- lumos/common/keyhelpers.py +41 -0
- lumos/common/logging.py +30 -0
- lumos/common/models.py +236 -0
- lumos/list_collections/__init__.py +1 -0
- lumos/list_collections/cli.py +289 -0
- lumos/request/__init__.py +1 -0
- lumos/request/cli.py +523 -0
- lumos_identity_cli-0.0.0.dev1.dist-info/METADATA +77 -0
- lumos_identity_cli-0.0.0.dev1.dist-info/RECORD +18 -0
- lumos_identity_cli-0.0.0.dev1.dist-info/WHEEL +4 -0
- lumos_identity_cli-0.0.0.dev1.dist-info/entry_points.txt +2 -0
lumos/request/cli.py
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import time
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from click_extra import Context, confirm, echo, group, option, pass_context, prompt
|
|
6
|
+
from pick import pick
|
|
7
|
+
from tabulate import tabulate
|
|
8
|
+
|
|
9
|
+
from lumos.common.client import ApiClient
|
|
10
|
+
from lumos.common.helpers import authenticate
|
|
11
|
+
from lumos.common.models import (
|
|
12
|
+
AccessRequest,
|
|
13
|
+
App,
|
|
14
|
+
Permission,
|
|
15
|
+
ProvisioningMethodOption,
|
|
16
|
+
SupportRequestStatus,
|
|
17
|
+
User,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
POLLING_INTERVAL = 6
|
|
21
|
+
client = ApiClient()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@group(name="request", invoke_without_command=True)
|
|
25
|
+
@pass_context
|
|
26
|
+
@option("--reason", default=None, help="Business justification for request")
|
|
27
|
+
@option(
|
|
28
|
+
"--for-user",
|
|
29
|
+
default=None,
|
|
30
|
+
type=str,
|
|
31
|
+
help="UUID of user for whom to request access. Takes precedence over --user-like",
|
|
32
|
+
)
|
|
33
|
+
@option("--for-me", is_flag=True, help="Makes the request for the current user.")
|
|
34
|
+
@option(
|
|
35
|
+
"--mine",
|
|
36
|
+
is_flag=True,
|
|
37
|
+
help="Makes the request for the current user. Duplicate of --for-me for convenience.",
|
|
38
|
+
)
|
|
39
|
+
@option("--app", default=None, type=str, help="App UUID. Takes precedence over --app-like")
|
|
40
|
+
@option(
|
|
41
|
+
"--permission",
|
|
42
|
+
multiple=True,
|
|
43
|
+
type=str,
|
|
44
|
+
help="List of permission UUIDs. Takes precedence over --permission-like",
|
|
45
|
+
)
|
|
46
|
+
@option(
|
|
47
|
+
"--length",
|
|
48
|
+
default=None,
|
|
49
|
+
help="Length of access request in seconds, or a string like '12 hours', '2d', 'unlimited', etc. Every app/permission has different configurations.",
|
|
50
|
+
)
|
|
51
|
+
@option(
|
|
52
|
+
"--user-like",
|
|
53
|
+
default=None,
|
|
54
|
+
help="User name/email like--filters users shown as options when selecting, if the request is not for the current user",
|
|
55
|
+
)
|
|
56
|
+
@option(
|
|
57
|
+
"--app-like",
|
|
58
|
+
default=None,
|
|
59
|
+
help="App name like--filters apps shown as options when selecting",
|
|
60
|
+
)
|
|
61
|
+
@option(
|
|
62
|
+
"--permission-like",
|
|
63
|
+
default=None,
|
|
64
|
+
help="Permission name like--filters permissions shown as options when selecting",
|
|
65
|
+
)
|
|
66
|
+
@option("--wait/--no-wait", default=None, help="Wait for the request to complete")
|
|
67
|
+
@option(
|
|
68
|
+
"--dry-run",
|
|
69
|
+
is_flag=True,
|
|
70
|
+
help="Print the request command without actually making the request",
|
|
71
|
+
)
|
|
72
|
+
@authenticate
|
|
73
|
+
def request(
|
|
74
|
+
ctx: Context,
|
|
75
|
+
reason: str | None,
|
|
76
|
+
for_user: str | None,
|
|
77
|
+
for_me: bool,
|
|
78
|
+
mine: bool,
|
|
79
|
+
app: str | None,
|
|
80
|
+
permission: tuple,
|
|
81
|
+
length: str | None,
|
|
82
|
+
user_like: str | None,
|
|
83
|
+
app_like: str | None,
|
|
84
|
+
permission_like: str | None,
|
|
85
|
+
wait: bool | None,
|
|
86
|
+
dry_run: bool,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Request access to an app."""
|
|
89
|
+
if ctx.invoked_subcommand is None:
|
|
90
|
+
for_user_uuid = None
|
|
91
|
+
if for_me is not True and mine is not True:
|
|
92
|
+
if user_like is not None or not confirm("This request is for you?", default=True):
|
|
93
|
+
for_user_uuid = select_user(user_like)
|
|
94
|
+
elif for_user:
|
|
95
|
+
for_user_uuid = UUID(for_user)
|
|
96
|
+
|
|
97
|
+
# Validate parameters or interactively input them
|
|
98
|
+
app_uuid = UUID(app) if app else None
|
|
99
|
+
selected_app: App = get_valid_app(app_uuid, app_like)
|
|
100
|
+
app_settings = client.get_appstore_app_setting(selected_app.id)
|
|
101
|
+
duration_options = set(app_settings.provisioning.time_based_access)
|
|
102
|
+
selected_permissions = None
|
|
103
|
+
|
|
104
|
+
permission_uuids = [UUID(p) for p in permission] if permission else None
|
|
105
|
+
|
|
106
|
+
if app_settings.provisioning.groups_provisioning == ProvisioningMethodOption.GROUPS_AND_VISIBLE:
|
|
107
|
+
selected_permissions = select_permissions(
|
|
108
|
+
selected_app,
|
|
109
|
+
app_settings.provisioning.allow_multiple_permission_selection,
|
|
110
|
+
permission_uuids,
|
|
111
|
+
permission_like,
|
|
112
|
+
)
|
|
113
|
+
if selected_permissions:
|
|
114
|
+
duration_options = set(selected_permissions[0].duration_options)
|
|
115
|
+
for perm in selected_permissions[1:]:
|
|
116
|
+
duration_options = duration_options.intersection(perm.duration_options)
|
|
117
|
+
|
|
118
|
+
duration, duration_friendly = get_duration(duration_options, length)
|
|
119
|
+
|
|
120
|
+
while not reason or len(reason) < 1:
|
|
121
|
+
reason = prompt("\nEnter your business justification for the request")
|
|
122
|
+
|
|
123
|
+
if wait is None:
|
|
124
|
+
wait = confirm("Do you want to wait for the request to complete?", default=True)
|
|
125
|
+
|
|
126
|
+
echo("\nAPP")
|
|
127
|
+
echo(f" {selected_app.user_friendly_label} [{selected_app.id}]")
|
|
128
|
+
if selected_permissions:
|
|
129
|
+
echo("\nPERMISSIONS")
|
|
130
|
+
for perm in selected_permissions:
|
|
131
|
+
echo(f" {perm.label} [{perm.id}]")
|
|
132
|
+
echo("\nDURATION")
|
|
133
|
+
echo(f" {duration_friendly or 'Unlimited'} {f'({duration} seconds)' if duration else ''}")
|
|
134
|
+
echo("\nREASON")
|
|
135
|
+
echo(f" {reason}")
|
|
136
|
+
|
|
137
|
+
if for_user_uuid:
|
|
138
|
+
echo("\nTARGET USER")
|
|
139
|
+
echo(f" {for_user_uuid}")
|
|
140
|
+
|
|
141
|
+
permission_flags = ""
|
|
142
|
+
if selected_permissions:
|
|
143
|
+
permission_flags = " ".join([f"--permission {perm.id}" for perm in selected_permissions])
|
|
144
|
+
|
|
145
|
+
command_str = f'lumos request --app {selected_app.id} {permission_flags} --reason "{reason}"'
|
|
146
|
+
if duration:
|
|
147
|
+
command_str += f" --length {duration}"
|
|
148
|
+
for_user_flag = "--for-me"
|
|
149
|
+
if for_user_uuid:
|
|
150
|
+
for_user_flag = "--for-user USER_ID"
|
|
151
|
+
command_str += f" {for_user_flag}"
|
|
152
|
+
if wait:
|
|
153
|
+
command_str += " --wait"
|
|
154
|
+
else:
|
|
155
|
+
command_str += " --no-wait"
|
|
156
|
+
if dry_run:
|
|
157
|
+
echo("\nCOMMAND")
|
|
158
|
+
echo(f" {command_str}")
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
echo("\nIf you need to make this same request in the future, use:")
|
|
162
|
+
echo(f"\n `{command_str}`\n")
|
|
163
|
+
|
|
164
|
+
request_id = create_access_request(
|
|
165
|
+
app_id=selected_app.id,
|
|
166
|
+
requestable_permission_ids=([p.id for p in selected_permissions] if selected_permissions else None),
|
|
167
|
+
note=reason,
|
|
168
|
+
expiration=duration,
|
|
169
|
+
target_user_id=for_user_uuid,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if wait and request_id:
|
|
173
|
+
_poll(request_id)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@request.command(
|
|
177
|
+
"status",
|
|
178
|
+
help="Check the status of a request by ID or `--last` for the most recent request",
|
|
179
|
+
)
|
|
180
|
+
@option("--request-id", default=None, help="Request ID")
|
|
181
|
+
@option("--last", is_flag=True, help="Get the last request")
|
|
182
|
+
@option("--status-only", is_flag=True, help="Output status only")
|
|
183
|
+
@option("--permission-only", is_flag=True, help="Output permission only")
|
|
184
|
+
@option("--id-only", is_flag=True, help="Output request ID only")
|
|
185
|
+
@authenticate
|
|
186
|
+
def status(
|
|
187
|
+
request_id: str | None,
|
|
188
|
+
last: bool,
|
|
189
|
+
status_only: bool,
|
|
190
|
+
permission_only: bool,
|
|
191
|
+
id_only: bool,
|
|
192
|
+
) -> None:
|
|
193
|
+
access_request: AccessRequest | None
|
|
194
|
+
if last:
|
|
195
|
+
access_requests, count, _, _, _ = client.get_access_requests(target_user_id=client.get_current_user_id())
|
|
196
|
+
if count == 0:
|
|
197
|
+
echo("No pending requests found")
|
|
198
|
+
return
|
|
199
|
+
access_request = access_requests[0]
|
|
200
|
+
else:
|
|
201
|
+
request_uuid: UUID | None = None
|
|
202
|
+
while not request_uuid:
|
|
203
|
+
try:
|
|
204
|
+
request_uuid = UUID(request_id)
|
|
205
|
+
break
|
|
206
|
+
except ValueError:
|
|
207
|
+
if request_id:
|
|
208
|
+
echo("Invalid request ID")
|
|
209
|
+
request_id = None
|
|
210
|
+
request_id = prompt("Please provide a request ID")
|
|
211
|
+
|
|
212
|
+
access_request = client.get_request_status(request_uuid)
|
|
213
|
+
if not access_request:
|
|
214
|
+
echo("Request not found")
|
|
215
|
+
return
|
|
216
|
+
if status_only:
|
|
217
|
+
echo(access_request.status)
|
|
218
|
+
return
|
|
219
|
+
if permission_only:
|
|
220
|
+
if access_request.requestable_permissions:
|
|
221
|
+
for perm in access_request.requestable_permissions:
|
|
222
|
+
echo(perm.label)
|
|
223
|
+
return
|
|
224
|
+
if last and id_only:
|
|
225
|
+
echo(access_request.status)
|
|
226
|
+
return
|
|
227
|
+
print(tabulate([access_request.tabulate()], headers=AccessRequest.headers()), "\n")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@request.command("poll", help="Poll a request by ID for up to 5 minutes")
|
|
231
|
+
@option("--request-id", default=None, type=str, help="Request ID")
|
|
232
|
+
@option("--wait", default=2, help="How many minutes to wait. Max 5.")
|
|
233
|
+
@authenticate
|
|
234
|
+
def poll(request_id: str | None, wait: int) -> None:
|
|
235
|
+
request_uuid = UUID(request_id) if request_id else None
|
|
236
|
+
while not request_uuid:
|
|
237
|
+
request_id_str = prompt("Please provide a request ID")
|
|
238
|
+
request_uuid = UUID(request_id_str)
|
|
239
|
+
|
|
240
|
+
_poll(request_uuid, (wait or 2) * 60)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _poll(request_id: UUID, wait_max: int = 120):
|
|
244
|
+
if wait_max < 10 or wait_max > 300:
|
|
245
|
+
wait_max = 120
|
|
246
|
+
while (access_request := client.get_request_status(request_id)) is not None:
|
|
247
|
+
if access_request.status not in SupportRequestStatus.PENDING_STATUSES or wait_max <= 0:
|
|
248
|
+
break
|
|
249
|
+
wait_max -= POLLING_INTERVAL
|
|
250
|
+
for num_decimals in range(POLLING_INTERVAL):
|
|
251
|
+
time.sleep(1)
|
|
252
|
+
print(
|
|
253
|
+
" ⏰ Waiting for request to complete"
|
|
254
|
+
+ ("." * num_decimals)
|
|
255
|
+
+ (" " * (POLLING_INTERVAL - num_decimals)),
|
|
256
|
+
end="\r",
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if not access_request:
|
|
260
|
+
echo("Request not found")
|
|
261
|
+
raise SystemExit(1)
|
|
262
|
+
|
|
263
|
+
if access_request.status == SupportRequestStatus.COMPLETED:
|
|
264
|
+
echo(" ✅ Request completed!")
|
|
265
|
+
return
|
|
266
|
+
echo(f" ⏰ Request status: {access_request.status}" + (" " * 20) + "\n")
|
|
267
|
+
echo(f"Use `lumos request status --request-id {request_id}` to check the status later.")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@request.command("cancel", help="Cancel a request by ID")
|
|
271
|
+
@option("--request-id", default=None, type=str, help="Request ID")
|
|
272
|
+
@option("--reason", default=None, help="Reason for cancellation")
|
|
273
|
+
@authenticate
|
|
274
|
+
def cancel(request_id: str | None, reason: str | None) -> None:
|
|
275
|
+
request_uuid = None
|
|
276
|
+
while not request_uuid:
|
|
277
|
+
if not request_id:
|
|
278
|
+
request_id = prompt(
|
|
279
|
+
"Please provide a request ID or press enter to look up a request",
|
|
280
|
+
default="",
|
|
281
|
+
show_default=False,
|
|
282
|
+
)
|
|
283
|
+
if not request_id:
|
|
284
|
+
pending_requests, _, _, _, _ = client.get_access_requests(
|
|
285
|
+
client.get_current_user_id(),
|
|
286
|
+
SupportRequestStatus.PENDING_STATUSES,
|
|
287
|
+
all=True,
|
|
288
|
+
)
|
|
289
|
+
if len(pending_requests) == 0:
|
|
290
|
+
echo("No pending requests found")
|
|
291
|
+
raise SystemExit(1)
|
|
292
|
+
access_request, _ = pick(pending_requests, "Select a request to cancel")
|
|
293
|
+
request_uuid = access_request.id
|
|
294
|
+
else:
|
|
295
|
+
request_uuid = UUID(request_id)
|
|
296
|
+
|
|
297
|
+
if (
|
|
298
|
+
access_request := client.get_request_status(request_uuid)
|
|
299
|
+
) is not None and access_request.status not in SupportRequestStatus.PENDING_STATUSES:
|
|
300
|
+
echo(f"Request is not pending, cannot cancel. Status is {access_request.status}.")
|
|
301
|
+
raise SystemExit(1)
|
|
302
|
+
|
|
303
|
+
client.cancel_access_request(request_uuid, reason)
|
|
304
|
+
echo("Request cancelled! 🚫")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def select_user(user_like: str | None = None) -> UUID:
|
|
308
|
+
users: list[User] = []
|
|
309
|
+
for_user: User | None = None
|
|
310
|
+
while for_user is None:
|
|
311
|
+
while True:
|
|
312
|
+
users, count, total = client.get_users(like=user_like)
|
|
313
|
+
if total == 0:
|
|
314
|
+
if user_like:
|
|
315
|
+
echo(f"No users found for '{user_like}'")
|
|
316
|
+
user_like = prompt("🔍 Give me something to search on")
|
|
317
|
+
else:
|
|
318
|
+
echo("No users found")
|
|
319
|
+
raise SystemExit(1)
|
|
320
|
+
elif count == 1:
|
|
321
|
+
for_user = users[0]
|
|
322
|
+
break
|
|
323
|
+
elif count < total:
|
|
324
|
+
echo("Too many users to show.")
|
|
325
|
+
user_like = prompt("🔍 Give me something to search on")
|
|
326
|
+
else:
|
|
327
|
+
break
|
|
328
|
+
if not for_user:
|
|
329
|
+
description = "Select user (use ENTER to confirm)"
|
|
330
|
+
for_user, _ = pick(users, description)
|
|
331
|
+
echo(f"USER: {for_user.email} [{for_user.id}]\n")
|
|
332
|
+
return for_user.id
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def get_valid_app(app_id: UUID | None = None, app_like: str | None = None) -> App:
|
|
336
|
+
app = None
|
|
337
|
+
while not app_id or not (app := client.get_appstore_app(app_id)):
|
|
338
|
+
apps: list[App] = []
|
|
339
|
+
while True:
|
|
340
|
+
print("\n⏳ Loading your apps ...", end="\r")
|
|
341
|
+
apps, count, total = client.get_appstore_apps(name_search=app_like, page_size=25)
|
|
342
|
+
print(" ", end="\r")
|
|
343
|
+
if total == 0:
|
|
344
|
+
if app_like:
|
|
345
|
+
app_like = prompt(f"No apps found for '{app_like}'\n🔍 Give me something to search on")
|
|
346
|
+
else:
|
|
347
|
+
echo("No apps found")
|
|
348
|
+
raise SystemExit(1)
|
|
349
|
+
elif count < total:
|
|
350
|
+
echo("")
|
|
351
|
+
app_like = prompt("Too many apps to show.\n🔍 Give me something to search on")
|
|
352
|
+
else:
|
|
353
|
+
break
|
|
354
|
+
if count == 1:
|
|
355
|
+
app = apps[0]
|
|
356
|
+
else:
|
|
357
|
+
app, _ = pick(apps, "Select an app (press ENTER to confirm)")
|
|
358
|
+
if app:
|
|
359
|
+
app_id = app.id
|
|
360
|
+
echo(f"APP: {app.user_friendly_label} [{app.id}]\n")
|
|
361
|
+
return app
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def select_permissions(
|
|
365
|
+
app: App,
|
|
366
|
+
allow_multiple_permission_selection: bool,
|
|
367
|
+
permission_ids: list[UUID] | None,
|
|
368
|
+
permission_like: str | None = None,
|
|
369
|
+
) -> list[Permission] | None:
|
|
370
|
+
valid_permissions = get_valid_permissions(app, permission_ids, allow_multiple_permission_selection)
|
|
371
|
+
if len(valid_permissions) > 0:
|
|
372
|
+
return valid_permissions
|
|
373
|
+
done_selecting = False
|
|
374
|
+
valid_permissions_dict: dict[str, Permission] = {}
|
|
375
|
+
while not done_selecting:
|
|
376
|
+
permissions: list[Permission] = []
|
|
377
|
+
while True:
|
|
378
|
+
print("\n⏳ Loading permissions for app ...", end="\r")
|
|
379
|
+
permissions, count, total = client.get_app_requestable_permissions(
|
|
380
|
+
app_id=app.id, search_term=permission_like, page_size=25
|
|
381
|
+
)
|
|
382
|
+
print(" ", end="\r")
|
|
383
|
+
if total == 0:
|
|
384
|
+
if permission_like:
|
|
385
|
+
permission_like = prompt(
|
|
386
|
+
f"No permissions found for '{permission_like}'\n🔍 Give me something to search on"
|
|
387
|
+
)
|
|
388
|
+
else:
|
|
389
|
+
echo("No permissions found (you're just requesting the app)")
|
|
390
|
+
return None
|
|
391
|
+
elif count < total:
|
|
392
|
+
permission_like = prompt("Too many permissions to show.\n🔍 Give me something to search on")
|
|
393
|
+
else:
|
|
394
|
+
break
|
|
395
|
+
if count > 1:
|
|
396
|
+
already_selected = ", ".join([p.label for p in valid_permissions_dict.values()])
|
|
397
|
+
if allow_multiple_permission_selection:
|
|
398
|
+
description = f"Select at least one permissions\n{f'(already selected: {already_selected})' if already_selected else ''}\nUse SPACE or right arrow to select, ENTER to confirm"
|
|
399
|
+
selected = pick(permissions, description, multiselect=True, min_selection_count=1) # type: ignore[misc]
|
|
400
|
+
for option, _ in selected:
|
|
401
|
+
valid_permissions_dict[str(option.id)] = option
|
|
402
|
+
else:
|
|
403
|
+
option, _ = pick(permissions, "Select permission (use ENTER to confirm)")
|
|
404
|
+
valid_permissions_dict[str(option.id)] = option
|
|
405
|
+
elif count == 1:
|
|
406
|
+
permission = permissions[0]
|
|
407
|
+
valid_permissions_dict[str(permission.id)] = permission
|
|
408
|
+
permission_like = None
|
|
409
|
+
echo("PERMISSIONS: ")
|
|
410
|
+
for permission in valid_permissions_dict.values():
|
|
411
|
+
echo(f" {permission.label} [{permission.id}]")
|
|
412
|
+
if not permission_like or not allow_multiple_permission_selection:
|
|
413
|
+
break
|
|
414
|
+
if confirm("Done selecting permissions?", default=True):
|
|
415
|
+
done_selecting = True
|
|
416
|
+
else:
|
|
417
|
+
permission_like = prompt("🔍 Give me something to search on")
|
|
418
|
+
return list(valid_permissions_dict.values())
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def get_valid_permissions(
|
|
422
|
+
app: App,
|
|
423
|
+
permission_ids: list[UUID] | None,
|
|
424
|
+
allow_multiple_permission_selection: bool,
|
|
425
|
+
) -> list[Permission]:
|
|
426
|
+
valid_permissions: list[Permission] = []
|
|
427
|
+
if not permission_ids:
|
|
428
|
+
return []
|
|
429
|
+
if len(permission_ids) > 1 and not allow_multiple_permission_selection:
|
|
430
|
+
return []
|
|
431
|
+
for permission_id in permission_ids:
|
|
432
|
+
if not (permission := client.get_app_requestable_permission(permission_id)):
|
|
433
|
+
return []
|
|
434
|
+
if not permission.app_id.__eq__(app.id):
|
|
435
|
+
return []
|
|
436
|
+
valid_permissions.append(permission)
|
|
437
|
+
return valid_permissions
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def select_duration(durations: set[str], duration_friendly: str | None) -> tuple[int | None, str]:
|
|
441
|
+
selected: str = ""
|
|
442
|
+
if len(durations) == 1:
|
|
443
|
+
selected = next(iter(durations))
|
|
444
|
+
elif duration_friendly in durations:
|
|
445
|
+
selected = duration_friendly
|
|
446
|
+
elif len(durations) > 1:
|
|
447
|
+
selected, _ = pick(
|
|
448
|
+
list(durations),
|
|
449
|
+
"Select duration (use ENTER to confirm)",
|
|
450
|
+
multiselect=False,
|
|
451
|
+
min_selection_count=1,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
time_in_seconds = None
|
|
455
|
+
if match := re.match(r"(\d+) ", selected):
|
|
456
|
+
time_in_seconds = int(match.group(1)) * 60 * 60
|
|
457
|
+
if re.match(r".*days", selected):
|
|
458
|
+
time_in_seconds = 24 * time_in_seconds
|
|
459
|
+
echo(f"DURATION: {selected}{f' ({time_in_seconds} seconds)' if time_in_seconds else ''}")
|
|
460
|
+
return time_in_seconds, selected
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def parse_duration(duration: str) -> tuple[int | None, str]:
|
|
464
|
+
time_in_seconds = None
|
|
465
|
+
if match := re.match(r"(\d+) ", duration):
|
|
466
|
+
time_in_seconds = int(match.group(1)) * 60 * 60
|
|
467
|
+
if re.match(r".* d", duration):
|
|
468
|
+
time_in_seconds = 24 * time_in_seconds
|
|
469
|
+
return time_in_seconds, duration.replace(" ", "").lower()
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def get_duration(possible_durations: set[str], input_length: str | None) -> tuple[int | None, str]:
|
|
473
|
+
duration_friendly: str | None = None
|
|
474
|
+
durations = {}
|
|
475
|
+
for possible_duration in possible_durations:
|
|
476
|
+
length, key = parse_duration(possible_duration)
|
|
477
|
+
|
|
478
|
+
durations[key] = {
|
|
479
|
+
"label": possible_duration,
|
|
480
|
+
"length": length,
|
|
481
|
+
}
|
|
482
|
+
if input_length:
|
|
483
|
+
try:
|
|
484
|
+
duration = int(input_length)
|
|
485
|
+
duration_friendly = None
|
|
486
|
+
for _key, v in durations.items():
|
|
487
|
+
if v["length"] == duration:
|
|
488
|
+
duration_friendly = v["label"]
|
|
489
|
+
break
|
|
490
|
+
except ValueError:
|
|
491
|
+
for key, v in durations.items():
|
|
492
|
+
if key.startswith(input_length.replace(" ", "").lower()):
|
|
493
|
+
duration_friendly = v["label"]
|
|
494
|
+
break
|
|
495
|
+
|
|
496
|
+
duration, duration_friendly = select_duration(possible_durations, duration_friendly)
|
|
497
|
+
return duration, duration_friendly
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def create_access_request(
|
|
501
|
+
app_id: UUID,
|
|
502
|
+
requestable_permission_ids: list[UUID] | None,
|
|
503
|
+
note: str,
|
|
504
|
+
expiration: int | None = None,
|
|
505
|
+
target_user_id: UUID | None = None,
|
|
506
|
+
) -> UUID | None:
|
|
507
|
+
response = client.create_access_request(
|
|
508
|
+
app_id=app_id,
|
|
509
|
+
permission_ids=requestable_permission_ids,
|
|
510
|
+
note=note,
|
|
511
|
+
expiration_in_seconds=expiration,
|
|
512
|
+
target_user_id=target_user_id,
|
|
513
|
+
)
|
|
514
|
+
if not response:
|
|
515
|
+
return None
|
|
516
|
+
|
|
517
|
+
echo(f"\nYour request (ID {response.id}) is in progress! 🏃🌴\n")
|
|
518
|
+
|
|
519
|
+
return response.id
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
if __name__ == "__main__":
|
|
523
|
+
request()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lumos-identity-cli
|
|
3
|
+
Version: 0.0.0.dev1
|
|
4
|
+
Summary: Lumos command line interface.
|
|
5
|
+
Project-URL: Homepage, https://github.com/teamlumos/lumos-cli
|
|
6
|
+
Project-URL: Repository, https://github.com/teamlumos/lumos-cli
|
|
7
|
+
Project-URL: Issues, https://github.com/teamlumos/lumos-cli/issues
|
|
8
|
+
Author: lumos-dev
|
|
9
|
+
License: MIT
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: System Administrators
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: <4,>=3.10.6
|
|
17
|
+
Requires-Dist: click-extra<8,>=7.4.0
|
|
18
|
+
Requires-Dist: pick<3,>=2.2.0
|
|
19
|
+
Requires-Dist: pydantic<3,>=2.6.0
|
|
20
|
+
Requires-Dist: pytz>=2024.1
|
|
21
|
+
Requires-Dist: requests<3,>=2.31.0
|
|
22
|
+
Requires-Dist: tabulate<1,>=0.9.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Lumos CLI
|
|
26
|
+
|
|
27
|
+
A command-line interface for the Lumos platform. Manage access requests, list resources, and automate workflows.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
### Homebrew (macOS and Linux)
|
|
32
|
+
|
|
33
|
+
```shell
|
|
34
|
+
brew install teamlumos/tap/lumos
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### PyPI (Python environments)
|
|
38
|
+
|
|
39
|
+
```shell
|
|
40
|
+
pip install lumos-identity-cli
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Other Methods
|
|
44
|
+
|
|
45
|
+
See [Installation Guide](docs/installation.md) for pip, uv, and binary installation options.
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
```shell
|
|
50
|
+
# Setup authentication
|
|
51
|
+
lumos setup
|
|
52
|
+
|
|
53
|
+
# Check current user
|
|
54
|
+
lumos whoami
|
|
55
|
+
|
|
56
|
+
# List available apps
|
|
57
|
+
lumos list apps --like github
|
|
58
|
+
|
|
59
|
+
# Request access
|
|
60
|
+
lumos request
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Documentation
|
|
64
|
+
|
|
65
|
+
- **[Installation](docs/installation.md)** - Detailed installation instructions for all platforms
|
|
66
|
+
- **[Examples](docs/examples.md)** - Usage examples with code samples
|
|
67
|
+
- **[CLI Reference](docs/reference.md)** - Complete command reference
|
|
68
|
+
|
|
69
|
+
Online documentation: [teamlumos.github.io/lumos-cli](https://teamlumos.github.io/lumos-cli/)
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
lumos/__init__.py,sha256=EUuV3XMjE8eHwrdlQ3wm0sTS-t9FjmqYvHThOlIaQQw,113
|
|
2
|
+
lumos/__main__.py,sha256=ZoCjA4DBKJmVL6eNeaWMjeoxWH4f7V6DcroWJIIt4bw,94
|
|
3
|
+
lumos/cli.py,sha256=CgxOotePGsrZKTSGyGfz-O8nNPnWkVOc4Mwdn642sv0,2748
|
|
4
|
+
lumos/common/__init__.py,sha256=Ns0NGKHhAuiv4_SYqbIQn1-C8mRvRdaGrHAHUo99USE,33
|
|
5
|
+
lumos/common/client.py,sha256=zk-hSTf7gJYaBFfPu_y3HwbqhlRUrI8kF5YI03y0htU,16834
|
|
6
|
+
lumos/common/client_helpers.py,sha256=n9PF2wc25-sfZd7dLgcZFBlls3DrFAWhYUbEYXq68NY,1253
|
|
7
|
+
lumos/common/helpers.py,sha256=pv_OAQXRWz2gWNvjG_Sr609yCSz7o803tAubyH9kopQ,3004
|
|
8
|
+
lumos/common/keyhelpers.py,sha256=bAxiuHDvUf2GB3eEfBSlZsiwSL0RNKOZ5NSKeVkFNVY,955
|
|
9
|
+
lumos/common/logging.py,sha256=A4617xvoM5sJg0H6vC6r6NWNcr0ftmFKIY2JqI69-fg,937
|
|
10
|
+
lumos/common/models.py,sha256=llyWPG7QYoGn12u9-zDiAJPGUsmqN9QSniXY7aDknPo,7252
|
|
11
|
+
lumos/list_collections/__init__.py,sha256=so_qquyp3GDq_6VgQ58hb1ka7kttpvERnvuaH0jEKBo,42
|
|
12
|
+
lumos/list_collections/cli.py,sha256=8gDbyiLQfQvGfq3e6tTd3dSiXznIid9OKTf40shY4Dk,8246
|
|
13
|
+
lumos/request/__init__.py,sha256=5oDglMtFk9kr4BS-jyEn7yeXrqneWktoel0S5zmdWyM,36
|
|
14
|
+
lumos/request/cli.py,sha256=tg3ZO0Tq0vR-htEA_QFrR3JjCSg58h5CRj88xL9FpHw,19168
|
|
15
|
+
lumos_identity_cli-0.0.0.dev1.dist-info/METADATA,sha256=U9mACEoH9xezYzyBnB21E-89jvo8h2cjfevPdgTuVsw,1957
|
|
16
|
+
lumos_identity_cli-0.0.0.dev1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
17
|
+
lumos_identity_cli-0.0.0.dev1.dist-info/entry_points.txt,sha256=CiQ7iTJBH8Ay4RAZ9if8t_AJtY3hYei7rKToz3Rr-74,42
|
|
18
|
+
lumos_identity_cli-0.0.0.dev1.dist-info/RECORD,,
|