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/common/helpers.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from click_extra import confirm, echo, prompt
|
|
5
|
+
from pick import pick
|
|
6
|
+
|
|
7
|
+
from lumos.common.client import AuthClient
|
|
8
|
+
from lumos.common.keyhelpers import key_file_path, read_key, write_key
|
|
9
|
+
from lumos.common.models import App, Permission, SupportRequestStatus
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def authenticate(func):
|
|
13
|
+
"""Makes sure client is authenticated first"""
|
|
14
|
+
|
|
15
|
+
@functools.wraps(func)
|
|
16
|
+
def wrapper_authenticate(*args, **kwargs):
|
|
17
|
+
setup(show_prompt=True)
|
|
18
|
+
return func(*args, **kwargs)
|
|
19
|
+
|
|
20
|
+
return wrapper_authenticate
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def setup(show_prompt: bool = False, show_overwrite_prompt: bool = False):
|
|
24
|
+
key_file = key_file_path()
|
|
25
|
+
# if the key file exists, ask if they want to overwrite
|
|
26
|
+
if key_file.exists():
|
|
27
|
+
read_key()
|
|
28
|
+
if not show_overwrite_prompt or not confirm(
|
|
29
|
+
"You already have a key setup. Do you want to overwrite?", default=True
|
|
30
|
+
):
|
|
31
|
+
return
|
|
32
|
+
if show_prompt and not confirm(
|
|
33
|
+
" 🛠️ You need to authenticate to use this application. Do you want to do that now?",
|
|
34
|
+
default=True,
|
|
35
|
+
):
|
|
36
|
+
raise SystemExit(1)
|
|
37
|
+
selected, _ = pick(["OAuth 2.0", "API key"], "How do you want to authenticate?")
|
|
38
|
+
if selected == "API key":
|
|
39
|
+
echo(" ⚙️ Go to your Lumos account > Settings > API Tokens > Add an API Token, and copy the token.")
|
|
40
|
+
api_key = prompt("API key", hide_input=True, confirmation_prompt=True)
|
|
41
|
+
write_key(api_key)
|
|
42
|
+
else:
|
|
43
|
+
login()
|
|
44
|
+
|
|
45
|
+
read_key()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def login(admin: bool | None = False):
|
|
49
|
+
AuthClient().authenticate(admin or False)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def logout():
|
|
53
|
+
key_file = key_file_path()
|
|
54
|
+
key_file.unlink(missing_ok=True)
|
|
55
|
+
if os.environ.get("API_KEY"):
|
|
56
|
+
os.environ["API_KEY"] = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_statuses(status: list[SupportRequestStatus], pending: bool, past: bool) -> set[SupportRequestStatus]:
|
|
60
|
+
if not status:
|
|
61
|
+
status = []
|
|
62
|
+
if pending:
|
|
63
|
+
status += SupportRequestStatus.PENDING_STATUSES
|
|
64
|
+
if past:
|
|
65
|
+
for e in SupportRequestStatus.ALL_STATUSES:
|
|
66
|
+
if e in SupportRequestStatus.PENDING_STATUSES:
|
|
67
|
+
continue
|
|
68
|
+
status.append(e)
|
|
69
|
+
|
|
70
|
+
if len(status) == 0:
|
|
71
|
+
return None
|
|
72
|
+
return set(status)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def check_current_apps(
|
|
76
|
+
apps: list[App], selected_app: App, selected_permissions: list[Permission] | None
|
|
77
|
+
) -> tuple[App, str | None]:
|
|
78
|
+
for app in apps:
|
|
79
|
+
if str(app.app_id) == str(selected_app.id):
|
|
80
|
+
if len(app.requestable_permissions) > 0 and selected_permissions:
|
|
81
|
+
for permission in [str(r.id) for r in app.requestable_permissions]:
|
|
82
|
+
if permission in [str(r.id) for r in selected_permissions]:
|
|
83
|
+
return (
|
|
84
|
+
app,
|
|
85
|
+
"There's already a request for this app and permission",
|
|
86
|
+
)
|
|
87
|
+
elif len(app.requestable_permissions) == 0 and not selected_permissions:
|
|
88
|
+
return app, "There's already a request for this app"
|
|
89
|
+
|
|
90
|
+
return None, None
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from lumos.common.logging import logdebug
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def key_file_path() -> Path:
|
|
8
|
+
if os.environ.get("DEV_MODE"):
|
|
9
|
+
return Path.home() / ".lumos-dev"
|
|
10
|
+
return Path.home() / ".lumos"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def write_key(key: str | None, scope: str | None = None) -> None:
|
|
14
|
+
if not key:
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
os.environ["API_KEY"] = key
|
|
18
|
+
os.environ["SCOPE"] = scope or ""
|
|
19
|
+
|
|
20
|
+
if scope:
|
|
21
|
+
key = f"{scope}:{key}"
|
|
22
|
+
|
|
23
|
+
key_file = key_file_path()
|
|
24
|
+
logdebug(f"Writing token [{key[:9]}...] to {key_file}")
|
|
25
|
+
with key_file.open("w") as f:
|
|
26
|
+
f.write(key)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def read_key() -> str:
|
|
30
|
+
api_key = os.environ.get("API_KEY")
|
|
31
|
+
if api_key:
|
|
32
|
+
return api_key
|
|
33
|
+
|
|
34
|
+
key_file = key_file_path()
|
|
35
|
+
with key_file.open("r") as f:
|
|
36
|
+
api_key = f.read().strip()
|
|
37
|
+
if api_key.count(":") == 1:
|
|
38
|
+
scope, api_key = api_key.split(":")
|
|
39
|
+
os.environ["SCOPE"] = scope
|
|
40
|
+
os.environ["API_KEY"] = api_key
|
|
41
|
+
return api_key
|
lumos/common/logging.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from click_extra import secho
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def logdebug_request(url, headers, body, params, method) -> None:
|
|
7
|
+
logdebug(method + " " + url)
|
|
8
|
+
headers_copy = headers.copy()
|
|
9
|
+
if headers_copy.get("Authorization"):
|
|
10
|
+
headers_copy["Authorization"] = headers_copy["Authorization"][:12] + "..."
|
|
11
|
+
logdebug("HEADERS: " + str(headers_copy))
|
|
12
|
+
logdebug("BODY: " + str(body))
|
|
13
|
+
logdebug("PARAMETERS: " + str(params))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def logdebug_response(response) -> None:
|
|
17
|
+
logdebug("\nRESPONSE: " + str(response.status_code))
|
|
18
|
+
try:
|
|
19
|
+
json = response.json()
|
|
20
|
+
if json.get("access_token"):
|
|
21
|
+
json["access_token"] = json["access_token"][:5] + "..."
|
|
22
|
+
logdebug("HEADERS: " + str(response.headers))
|
|
23
|
+
logdebug("CONTENT: " + str(json))
|
|
24
|
+
except Exception:
|
|
25
|
+
logdebug("CONTENT: " + str(response.content))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def logdebug(msg: str) -> None:
|
|
29
|
+
if os.environ.get("DEBUG"):
|
|
30
|
+
secho(msg, fg="red")
|
lumos/common/models.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
import pytz
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProvisioningMethodOption(Enum):
|
|
11
|
+
DIRECT_TO_USER = "DIRECT_TO_USER"
|
|
12
|
+
GROUPS_AND_HIDDEN = "GROUPS_AND_HIDDEN"
|
|
13
|
+
GROUPS_AND_VISIBLE = "GROUPS_AND_VISIBLE"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LumosModel(BaseModel):
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def tabulate(self) -> list[str]:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AppSettingProvisioning(BaseModel):
|
|
23
|
+
groups_provisioning: ProvisioningMethodOption
|
|
24
|
+
time_based_access: list[str] = []
|
|
25
|
+
allow_multiple_permission_selection: bool
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AppSetting(BaseModel):
|
|
29
|
+
custom_request_instructions: str
|
|
30
|
+
provisioning: AppSettingProvisioning
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class App(LumosModel):
|
|
34
|
+
id: UUID
|
|
35
|
+
user_friendly_label: str
|
|
36
|
+
app_class_id: str
|
|
37
|
+
instance_id: str
|
|
38
|
+
|
|
39
|
+
def __str__(self):
|
|
40
|
+
return self.user_friendly_label
|
|
41
|
+
|
|
42
|
+
def tabulate(self):
|
|
43
|
+
return [str(self.id), self.user_friendly_label]
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def headers():
|
|
47
|
+
return ["ID", "App"]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Group(LumosModel):
|
|
51
|
+
id: UUID
|
|
52
|
+
name: str
|
|
53
|
+
description: str
|
|
54
|
+
app_id: str
|
|
55
|
+
source_app_id: str
|
|
56
|
+
integration_specific_id: str
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def headers():
|
|
60
|
+
return ["ID", "Name", "Description"]
|
|
61
|
+
|
|
62
|
+
def tabulate(self):
|
|
63
|
+
return [str(self.id), self.name, self.description]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Permission(LumosModel):
|
|
67
|
+
id: UUID
|
|
68
|
+
label: str
|
|
69
|
+
app_id: str
|
|
70
|
+
app_class_id: str
|
|
71
|
+
duration_options: list[str] = []
|
|
72
|
+
|
|
73
|
+
def __str__(self):
|
|
74
|
+
return self.label
|
|
75
|
+
|
|
76
|
+
def tabulate(self):
|
|
77
|
+
return [str(self.id), self.label, ", ".join(self.duration_options)]
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def headers():
|
|
81
|
+
return ["ID", "Permission", "Access length options"]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class User(LumosModel):
|
|
85
|
+
id: UUID
|
|
86
|
+
given_name: str
|
|
87
|
+
family_name: str
|
|
88
|
+
email: str
|
|
89
|
+
|
|
90
|
+
def __str__(self):
|
|
91
|
+
return f"{self.given_name} {self.family_name} ({self.email})"
|
|
92
|
+
|
|
93
|
+
def tabulate(self):
|
|
94
|
+
return [f"{self.given_name} {self.family_name}", self.email, str(self.id)]
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def headers():
|
|
98
|
+
return ["Name", "Email", "ID"]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class AccessRequest(LumosModel):
|
|
102
|
+
id: UUID
|
|
103
|
+
app_id: UUID
|
|
104
|
+
app_name: str
|
|
105
|
+
status: str
|
|
106
|
+
requested_at: str
|
|
107
|
+
expires_at: str | None
|
|
108
|
+
requester_user: User
|
|
109
|
+
supporter_user: User | None
|
|
110
|
+
target_user: User
|
|
111
|
+
requestable_permission_ids: list[UUID] | None = None
|
|
112
|
+
requestable_permissions: list[Permission] | None = None
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def _convert_to_human_date(inp: str) -> str:
|
|
116
|
+
if not inp:
|
|
117
|
+
return ""
|
|
118
|
+
import datetime
|
|
119
|
+
|
|
120
|
+
# Parse the input UTC time string to a datetime object
|
|
121
|
+
utc_time = datetime.datetime.strptime(inp, "%Y-%m-%dT%H:%M:%S")
|
|
122
|
+
|
|
123
|
+
# Set the timezone to UTC for the parsed datetime
|
|
124
|
+
utc_time = utc_time.replace(tzinfo=pytz.utc)
|
|
125
|
+
|
|
126
|
+
# Convert UTC time to EST
|
|
127
|
+
est_time = utc_time.astimezone(pytz.timezone("America/New_York"))
|
|
128
|
+
|
|
129
|
+
# Format the EST time into a more human-readable string
|
|
130
|
+
human_date = est_time.strftime("%a %b %d, %Y %I:%M %p EST")
|
|
131
|
+
|
|
132
|
+
return human_date
|
|
133
|
+
|
|
134
|
+
def __str__(self):
|
|
135
|
+
return f"{self.app_name} ({self.status})"
|
|
136
|
+
|
|
137
|
+
def tabulate(self):
|
|
138
|
+
return [str(self.id), *self._tabulate()]
|
|
139
|
+
|
|
140
|
+
def tabulate_as_app(self):
|
|
141
|
+
return [self.app_id, *self._tabulate()]
|
|
142
|
+
|
|
143
|
+
def _tabulate(self):
|
|
144
|
+
return [
|
|
145
|
+
self.app_name,
|
|
146
|
+
"\n".join([p.label for p in self.requestable_permissions]) if self.requestable_permissions else "-----",
|
|
147
|
+
self.requester_user.given_name + " " + self.requester_user.family_name,
|
|
148
|
+
self.target_user.given_name + " " + self.target_user.family_name
|
|
149
|
+
if self.requester_user.id != self.target_user.id
|
|
150
|
+
else "(self)",
|
|
151
|
+
self.status,
|
|
152
|
+
self._convert_to_human_date(self.requested_at),
|
|
153
|
+
self._convert_to_human_date(self.expires_at),
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def headers():
|
|
158
|
+
return ["ID", "App", "Permissions", "Requester", "For", "Status", "Requested at", "Expires at"]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# If this enum is updated both this table as well as SupportRequestComments must be updated.
|
|
162
|
+
class SupportRequestStatus:
|
|
163
|
+
# initial status, right after creating a request, no action should be taken on the request
|
|
164
|
+
# at this stage
|
|
165
|
+
PENDING = "PENDING"
|
|
166
|
+
# after recognizing the manager approval for the request is required, it's waiting for manager
|
|
167
|
+
PENDING_MANAGER_APPROVAL = "PENDING_MANAGER_APPROVAL"
|
|
168
|
+
# after a manager approves it
|
|
169
|
+
MANAGER_APPROVED = "MANAGER_APPROVED"
|
|
170
|
+
# when a manager rejects it
|
|
171
|
+
MANAGER_DENIED = "MANAGER_DENIED"
|
|
172
|
+
# after recognizing the approval for the request is required, it's waiting for end-user
|
|
173
|
+
PENDING_APPROVAL = "PENDING_APPROVAL"
|
|
174
|
+
# after an approver approves it or assigned automatically when there is no need for approval
|
|
175
|
+
APPROVED = "APPROVED"
|
|
176
|
+
# when an approver rejects it
|
|
177
|
+
DENIED = "DENIED"
|
|
178
|
+
# assigned automatically when neither the approver or admin completes the request in time
|
|
179
|
+
EXPIRED = "EXPIRED"
|
|
180
|
+
# after user cancels the request
|
|
181
|
+
CANCELLED = "CANCELLED"
|
|
182
|
+
# when the request enters provisioning and it's still not clear whether it'll happen
|
|
183
|
+
# automatically or we will need to ask for manual provisioning
|
|
184
|
+
PENDING_PROVISIONING = "PENDING_PROVISIONING"
|
|
185
|
+
# after recognizing the provisioning for the request needs to be done manually,
|
|
186
|
+
# it's waiting for end-user
|
|
187
|
+
PENDING_MANUAL_PROVISIONING = "PENDING_MANUAL_PROVISIONING"
|
|
188
|
+
# when admin denied the request
|
|
189
|
+
DENIED_PROVISIONING = "DENIED_PROVISIONING"
|
|
190
|
+
# after either the manual or automatic provisioning,
|
|
191
|
+
# when it's waiting for the final "cleanup" on our end
|
|
192
|
+
PROVISIONED = "PROVISIONED"
|
|
193
|
+
# when a time based request has been completed and the time has elapsed but automatic removal
|
|
194
|
+
# failed and access needs to be manually removed
|
|
195
|
+
PENDING_MANUAL_DEPROVISIONING = "PENDING_MANUAL_DEPROVISIONING"
|
|
196
|
+
# when the request has been completed and access has been removed because the request was
|
|
197
|
+
# only for a limited amount of time
|
|
198
|
+
TIME_BASED_EXPIRED = "TIME_BASED_EXPIRED"
|
|
199
|
+
# when the request has been fully finalized and there are no more actions pending
|
|
200
|
+
COMPLETED = "COMPLETED"
|
|
201
|
+
# when the request has been marked to be reverted by a user via the "Undo an action" flow
|
|
202
|
+
# but before the actual reverting is completed
|
|
203
|
+
REVERTING = "REVERTING"
|
|
204
|
+
# when the request has been fully reverted
|
|
205
|
+
REVERTED = "REVERTED"
|
|
206
|
+
|
|
207
|
+
PENDING_STATUSES: ClassVar[list[str]] = [
|
|
208
|
+
PENDING,
|
|
209
|
+
PENDING_MANAGER_APPROVAL,
|
|
210
|
+
PENDING_MANUAL_PROVISIONING,
|
|
211
|
+
PENDING_APPROVAL,
|
|
212
|
+
PENDING_PROVISIONING,
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
SUCCESS_STATUSES: ClassVar[list[str]] = [APPROVED, COMPLETED, PROVISIONED]
|
|
216
|
+
|
|
217
|
+
ALL_STATUSES: ClassVar[list[str]] = [
|
|
218
|
+
PENDING,
|
|
219
|
+
PENDING_MANAGER_APPROVAL,
|
|
220
|
+
MANAGER_APPROVED,
|
|
221
|
+
MANAGER_DENIED,
|
|
222
|
+
PENDING_APPROVAL,
|
|
223
|
+
APPROVED,
|
|
224
|
+
DENIED,
|
|
225
|
+
EXPIRED,
|
|
226
|
+
CANCELLED,
|
|
227
|
+
PENDING_PROVISIONING,
|
|
228
|
+
PENDING_MANUAL_PROVISIONING,
|
|
229
|
+
DENIED_PROVISIONING,
|
|
230
|
+
PROVISIONED,
|
|
231
|
+
PENDING_MANUAL_DEPROVISIONING,
|
|
232
|
+
TIME_BASED_EXPIRED,
|
|
233
|
+
COMPLETED,
|
|
234
|
+
REVERTING,
|
|
235
|
+
REVERTED,
|
|
236
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .cli import list_group as list_group
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
from json import dumps
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
from click_extra import group, option
|
|
5
|
+
from tabulate import tabulate
|
|
6
|
+
|
|
7
|
+
from lumos.common.client import ApiClient
|
|
8
|
+
from lumos.common.helpers import authenticate, get_statuses
|
|
9
|
+
from lumos.common.models import AccessRequest, LumosModel
|
|
10
|
+
|
|
11
|
+
client = ApiClient()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@group(name="list")
|
|
15
|
+
def list_group():
|
|
16
|
+
"""List various Lumos resources"""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@list_group.command("users", help="List users in Lumos")
|
|
21
|
+
@option("--like", default=None, help="Search by name or email")
|
|
22
|
+
@option("--csv", is_flag=True, help="Output as CSV")
|
|
23
|
+
@option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
24
|
+
@option("--paginate/--no-paginate", default=True, help="Pagination")
|
|
25
|
+
@option("--page-size", default=100, help="Page size")
|
|
26
|
+
@option("--page", default=1, help="Page")
|
|
27
|
+
@option("--id-only", is_flag=True, help="Output ID only")
|
|
28
|
+
@authenticate
|
|
29
|
+
def list_users(
|
|
30
|
+
like: str | None,
|
|
31
|
+
csv: bool,
|
|
32
|
+
output_json: bool,
|
|
33
|
+
paginate: bool,
|
|
34
|
+
page_size: int,
|
|
35
|
+
page: int,
|
|
36
|
+
id_only: bool,
|
|
37
|
+
) -> None:
|
|
38
|
+
all = csv or output_json or not paginate
|
|
39
|
+
users, count, total = client.get_users(like=like, all=all, page=page, page_size=page_size)
|
|
40
|
+
display(
|
|
41
|
+
"users",
|
|
42
|
+
users,
|
|
43
|
+
count,
|
|
44
|
+
total,
|
|
45
|
+
csv,
|
|
46
|
+
output_json,
|
|
47
|
+
page=page,
|
|
48
|
+
page_size=page_size,
|
|
49
|
+
id_only=id_only,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@list_group.command("permissions", help="List permissions for a given app")
|
|
54
|
+
@option("--app", required=True, type=str, help="App UUID")
|
|
55
|
+
@option("--like", default=None, help="Filters permissions")
|
|
56
|
+
@option("--csv", is_flag=True, help="Output as CSV")
|
|
57
|
+
@option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
58
|
+
@option("--paginate/--no-paginate", default=True, help="Pagination")
|
|
59
|
+
@option("--page-size", default=100, help="Page size")
|
|
60
|
+
@option("--page", default=1, help="Page")
|
|
61
|
+
@option("--id-only", is_flag=True, help="Output ID only")
|
|
62
|
+
@authenticate
|
|
63
|
+
def list_permissions(
|
|
64
|
+
app: str,
|
|
65
|
+
like: str | None,
|
|
66
|
+
csv: bool,
|
|
67
|
+
output_json: bool,
|
|
68
|
+
paginate: bool,
|
|
69
|
+
page_size: int,
|
|
70
|
+
page: int,
|
|
71
|
+
id_only: bool,
|
|
72
|
+
) -> None:
|
|
73
|
+
app_uuid = UUID(app)
|
|
74
|
+
all = csv or output_json or not paginate
|
|
75
|
+
permissions, count, total = client.get_app_requestable_permissions(
|
|
76
|
+
app_id=app_uuid, search_term=like, all=all, page=page, page_size=page_size
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
display(
|
|
80
|
+
"permissions",
|
|
81
|
+
permissions,
|
|
82
|
+
count,
|
|
83
|
+
total,
|
|
84
|
+
csv,
|
|
85
|
+
output_json,
|
|
86
|
+
page=page,
|
|
87
|
+
page_size=page_size,
|
|
88
|
+
id_only=id_only,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@list_group.command("groups", help="List groups for the domain or the specified --app")
|
|
93
|
+
@option(
|
|
94
|
+
"--app",
|
|
95
|
+
default=None,
|
|
96
|
+
type=str,
|
|
97
|
+
help="App ID to filter groups by. If not provided, lists all groups.",
|
|
98
|
+
)
|
|
99
|
+
@option("--like", default=None, help="Filters groups")
|
|
100
|
+
@option("--csv", is_flag=True, help="Output as CSV")
|
|
101
|
+
@option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
102
|
+
@option("--paginate/--no-paginate", default=True, help="Pagination")
|
|
103
|
+
@option("--page-size", default=100, help="Page size")
|
|
104
|
+
@option("--page", default=1, help="Page")
|
|
105
|
+
@option("--id-only", is_flag=True, help="Output ID only")
|
|
106
|
+
@authenticate
|
|
107
|
+
def list_groups(
|
|
108
|
+
app: str | None,
|
|
109
|
+
like: str | None,
|
|
110
|
+
csv: bool,
|
|
111
|
+
output_json: bool,
|
|
112
|
+
paginate: bool,
|
|
113
|
+
page_size: int,
|
|
114
|
+
page: int,
|
|
115
|
+
id_only: bool,
|
|
116
|
+
) -> None:
|
|
117
|
+
app_uuid = UUID(app) if app else None
|
|
118
|
+
all = csv or output_json or not paginate
|
|
119
|
+
groups, count, total = client.get_groups(app_id=app_uuid, search_term=like, all=all, page=page, page_size=page_size)
|
|
120
|
+
|
|
121
|
+
display(
|
|
122
|
+
"groups",
|
|
123
|
+
groups,
|
|
124
|
+
count,
|
|
125
|
+
total,
|
|
126
|
+
csv,
|
|
127
|
+
output_json,
|
|
128
|
+
page=page,
|
|
129
|
+
page_size=page_size,
|
|
130
|
+
id_only=id_only,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@list_group.command("requests", help="List access requests")
|
|
135
|
+
@option(
|
|
136
|
+
"--for-user",
|
|
137
|
+
default=None,
|
|
138
|
+
type=str,
|
|
139
|
+
help="Show only requests for ('targetting') a particular user",
|
|
140
|
+
)
|
|
141
|
+
@option(
|
|
142
|
+
"--mine",
|
|
143
|
+
is_flag=True,
|
|
144
|
+
help="Show only requests for ('targetting') me. Takes precedence over --for-user.",
|
|
145
|
+
)
|
|
146
|
+
@option(
|
|
147
|
+
"--status",
|
|
148
|
+
multiple=True,
|
|
149
|
+
help="One of `PENDING`, `COMPLETED`, `DENIED_PROVISIONING`, etc",
|
|
150
|
+
)
|
|
151
|
+
@option("--pending", is_flag=True, help="Show only pending requests")
|
|
152
|
+
@option("--past", is_flag=True, help="Show only past requests")
|
|
153
|
+
@option("--csv", is_flag=True, help="Output as CSV")
|
|
154
|
+
@option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
155
|
+
@option("--paginate/--no-paginate", default=True, help="Pagination")
|
|
156
|
+
@option("--page-size", default=100, help="Page size")
|
|
157
|
+
@option("--page", default=1, help="Page")
|
|
158
|
+
@option("--id-only", is_flag=True, help="Output ID only")
|
|
159
|
+
@authenticate
|
|
160
|
+
def list_requests(
|
|
161
|
+
for_user: str | None,
|
|
162
|
+
mine: bool,
|
|
163
|
+
status: tuple,
|
|
164
|
+
pending: bool,
|
|
165
|
+
past: bool,
|
|
166
|
+
csv: bool,
|
|
167
|
+
output_json: bool,
|
|
168
|
+
paginate: bool,
|
|
169
|
+
page_size: int,
|
|
170
|
+
page: int,
|
|
171
|
+
id_only: bool,
|
|
172
|
+
) -> None:
|
|
173
|
+
user_uuid = None
|
|
174
|
+
if mine:
|
|
175
|
+
user_uuid = client.get_current_user_id()
|
|
176
|
+
elif for_user:
|
|
177
|
+
user_uuid = UUID(for_user)
|
|
178
|
+
|
|
179
|
+
status_list = list(status) if status else None
|
|
180
|
+
status_set = get_statuses(status_list, pending, past)
|
|
181
|
+
|
|
182
|
+
all = csv or output_json or not paginate
|
|
183
|
+
access_requests, count, total, _, _ = client.get_access_requests(
|
|
184
|
+
target_user_id=user_uuid,
|
|
185
|
+
status=status_set,
|
|
186
|
+
all=all,
|
|
187
|
+
page=page,
|
|
188
|
+
page_size=page_size,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
access_requests = sorted(access_requests, key=lambda x: x.requested_at, reverse=True)
|
|
192
|
+
|
|
193
|
+
display(
|
|
194
|
+
"requests",
|
|
195
|
+
access_requests,
|
|
196
|
+
count,
|
|
197
|
+
total,
|
|
198
|
+
csv,
|
|
199
|
+
output_json,
|
|
200
|
+
page_size=page_size,
|
|
201
|
+
page=page,
|
|
202
|
+
id_only=id_only,
|
|
203
|
+
search=False,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@list_group.command("apps", help="List apps in the appstore")
|
|
208
|
+
@option("--like", default=None, help="Filters apps by search term")
|
|
209
|
+
@option("--mine", is_flag=True, help="Show only my apps.")
|
|
210
|
+
@option("--csv", is_flag=True, help="Output as CSV")
|
|
211
|
+
@option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
212
|
+
@option("--paginate/--no-paginate", default=True, help="Pagination")
|
|
213
|
+
@option("--page-size", default=100, help="Page size")
|
|
214
|
+
@option("--page", default=1, help="Page")
|
|
215
|
+
@option("--id-only", is_flag=True, help="Output ID only")
|
|
216
|
+
@authenticate
|
|
217
|
+
def list_apps(
|
|
218
|
+
like: str | None,
|
|
219
|
+
mine: bool,
|
|
220
|
+
csv: bool,
|
|
221
|
+
output_json: bool,
|
|
222
|
+
paginate: bool,
|
|
223
|
+
page_size: int,
|
|
224
|
+
page: int,
|
|
225
|
+
id_only: bool,
|
|
226
|
+
) -> None:
|
|
227
|
+
all = csv or output_json or not paginate
|
|
228
|
+
if mine:
|
|
229
|
+
access_requests = client.get_my_apps()
|
|
230
|
+
if len(access_requests) > 0:
|
|
231
|
+
print(
|
|
232
|
+
tabulate(
|
|
233
|
+
[req.tabulate_as_app() for req in access_requests],
|
|
234
|
+
headers=AccessRequest.headers(),
|
|
235
|
+
),
|
|
236
|
+
"\n",
|
|
237
|
+
)
|
|
238
|
+
else:
|
|
239
|
+
print("No apps found.")
|
|
240
|
+
return
|
|
241
|
+
apps, count, total = client.get_appstore_apps(name_search=like, all=all, page_size=page_size, page=page)
|
|
242
|
+
display(
|
|
243
|
+
"apps",
|
|
244
|
+
apps,
|
|
245
|
+
count,
|
|
246
|
+
total,
|
|
247
|
+
csv,
|
|
248
|
+
output_json,
|
|
249
|
+
page=page,
|
|
250
|
+
page_size=page_size,
|
|
251
|
+
id_only=id_only,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def display(
|
|
256
|
+
description: str,
|
|
257
|
+
data: list[LumosModel],
|
|
258
|
+
count: int,
|
|
259
|
+
total: int,
|
|
260
|
+
csv: bool,
|
|
261
|
+
json: bool,
|
|
262
|
+
page: int,
|
|
263
|
+
page_size: int,
|
|
264
|
+
id_only: bool = False,
|
|
265
|
+
search: bool = True,
|
|
266
|
+
):
|
|
267
|
+
if len(data) == 0:
|
|
268
|
+
print(f"No {description} found.")
|
|
269
|
+
return
|
|
270
|
+
if csv:
|
|
271
|
+
for row in data:
|
|
272
|
+
print(",".join([str(cell).replace(", ", "|") for cell in row.tabulate()]))
|
|
273
|
+
return
|
|
274
|
+
if json:
|
|
275
|
+
print(dumps([d.__dict__ for d in data], default=str, indent=2))
|
|
276
|
+
return
|
|
277
|
+
if id_only:
|
|
278
|
+
if len(data) > 0:
|
|
279
|
+
for row in data:
|
|
280
|
+
print(row.tabulate()[0])
|
|
281
|
+
return
|
|
282
|
+
headers = data[0].headers()
|
|
283
|
+
print(tabulate([d.tabulate() for d in data], headers=headers), "\n")
|
|
284
|
+
remaining = total - count - page_size * (page - 1)
|
|
285
|
+
if remaining > 0:
|
|
286
|
+
if search:
|
|
287
|
+
print(f"There are {remaining} more {description} that match your search. Use --like to search.\n")
|
|
288
|
+
else:
|
|
289
|
+
print(f"There are {remaining} more {description} not shown.\n")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .cli import request as request
|