midpoint-cli 1.5.0__tar.gz → 1.5.0.dev2__tar.gz
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.
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/PKG-INFO +11 -20
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/README.md +6 -15
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/pyproject.toml +5 -8
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/src/midpoint_cli/__init__.py +19 -32
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/src/midpoint_cli/client/__init__.py +0 -91
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/src/midpoint_cli/client/objects.py +21 -91
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/src/midpoint_cli/prompt/__init__.py +2 -74
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/src/midpoint_cli/prompt/base.py +0 -8
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/src/midpoint_cli/prompt/console.py +8 -7
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/src/midpoint_cli/prompt/delete.py +0 -3
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/src/midpoint_cli/prompt/get.py +0 -1
- midpoint_cli-1.5.0.dev2/src/midpoint_cli/prompt/org.py +87 -0
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/src/midpoint_cli/prompt/put.py +0 -1
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/src/midpoint_cli/prompt/resource.py +1 -9
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/src/midpoint_cli/prompt/script.py +0 -1
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/src/midpoint_cli/prompt/task.py +1 -16
- midpoint_cli-1.5.0.dev2/src/midpoint_cli/prompt/user.py +120 -0
- midpoint_cli-1.5.0/src/midpoint_cli/prompt/info.py +0 -27
- midpoint_cli-1.5.0/src/midpoint_cli/prompt/org.py +0 -146
- midpoint_cli-1.5.0/src/midpoint_cli/prompt/role.py +0 -122
- midpoint_cli-1.5.0/src/midpoint_cli/prompt/user.py +0 -293
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/LICENSE +0 -0
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/src/midpoint_cli/client/observer.py +0 -0
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/src/midpoint_cli/client/patch.py +0 -0
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/src/midpoint_cli/client/progress.py +0 -0
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/src/midpoint_cli/client/session.py +0 -0
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/src/midpoint_cli/prompt/complete.py +0 -0
- {midpoint_cli-1.5.0 → midpoint_cli-1.5.0.dev2}/src/midpoint_cli/prompt/configuration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: midpoint-cli
|
|
3
|
-
Version: 1.5.0
|
|
3
|
+
Version: 1.5.0.dev2
|
|
4
4
|
Summary: A command line client to Midpoint Identity Management system.
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -22,11 +22,11 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.14
|
|
23
23
|
Classifier: Topic :: System :: Systems Administration
|
|
24
24
|
Requires-Dist: prompt-toolkit (>=3.0,<4.0)
|
|
25
|
-
Requires-Dist: pygments (>=2.
|
|
26
|
-
Requires-Dist: requests (>=2.
|
|
27
|
-
Requires-Dist: rich (>=13.0,<
|
|
25
|
+
Requires-Dist: pygments (>=2.0,<3.0)
|
|
26
|
+
Requires-Dist: requests (>=2.31,<3.0)
|
|
27
|
+
Requires-Dist: rich (>=13.0,<14.0)
|
|
28
28
|
Requires-Dist: unidecode (>=1.3,<2.0)
|
|
29
|
-
Requires-Dist: urllib3 (>=2.
|
|
29
|
+
Requires-Dist: urllib3 (>=2.0,<3.0)
|
|
30
30
|
Project-URL: Repository, https://gitlab.com/alcibiade/midpoint-cli
|
|
31
31
|
Description-Content-Type: text/markdown
|
|
32
32
|
|
|
@@ -188,9 +188,8 @@ Available commands:
|
|
|
188
188
|
user Manage users.
|
|
189
189
|
Subcommands:
|
|
190
190
|
- users: List all users
|
|
191
|
-
- user search <term
|
|
191
|
+
- user search <term>: Search for users by name
|
|
192
192
|
Example: user search john
|
|
193
|
-
Example: user search +Engineering Doe Smith
|
|
194
193
|
```
|
|
195
194
|
|
|
196
195
|
### External configuration files (since v1.2)
|
|
@@ -278,10 +277,7 @@ midpoint> users
|
|
|
278
277
|
# Lists all users with their details
|
|
279
278
|
|
|
280
279
|
midpoint> user search john
|
|
281
|
-
# Search for users matching "john"
|
|
282
|
-
|
|
283
|
-
midpoint> user search +Engineering Doe Smith
|
|
284
|
-
# Users in Engineering who match Doe or Smith (mandatory + optional OR)
|
|
280
|
+
# Search for users matching "john"
|
|
285
281
|
|
|
286
282
|
midpoint> org
|
|
287
283
|
# List all organizational units
|
|
@@ -378,18 +374,13 @@ midpoint-cli -u admin -p pass -U https://localhost:8080/midpoint/ users
|
|
|
378
374
|
#### Search for a specific user
|
|
379
375
|
|
|
380
376
|
```bash
|
|
381
|
-
# Search by
|
|
382
|
-
midpoint-cli user search john
|
|
383
|
-
|
|
384
|
-
# Mandatory terms with + prefix (AND: must contain both "john" AND "doe")
|
|
385
|
-
midpoint-cli user search +john +doe
|
|
377
|
+
# Search by name or username
|
|
378
|
+
midpoint-cli user search john
|
|
386
379
|
|
|
387
|
-
#
|
|
388
|
-
midpoint-cli user search
|
|
380
|
+
# Search with wildcards
|
|
381
|
+
midpoint-cli user search "j*"
|
|
389
382
|
```
|
|
390
383
|
|
|
391
|
-
Search terms are case-insensitive and matched as substrings against all user fields (name, full name, email, OU, etc.). Unicode characters are normalised before matching (e.g. `François` matches `francois`).
|
|
392
|
-
|
|
393
384
|
#### Get user details (XML definition)
|
|
394
385
|
|
|
395
386
|
```bash
|
|
@@ -156,9 +156,8 @@ Available commands:
|
|
|
156
156
|
user Manage users.
|
|
157
157
|
Subcommands:
|
|
158
158
|
- users: List all users
|
|
159
|
-
- user search <term
|
|
159
|
+
- user search <term>: Search for users by name
|
|
160
160
|
Example: user search john
|
|
161
|
-
Example: user search +Engineering Doe Smith
|
|
162
161
|
```
|
|
163
162
|
|
|
164
163
|
### External configuration files (since v1.2)
|
|
@@ -246,10 +245,7 @@ midpoint> users
|
|
|
246
245
|
# Lists all users with their details
|
|
247
246
|
|
|
248
247
|
midpoint> user search john
|
|
249
|
-
# Search for users matching "john"
|
|
250
|
-
|
|
251
|
-
midpoint> user search +Engineering Doe Smith
|
|
252
|
-
# Users in Engineering who match Doe or Smith (mandatory + optional OR)
|
|
248
|
+
# Search for users matching "john"
|
|
253
249
|
|
|
254
250
|
midpoint> org
|
|
255
251
|
# List all organizational units
|
|
@@ -346,18 +342,13 @@ midpoint-cli -u admin -p pass -U https://localhost:8080/midpoint/ users
|
|
|
346
342
|
#### Search for a specific user
|
|
347
343
|
|
|
348
344
|
```bash
|
|
349
|
-
# Search by
|
|
350
|
-
midpoint-cli user search john
|
|
351
|
-
|
|
352
|
-
# Mandatory terms with + prefix (AND: must contain both "john" AND "doe")
|
|
353
|
-
midpoint-cli user search +john +doe
|
|
345
|
+
# Search by name or username
|
|
346
|
+
midpoint-cli user search john
|
|
354
347
|
|
|
355
|
-
#
|
|
356
|
-
midpoint-cli user search
|
|
348
|
+
# Search with wildcards
|
|
349
|
+
midpoint-cli user search "j*"
|
|
357
350
|
```
|
|
358
351
|
|
|
359
|
-
Search terms are case-insensitive and matched as substrings against all user fields (name, full name, email, OU, etc.). Unicode characters are normalised before matching (e.g. `François` matches `francois`).
|
|
360
|
-
|
|
361
352
|
#### Get user details (XML definition)
|
|
362
353
|
|
|
363
354
|
```bash
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "midpoint-cli"
|
|
7
|
-
version = "1.5.0"
|
|
7
|
+
version = "1.5.0.dev2"
|
|
8
8
|
description = "A command line client to Midpoint Identity Management system."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
|
|
@@ -38,11 +38,11 @@ midpoint-cli = 'midpoint_cli:main'
|
|
|
38
38
|
[tool.poetry.dependencies]
|
|
39
39
|
python = "^3.9.0"
|
|
40
40
|
unidecode = "^1.3"
|
|
41
|
-
requests = "^2.
|
|
42
|
-
urllib3 = "^2.
|
|
43
|
-
rich = "
|
|
41
|
+
requests = "^2.31"
|
|
42
|
+
urllib3 = "^2.0"
|
|
43
|
+
rich = "^13.0"
|
|
44
44
|
prompt-toolkit = "^3.0"
|
|
45
|
-
pygments = "^2.
|
|
45
|
+
pygments = "^2.0"
|
|
46
46
|
|
|
47
47
|
[tool.poetry.group.test.dependencies]
|
|
48
48
|
pytest = "*"
|
|
@@ -52,9 +52,6 @@ pytest-cov = "*"
|
|
|
52
52
|
ruff = "*"
|
|
53
53
|
mypy = "*"
|
|
54
54
|
|
|
55
|
-
[tool.pytest.ini_options]
|
|
56
|
-
testpaths = ["test/unit"]
|
|
57
|
-
|
|
58
55
|
[tool.ruff]
|
|
59
56
|
target-version = "py39"
|
|
60
57
|
line-length = 120
|
|
@@ -12,37 +12,6 @@ from midpoint_cli.prompt.configuration import compute_client_configuration
|
|
|
12
12
|
from midpoint_cli.prompt.console import ConsoleDisplay
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def _build_commands_epilog(version: str) -> str:
|
|
16
|
-
skip = {'EOF', 'exit'}
|
|
17
|
-
commands = []
|
|
18
|
-
for name in sorted(dir(MidpointClientPrompt)):
|
|
19
|
-
if name.startswith('do_'):
|
|
20
|
-
cmd = name[3:]
|
|
21
|
-
if cmd in skip:
|
|
22
|
-
continue
|
|
23
|
-
method = getattr(MidpointClientPrompt, name)
|
|
24
|
-
doc = method.__doc__.strip().splitlines()[0] if method.__doc__ else ''
|
|
25
|
-
commands.append((cmd, doc))
|
|
26
|
-
|
|
27
|
-
if not commands:
|
|
28
|
-
return ''
|
|
29
|
-
|
|
30
|
-
width = max(len(cmd) for cmd, _ in commands) + 2
|
|
31
|
-
lines = ['', 'Available commands:']
|
|
32
|
-
for cmd, doc in commands:
|
|
33
|
-
lines.append(f' {cmd:<{width}}{doc}')
|
|
34
|
-
|
|
35
|
-
lines += [
|
|
36
|
-
'',
|
|
37
|
-
'Server URL and credentials can also be stored in ~/.midpoint-cli.cfg,',
|
|
38
|
-
'or read from environment variables MIDPOINT_URL, MIDPOINT_USERNAME and MIDPOINT_PASSWORD.',
|
|
39
|
-
'',
|
|
40
|
-
f'Midpoint-cli version {version}, created and maintained by Yannick Kirschhoffer alcibiade@alcibiade.org',
|
|
41
|
-
'',
|
|
42
|
-
]
|
|
43
|
-
return '\n'.join(lines)
|
|
44
|
-
|
|
45
|
-
|
|
46
15
|
def main():
|
|
47
16
|
logging.basicConfig(level=logging.INFO)
|
|
48
17
|
logging.getLogger('urllib3.connectionpool').setLevel(logging.ERROR)
|
|
@@ -54,7 +23,25 @@ def main():
|
|
|
54
23
|
argument_parser = ArgumentParser(
|
|
55
24
|
description='An interactive Midpoint command line client.',
|
|
56
25
|
formatter_class=RawTextHelpFormatter,
|
|
57
|
-
epilog=
|
|
26
|
+
epilog="""
|
|
27
|
+
Available commands:
|
|
28
|
+
get Get an XML definition from the server from an existing OID reference.
|
|
29
|
+
put Create/Update a server object based on an XML structure.
|
|
30
|
+
delete Delete a server object based on its type and OID.
|
|
31
|
+
|
|
32
|
+
task Manage server tasks.
|
|
33
|
+
resource Manage resources on the server.
|
|
34
|
+
|
|
35
|
+
org Manage organizations.
|
|
36
|
+
user Manage users.
|
|
37
|
+
|
|
38
|
+
Server URL and credentials can also be stored in ~/.midpoint-cli.cfg, or read from environment variables MIDPOINT_URL,
|
|
39
|
+
MIDPOINT_USERNAME and MIDPOINT_PASSWORD.
|
|
40
|
+
|
|
41
|
+
Midpoint-cli version """
|
|
42
|
+
+ version
|
|
43
|
+
+ """, created and maintained by Yannick Kirschhoffer alcibiade@alcibiade.org
|
|
44
|
+
""",
|
|
58
45
|
)
|
|
59
46
|
argument_parser.add_argument(
|
|
60
47
|
'-v', '--version', help='Set the username to authenticate this session.', action='store_true'
|
|
@@ -14,8 +14,6 @@ from midpoint_cli.client.objects import (
|
|
|
14
14
|
MidpointObjectTypes,
|
|
15
15
|
MidpointOrganization,
|
|
16
16
|
MidpointResource,
|
|
17
|
-
MidpointRole,
|
|
18
|
-
MidpointShadow,
|
|
19
17
|
MidpointTask,
|
|
20
18
|
MidpointUser,
|
|
21
19
|
namespaces,
|
|
@@ -82,19 +80,6 @@ class RestApiClient:
|
|
|
82
80
|
|
|
83
81
|
return cast(str, response.content.decode())
|
|
84
82
|
|
|
85
|
-
def patch_element(self, element_class: MidpointObjectType, element_oid: str, xml_body: str) -> str:
|
|
86
|
-
assert element_class.endpoint is not None, f'Element class {element_class} has no endpoint'
|
|
87
|
-
target_url = urljoin(self.configuration.url, 'ws/rest/' + element_class.endpoint + '/' + element_oid)
|
|
88
|
-
response = self.requests_session.patch(
|
|
89
|
-
url=target_url,
|
|
90
|
-
data=xml_body,
|
|
91
|
-
headers={'Content-Type': 'application/xml'},
|
|
92
|
-
auth=(self.configuration.username, self.configuration.password),
|
|
93
|
-
)
|
|
94
|
-
if response.status_code >= 300:
|
|
95
|
-
raise MidpointServerError(f'Error {response.status_code} received from server at {target_url}')
|
|
96
|
-
return cast(str, response.content.decode())
|
|
97
|
-
|
|
98
83
|
def get_elements(self, element_class: MidpointObjectType) -> Element:
|
|
99
84
|
assert element_class.endpoint is not None, f'Element class {element_class} has no endpoint'
|
|
100
85
|
url = urljoin(self.configuration.url, 'ws/rest/' + element_class.endpoint)
|
|
@@ -114,13 +99,6 @@ class RestApiClient:
|
|
|
114
99
|
|
|
115
100
|
return cast(bytes, response.content)
|
|
116
101
|
|
|
117
|
-
def get_endpoint(self, path: str) -> str:
|
|
118
|
-
url = urljoin(self.configuration.url, path)
|
|
119
|
-
response = self.requests_session.get(url=url, auth=(self.configuration.username, self.configuration.password))
|
|
120
|
-
if response.status_code >= 300:
|
|
121
|
-
raise MidpointServerError(f'Server responded with status code {response.status_code} on {url}')
|
|
122
|
-
return cast(str, response.content.decode())
|
|
123
|
-
|
|
124
102
|
def put_element(
|
|
125
103
|
self, xml_filename: str, patch_file: Optional[str], patch_write: bool
|
|
126
104
|
) -> tuple[MidpointObjectType, str]:
|
|
@@ -218,55 +196,6 @@ class MidpointClient:
|
|
|
218
196
|
def get_orgs(self) -> MidpointObjectList:
|
|
219
197
|
return self.__get_collection(MidpointObjectTypes.ORG.value, MidpointOrganization)
|
|
220
198
|
|
|
221
|
-
def get_roles(self) -> MidpointObjectList:
|
|
222
|
-
return self.__get_collection(MidpointObjectTypes.ROLE.value, MidpointRole)
|
|
223
|
-
|
|
224
|
-
def assign_role(self, user_oid: str, role_oid: str) -> None:
|
|
225
|
-
xml_body = (
|
|
226
|
-
'<objectModification'
|
|
227
|
-
" xmlns='http://midpoint.evolveum.com/xml/ns/public/common/api-types-3'"
|
|
228
|
-
" xmlns:c='http://midpoint.evolveum.com/xml/ns/public/common/common-3'"
|
|
229
|
-
" xmlns:t='http://prism.evolveum.com/xml/ns/public/types-3'>"
|
|
230
|
-
'<itemDelta>'
|
|
231
|
-
'<t:modificationType>add</t:modificationType>'
|
|
232
|
-
'<t:path>c:assignment</t:path>'
|
|
233
|
-
'<t:value>'
|
|
234
|
-
f"<c:targetRef oid='{role_oid}' type='c:RoleType'/>"
|
|
235
|
-
'</t:value>'
|
|
236
|
-
'</itemDelta>'
|
|
237
|
-
'</objectModification>'
|
|
238
|
-
)
|
|
239
|
-
self.api_client.patch_element(MidpointObjectTypes.USER.value, user_oid, xml_body)
|
|
240
|
-
|
|
241
|
-
def unassign_role(self, user_oid: str, role_oid: str) -> None:
|
|
242
|
-
xml_body = (
|
|
243
|
-
'<objectModification'
|
|
244
|
-
" xmlns='http://midpoint.evolveum.com/xml/ns/public/common/api-types-3'"
|
|
245
|
-
" xmlns:c='http://midpoint.evolveum.com/xml/ns/public/common/common-3'"
|
|
246
|
-
" xmlns:t='http://prism.evolveum.com/xml/ns/public/types-3'>"
|
|
247
|
-
'<itemDelta>'
|
|
248
|
-
'<t:modificationType>delete</t:modificationType>'
|
|
249
|
-
'<t:path>c:assignment</t:path>'
|
|
250
|
-
'<t:value>'
|
|
251
|
-
f"<c:targetRef oid='{role_oid}' type='c:RoleType'/>"
|
|
252
|
-
'</t:value>'
|
|
253
|
-
'</itemDelta>'
|
|
254
|
-
'</objectModification>'
|
|
255
|
-
)
|
|
256
|
-
self.api_client.patch_element(MidpointObjectTypes.USER.value, user_oid, xml_body)
|
|
257
|
-
|
|
258
|
-
def get_resource(self, oid: str) -> MidpointResource:
|
|
259
|
-
return MidpointResource(self.__get_object(MidpointObjectTypes.RESOURCE.value, oid))
|
|
260
|
-
|
|
261
|
-
def get_role(self, oid: str) -> MidpointRole:
|
|
262
|
-
return MidpointRole(self.__get_object(MidpointObjectTypes.ROLE.value, oid))
|
|
263
|
-
|
|
264
|
-
def get_org(self, oid: str) -> MidpointOrganization:
|
|
265
|
-
return MidpointOrganization(self.__get_object(MidpointObjectTypes.ORG.value, oid))
|
|
266
|
-
|
|
267
|
-
def get_shadow(self, oid: str) -> MidpointShadow:
|
|
268
|
-
return MidpointShadow(self.__get_object(MidpointObjectTypes.SHADOW.value, oid))
|
|
269
|
-
|
|
270
199
|
def task_action(self, task_oid: str, task_action: str, progress_monitor: ProgressMonitor) -> Optional[MidpointTask]:
|
|
271
200
|
self.api_client.execute_action(MidpointObjectTypes.TASK.value, task_oid, task_action)
|
|
272
201
|
|
|
@@ -336,23 +265,3 @@ class MidpointClient:
|
|
|
336
265
|
|
|
337
266
|
def delete(self, midpoint_type: MidpointObjectType, oid: str) -> str:
|
|
338
267
|
return self.api_client.delete(midpoint_type, oid)
|
|
339
|
-
|
|
340
|
-
def get_system_info(self) -> dict[str, str]:
|
|
341
|
-
import json
|
|
342
|
-
|
|
343
|
-
raw = self.api_client.get_endpoint('actuator/info')
|
|
344
|
-
data = json.loads(raw)
|
|
345
|
-
|
|
346
|
-
info: dict[str, str] = {}
|
|
347
|
-
if 'version' in data:
|
|
348
|
-
info['Midpoint Version'] = data['version']
|
|
349
|
-
if 'build' in data:
|
|
350
|
-
info['Build'] = data['build']
|
|
351
|
-
if 'branch' in data:
|
|
352
|
-
info['Branch'] = data['branch']
|
|
353
|
-
if 'buildTimestamp' in data:
|
|
354
|
-
info['Build Timestamp'] = data['buildTimestamp']
|
|
355
|
-
if 'license' in data:
|
|
356
|
-
info['License'] = data['license']
|
|
357
|
-
|
|
358
|
-
return info
|
|
@@ -2,9 +2,9 @@ import itertools
|
|
|
2
2
|
import re
|
|
3
3
|
from collections import OrderedDict
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
from datetime import datetime
|
|
5
|
+
from datetime import datetime
|
|
6
6
|
from enum import Enum
|
|
7
|
-
from typing import Callable, Optional
|
|
7
|
+
from typing import Callable, Optional
|
|
8
8
|
from xml.etree.ElementTree import Element
|
|
9
9
|
|
|
10
10
|
from rich.console import Group
|
|
@@ -32,9 +32,6 @@ namespaces = {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
_T = TypeVar('_T', bound='MidpointObject')
|
|
36
|
-
|
|
37
|
-
|
|
38
35
|
class MidpointObject(OrderedDict):
|
|
39
36
|
def get_oid(self) -> Optional[str]:
|
|
40
37
|
oid = self.get('OID')
|
|
@@ -44,14 +41,6 @@ class MidpointObject(OrderedDict):
|
|
|
44
41
|
name = self.get('Name')
|
|
45
42
|
return str(name) if name is not None else None
|
|
46
43
|
|
|
47
|
-
@classmethod
|
|
48
|
-
def error_stub(cls: type[_T], oid: str) -> _T:
|
|
49
|
-
obj = cls.__new__(cls)
|
|
50
|
-
OrderedDict.__init__(obj)
|
|
51
|
-
obj['OID'] = oid
|
|
52
|
-
obj['Status'] = 'fetch error'
|
|
53
|
-
return obj
|
|
54
|
-
|
|
55
44
|
|
|
56
45
|
class MidpointObjectList(list[MidpointObject]):
|
|
57
46
|
def find_object(self, search_reference) -> Optional[MidpointObject]:
|
|
@@ -62,23 +51,21 @@ class MidpointObjectList(list[MidpointObject]):
|
|
|
62
51
|
return None
|
|
63
52
|
|
|
64
53
|
def filter(self, queryterms: list[str]):
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
mandatory_terms = [t[1:] for t in queryterms if t.startswith('+')]
|
|
68
|
-
optional_terms = [t for t in queryterms if not t.startswith('+')]
|
|
54
|
+
selected_users = MidpointObjectList()
|
|
69
55
|
|
|
70
|
-
for
|
|
71
|
-
|
|
56
|
+
for user in self:
|
|
57
|
+
selected = False
|
|
72
58
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
59
|
+
for uservalue in user.values():
|
|
60
|
+
if uservalue is not None:
|
|
61
|
+
for term in queryterms:
|
|
62
|
+
if unidecode(term).casefold() in unidecode(uservalue).casefold():
|
|
63
|
+
selected = True
|
|
77
64
|
|
|
78
|
-
if
|
|
79
|
-
|
|
65
|
+
if selected:
|
|
66
|
+
selected_users.append(user)
|
|
80
67
|
|
|
81
|
-
return
|
|
68
|
+
return selected_users
|
|
82
69
|
|
|
83
70
|
|
|
84
71
|
class MidpointTask(MidpointObject):
|
|
@@ -138,7 +125,7 @@ class MidpointTask(MidpointObject):
|
|
|
138
125
|
|
|
139
126
|
actions_executed = statistics.find('c:actionsExecuted', namespaces)
|
|
140
127
|
|
|
141
|
-
for object_actions_entry in actions_executed
|
|
128
|
+
for object_actions_entry in actions_executed or []:
|
|
142
129
|
object_type = safe_get_text(object_actions_entry.find('c:objectType', namespaces))
|
|
143
130
|
operation = safe_get_text(object_actions_entry.find('c:operation', namespaces))
|
|
144
131
|
count_success = safe_get_int(object_actions_entry.find('c:totalSuccessCount', namespaces))
|
|
@@ -221,9 +208,11 @@ class MidpointTask(MidpointObject):
|
|
|
221
208
|
if self.get('Execution Status') in ['RUNNING', 'RUNNABLE']:
|
|
222
209
|
# Get now in matching timezone awareness
|
|
223
210
|
if start_dt.tzinfo is not None and start_dt.tzinfo.utcoffset(start_dt) is not None:
|
|
211
|
+
from datetime import timezone
|
|
212
|
+
|
|
224
213
|
now = datetime.now(timezone.utc)
|
|
225
214
|
else:
|
|
226
|
-
now = datetime.
|
|
215
|
+
now = datetime.utcnow()
|
|
227
216
|
duration_seconds = (now - start_dt).total_seconds()
|
|
228
217
|
return self._format_duration(duration_seconds)
|
|
229
218
|
|
|
@@ -342,53 +331,14 @@ class MidpointUser(MidpointObject):
|
|
|
342
331
|
|
|
343
332
|
self._all_attributes = []
|
|
344
333
|
|
|
345
|
-
for child in itertools.chain(
|
|
346
|
-
xml_entity,
|
|
347
|
-
activation if activation is not None else [],
|
|
348
|
-
extfields if extfields is not None else [],
|
|
349
|
-
):
|
|
334
|
+
for child in itertools.chain(xml_entity, activation or [], extfields or []):
|
|
350
335
|
# Check if the child has no children and contains non-empty text
|
|
351
336
|
if len(child) == 0 and (child.text is not None and child.text.strip()):
|
|
352
337
|
self._all_attributes.append((self.strip_namespace(child.tag), child.text))
|
|
353
338
|
|
|
354
|
-
self._role_assignment_oids: list[str] = []
|
|
355
|
-
self._role_assignment_id_map: dict[str, str] = {}
|
|
356
|
-
self._org_assignment_oids: list[str] = []
|
|
357
|
-
for assignment in xml_entity.findall('c:assignment', namespaces):
|
|
358
|
-
target_ref = assignment.find('c:targetRef', namespaces)
|
|
359
|
-
if target_ref is not None:
|
|
360
|
-
ref_oid = target_ref.attrib.get('oid')
|
|
361
|
-
ref_type = target_ref.attrib.get('type', '')
|
|
362
|
-
if ref_oid:
|
|
363
|
-
if 'RoleType' in ref_type:
|
|
364
|
-
self._role_assignment_oids.append(ref_oid)
|
|
365
|
-
assignment_id = assignment.attrib.get('id')
|
|
366
|
-
if assignment_id:
|
|
367
|
-
self._role_assignment_id_map[ref_oid] = assignment_id
|
|
368
|
-
elif 'OrgType' in ref_type:
|
|
369
|
-
self._org_assignment_oids.append(ref_oid)
|
|
370
|
-
|
|
371
|
-
self._link_ref_oids: list[str] = []
|
|
372
|
-
for link_ref in xml_entity.findall('c:linkRef', namespaces):
|
|
373
|
-
ref_oid = link_ref.attrib.get('oid')
|
|
374
|
-
if ref_oid:
|
|
375
|
-
self._link_ref_oids.append(ref_oid)
|
|
376
|
-
|
|
377
339
|
def get_all_attributes(self):
|
|
378
340
|
return self._all_attributes
|
|
379
341
|
|
|
380
|
-
def get_role_assignment_oids(self) -> list[str]:
|
|
381
|
-
return self._role_assignment_oids
|
|
382
|
-
|
|
383
|
-
def get_role_assignment_id(self, role_oid: str) -> Optional[str]:
|
|
384
|
-
return self._role_assignment_id_map.get(role_oid)
|
|
385
|
-
|
|
386
|
-
def get_org_assignment_oids(self) -> list[str]:
|
|
387
|
-
return self._org_assignment_oids
|
|
388
|
-
|
|
389
|
-
def get_link_ref_oids(self) -> list[str]:
|
|
390
|
-
return self._link_ref_oids
|
|
391
|
-
|
|
392
342
|
|
|
393
343
|
class MidpointOrganization(MidpointObject):
|
|
394
344
|
def __init__(self, xml_entity: Element):
|
|
@@ -401,28 +351,6 @@ class MidpointOrganization(MidpointObject):
|
|
|
401
351
|
self['Parent'] = None if parentorg is None else parentorg.attrib['oid']
|
|
402
352
|
|
|
403
353
|
|
|
404
|
-
class MidpointRole(MidpointObject):
|
|
405
|
-
def __init__(self, xml_entity: Element):
|
|
406
|
-
super().__init__()
|
|
407
|
-
self['OID'] = xml_entity.attrib['oid']
|
|
408
|
-
self['Name'] = safe_get_text(xml_entity.find('c:name', namespaces))
|
|
409
|
-
self['DisplayName'] = optional_text(xml_entity.find('c:displayName', namespaces))
|
|
410
|
-
self['Description'] = optional_text(xml_entity.find('c:description', namespaces))
|
|
411
|
-
self['Status'] = safe_get_text(xml_entity.find('c:activation/c:effectiveStatus', namespaces))
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
class MidpointShadow(MidpointObject):
|
|
415
|
-
def __init__(self, xml_entity: Element):
|
|
416
|
-
super().__init__()
|
|
417
|
-
self['OID'] = xml_entity.attrib['oid']
|
|
418
|
-
self['Name'] = safe_get_text(xml_entity.find('c:name', namespaces))
|
|
419
|
-
resource_ref = xml_entity.find('c:resourceRef', namespaces)
|
|
420
|
-
self['ResourceOID'] = resource_ref.attrib['oid'] if resource_ref is not None else None
|
|
421
|
-
self['Kind'] = optional_text(xml_entity.find('c:kind', namespaces))
|
|
422
|
-
self['Intent'] = optional_text(xml_entity.find('c:intent', namespaces))
|
|
423
|
-
self['Status'] = safe_get_text(xml_entity.find('c:activation/c:effectiveStatus', namespaces))
|
|
424
|
-
|
|
425
|
-
|
|
426
354
|
@dataclass
|
|
427
355
|
class MidpointObjectType:
|
|
428
356
|
object_type: Optional[str]
|
|
@@ -524,7 +452,9 @@ class MidpointObjectTypes(Enum):
|
|
|
524
452
|
valid_types.append(mp_type.value.tagname)
|
|
525
453
|
|
|
526
454
|
valid_types_str = ', '.join(sorted(valid_types))
|
|
527
|
-
raise MidpointTypeNotFound(
|
|
455
|
+
raise MidpointTypeNotFound(
|
|
456
|
+
f'No type found for value "{name}". Valid types are: {valid_types_str}'
|
|
457
|
+
)
|
|
528
458
|
|
|
529
459
|
|
|
530
460
|
def optional_text(node: Optional[Element]) -> Optional[str]:
|
|
@@ -14,11 +14,9 @@ from midpoint_cli.client.objects import MidpointObjectList
|
|
|
14
14
|
from midpoint_cli.prompt.console import get_console, print_error
|
|
15
15
|
from midpoint_cli.prompt.delete import DeleteClientPrompt
|
|
16
16
|
from midpoint_cli.prompt.get import GetClientPrompt
|
|
17
|
-
from midpoint_cli.prompt.info import InfoClientPrompt
|
|
18
17
|
from midpoint_cli.prompt.org import OrgClientPrompt
|
|
19
18
|
from midpoint_cli.prompt.put import PutClientPrompt
|
|
20
19
|
from midpoint_cli.prompt.resource import ResourceClientPrompt
|
|
21
|
-
from midpoint_cli.prompt.role import RoleClientPrompt
|
|
22
20
|
from midpoint_cli.prompt.script import ScriptClientPrompt
|
|
23
21
|
from midpoint_cli.prompt.task import TaskClientPrompt
|
|
24
22
|
from midpoint_cli.prompt.user import UserClientPrompt
|
|
@@ -32,7 +30,6 @@ class MidpointCompleter(Completer):
|
|
|
32
30
|
self.commands = {
|
|
33
31
|
'exit': 'Exit the shell',
|
|
34
32
|
'help': 'Show help',
|
|
35
|
-
'info': 'Show server information',
|
|
36
33
|
'get': 'Get a server object',
|
|
37
34
|
'put': 'Create/Update a server object',
|
|
38
35
|
'delete': 'Delete a server object',
|
|
@@ -44,31 +41,24 @@ class MidpointCompleter(Completer):
|
|
|
44
41
|
'users': 'List all server users',
|
|
45
42
|
'org': 'Manage organizations',
|
|
46
43
|
'orgs': 'List all server organizations',
|
|
47
|
-
'role': 'Manage roles',
|
|
48
|
-
'roles': 'List all server roles',
|
|
49
44
|
'script': 'Execute server-side scripts',
|
|
50
45
|
}
|
|
51
46
|
|
|
52
47
|
# Subcommands for each main command
|
|
53
48
|
self.subcommands = {
|
|
54
49
|
'task': ['ls', 'get', 'run', 'wait', 'suspend', 'resume'],
|
|
55
|
-
'user': ['ls', 'search', 'get'
|
|
50
|
+
'user': ['ls', 'search', 'get'],
|
|
56
51
|
'org': ['ls', 'search'],
|
|
57
|
-
'role': ['ls', 'search'],
|
|
58
52
|
'resource': ['ls', 'test'],
|
|
59
53
|
}
|
|
60
54
|
|
|
61
55
|
# Client for dynamic completions
|
|
62
56
|
self.client = client
|
|
63
57
|
|
|
64
|
-
# Cache for task
|
|
58
|
+
# Cache for task list (to avoid repeated API calls)
|
|
65
59
|
self._task_cache: Optional[MidpointObjectList] = None
|
|
66
60
|
self._task_cache_time: float = 0.0
|
|
67
61
|
self._task_cache_ttl = 5 # Cache for 5 seconds
|
|
68
|
-
self._user_cache: Optional[MidpointObjectList] = None
|
|
69
|
-
self._user_cache_time: float = 0.0
|
|
70
|
-
self._role_cache: Optional[MidpointObjectList] = None
|
|
71
|
-
self._role_cache_time: float = 0.0
|
|
72
62
|
|
|
73
63
|
def _get_tasks(self):
|
|
74
64
|
"""Get tasks with caching to avoid repeated API calls."""
|
|
@@ -90,40 +80,6 @@ class MidpointCompleter(Completer):
|
|
|
90
80
|
# If fetching fails, return empty list (don't break autocomplete)
|
|
91
81
|
return []
|
|
92
82
|
|
|
93
|
-
def _get_users(self):
|
|
94
|
-
"""Get users with caching to avoid repeated API calls."""
|
|
95
|
-
if self.client is None:
|
|
96
|
-
return []
|
|
97
|
-
|
|
98
|
-
current_time = time.time()
|
|
99
|
-
if self._user_cache is not None and (current_time - self._user_cache_time) < self._task_cache_ttl:
|
|
100
|
-
return self._user_cache
|
|
101
|
-
|
|
102
|
-
try:
|
|
103
|
-
users = self.client.get_users()
|
|
104
|
-
self._user_cache = users
|
|
105
|
-
self._user_cache_time = current_time
|
|
106
|
-
return users
|
|
107
|
-
except Exception:
|
|
108
|
-
return []
|
|
109
|
-
|
|
110
|
-
def _get_roles(self):
|
|
111
|
-
"""Get roles with caching to avoid repeated API calls."""
|
|
112
|
-
if self.client is None:
|
|
113
|
-
return []
|
|
114
|
-
|
|
115
|
-
current_time = time.time()
|
|
116
|
-
if self._role_cache is not None and (current_time - self._role_cache_time) < self._task_cache_ttl:
|
|
117
|
-
return self._role_cache
|
|
118
|
-
|
|
119
|
-
try:
|
|
120
|
-
roles = self.client.get_roles()
|
|
121
|
-
self._role_cache = roles
|
|
122
|
-
self._role_cache_time = current_time
|
|
123
|
-
return roles
|
|
124
|
-
except Exception:
|
|
125
|
-
return []
|
|
126
|
-
|
|
127
83
|
def get_completions(self, document, complete_event):
|
|
128
84
|
text_before_cursor = document.text_before_cursor
|
|
129
85
|
words = text_before_cursor.split()
|
|
@@ -146,32 +102,6 @@ class MidpointCompleter(Completer):
|
|
|
146
102
|
if subcmd.startswith(word.lower()):
|
|
147
103
|
yield Completion(subcmd, start_position=-len(word))
|
|
148
104
|
|
|
149
|
-
# Complete user assign/unassign arguments
|
|
150
|
-
elif main_cmd == 'user' and len(words) >= 2:
|
|
151
|
-
subcmd = words[1]
|
|
152
|
-
if subcmd in ['assign', 'unassign']:
|
|
153
|
-
if text_before_cursor.endswith(' '):
|
|
154
|
-
word = ''
|
|
155
|
-
else:
|
|
156
|
-
word = words[-1] if words else ''
|
|
157
|
-
|
|
158
|
-
# Third position: complete user name
|
|
159
|
-
if len(words) == 2 or (len(words) == 3 and not text_before_cursor.endswith(' ')):
|
|
160
|
-
actual_word = words[2] if len(words) == 3 else ''
|
|
161
|
-
for u in self._get_users():
|
|
162
|
-
name = u.get_name()
|
|
163
|
-
oid = u.get_oid()
|
|
164
|
-
if name and actual_word.lower() in name.lower():
|
|
165
|
-
yield Completion(name, start_position=-len(actual_word), display_meta=f'OID: {oid}')
|
|
166
|
-
# Fourth position: complete role name
|
|
167
|
-
elif len(words) == 3 or (len(words) == 4 and not text_before_cursor.endswith(' ')):
|
|
168
|
-
actual_word = words[3] if len(words) == 4 else ''
|
|
169
|
-
for r in self._get_roles():
|
|
170
|
-
name = r.get_name()
|
|
171
|
-
oid = r.get_oid()
|
|
172
|
-
if name and actual_word.lower() in name.lower():
|
|
173
|
-
yield Completion(name, start_position=-len(actual_word), display_meta=f'OID: {oid}')
|
|
174
|
-
|
|
175
105
|
# Complete task arguments for task subcommands
|
|
176
106
|
elif main_cmd == 'task' and len(words) >= 2:
|
|
177
107
|
subcmd = words[1]
|
|
@@ -211,9 +141,7 @@ class MidpointClientPrompt(
|
|
|
211
141
|
ResourceClientPrompt,
|
|
212
142
|
UserClientPrompt,
|
|
213
143
|
OrgClientPrompt,
|
|
214
|
-
RoleClientPrompt,
|
|
215
144
|
ScriptClientPrompt,
|
|
216
|
-
InfoClientPrompt,
|
|
217
145
|
):
|
|
218
146
|
def __init__(self, client: MidpointClient):
|
|
219
147
|
Cmd.__init__(self)
|
|
@@ -11,11 +11,3 @@ class PromptBase:
|
|
|
11
11
|
self.client: MidpointClient
|
|
12
12
|
self.error_code: Optional[int] = None
|
|
13
13
|
self.error_message: Optional[str] = None
|
|
14
|
-
|
|
15
|
-
@staticmethod
|
|
16
|
-
def _subcommand_help(args, subparsers) -> bool:
|
|
17
|
-
"""If -h/--help follows a known subcommand, print that subcommand's help and return True."""
|
|
18
|
-
if args and args[0] in subparsers and any(a in ('-h', '--help') for a in args[1:]):
|
|
19
|
-
subparsers[args[0]].print_help()
|
|
20
|
-
return True
|
|
21
|
-
return False
|