midpoint-cli 1.6.0.dev3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. midpoint_cli/__init__.py +234 -0
  2. midpoint_cli/client/__init__.py +199 -0
  3. midpoint_cli/client/audit.py +178 -0
  4. midpoint_cli/client/audit_query.py +247 -0
  5. midpoint_cli/client/configuration.py +15 -0
  6. midpoint_cli/client/constants.py +37 -0
  7. midpoint_cli/client/errors.py +74 -0
  8. midpoint_cli/client/logging_config.py +122 -0
  9. midpoint_cli/client/midpoint_client.py +866 -0
  10. midpoint_cli/client/object_query.py +230 -0
  11. midpoint_cli/client/objects.py +814 -0
  12. midpoint_cli/client/observer.py +29 -0
  13. midpoint_cli/client/patch.py +113 -0
  14. midpoint_cli/client/progress.py +80 -0
  15. midpoint_cli/client/rest.py +490 -0
  16. midpoint_cli/client/safe_xml.py +24 -0
  17. midpoint_cli/client/server_info.py +130 -0
  18. midpoint_cli/client/server_version.py +74 -0
  19. midpoint_cli/client/session.py +142 -0
  20. midpoint_cli/client/task_definitions.py +110 -0
  21. midpoint_cli/prompt/__init__.py +196 -0
  22. midpoint_cli/prompt/audit.py +253 -0
  23. midpoint_cli/prompt/base.py +218 -0
  24. midpoint_cli/prompt/complete.py +291 -0
  25. midpoint_cli/prompt/configuration.py +258 -0
  26. midpoint_cli/prompt/connector.py +45 -0
  27. midpoint_cli/prompt/console.py +818 -0
  28. midpoint_cli/prompt/delete.py +89 -0
  29. midpoint_cli/prompt/get.py +66 -0
  30. midpoint_cli/prompt/info.py +53 -0
  31. midpoint_cli/prompt/log.py +98 -0
  32. midpoint_cli/prompt/org.py +122 -0
  33. midpoint_cli/prompt/output.py +33 -0
  34. midpoint_cli/prompt/put.py +71 -0
  35. midpoint_cli/prompt/report.py +69 -0
  36. midpoint_cli/prompt/resource.py +217 -0
  37. midpoint_cli/prompt/role.py +96 -0
  38. midpoint_cli/prompt/script.py +154 -0
  39. midpoint_cli/prompt/search.py +51 -0
  40. midpoint_cli/prompt/shadow.py +87 -0
  41. midpoint_cli/prompt/task.py +296 -0
  42. midpoint_cli/prompt/user.py +406 -0
  43. midpoint_cli-1.6.0.dev3.dist-info/METADATA +1334 -0
  44. midpoint_cli-1.6.0.dev3.dist-info/RECORD +47 -0
  45. midpoint_cli-1.6.0.dev3.dist-info/WHEEL +4 -0
  46. midpoint_cli-1.6.0.dev3.dist-info/entry_points.txt +3 -0
  47. midpoint_cli-1.6.0.dev3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,406 @@
1
+ import getpass
2
+ import os
3
+ import sys
4
+ from argparse import ArgumentParser, Namespace, RawTextHelpFormatter
5
+ from typing import Any
6
+
7
+ from midpoint_cli.client import (
8
+ MidpointObjectList,
9
+ MidpointObjectType,
10
+ MidpointObjectTypes,
11
+ MidpointOrganization,
12
+ MidpointRole,
13
+ MidpointServerError,
14
+ MidpointShadow,
15
+ MidpointUser,
16
+ )
17
+ from midpoint_cli.client.objects import MidpointObject
18
+
19
+ # User command wrapper parser
20
+ from midpoint_cli.prompt.base import PromptBase, add_listing_arguments
21
+ from midpoint_cli.prompt.complete import ROLE, USER, completes
22
+ from midpoint_cli.prompt.console import (
23
+ create_table,
24
+ get_console,
25
+ is_machine_output,
26
+ print_details,
27
+ print_error,
28
+ print_info,
29
+ print_rows,
30
+ )
31
+
32
+ user_parser = ArgumentParser(
33
+ formatter_class=RawTextHelpFormatter,
34
+ prog='user',
35
+ description='Manage server users.',
36
+ epilog="""
37
+ Available commands:
38
+ ls List all users.
39
+ search Search for a user.
40
+ get Get user details by OID or name.
41
+ assign Assign a role, org, archetype or service to a user.
42
+ unassign Remove a role, org, archetype or service assignment from a user.
43
+ enable Enable users.
44
+ disable Disable users.
45
+ passwd Set the password of a user.
46
+ """,
47
+ )
48
+ user_parser.add_argument('command', help='User command to execute.')
49
+ user_parser.add_argument('arg', help='Optional command arguments.', nargs='*')
50
+
51
+ user_ls_parser = add_listing_arguments(
52
+ ArgumentParser(
53
+ formatter_class=RawTextHelpFormatter,
54
+ prog='user ls',
55
+ description='List users.',
56
+ ),
57
+ 'users',
58
+ )
59
+
60
+ # User search parser
61
+
62
+ user_search_parser = ArgumentParser(
63
+ formatter_class=RawTextHelpFormatter,
64
+ prog='user search',
65
+ description='Search for users by substring of their name, full name or email address, ignoring case.\n'
66
+ 'Terms are combined with OR. Prefix a term with + to make it mandatory (AND).',
67
+ )
68
+ user_search_parser.add_argument(
69
+ 'searchquery',
70
+ help='String fragments to search in user data. Use +term to require a match (AND); plain terms are optional (OR).',
71
+ nargs='+',
72
+ )
73
+
74
+ user_get_parser = ArgumentParser(
75
+ formatter_class=RawTextHelpFormatter,
76
+ prog='user get',
77
+ description='Search for users by OID.',
78
+ )
79
+ completes(user_get_parser.add_argument('oid', help='An OID or name value.'), USER)
80
+
81
+ # The assignment target types `user assign/unassign` accept, by option.
82
+ ASSIGNMENT_TARGET_TYPES: dict[str, tuple[MidpointObjectType, str]] = {
83
+ 'role': (MidpointObjectTypes.ROLE.value, 'Role'),
84
+ 'org': (MidpointObjectTypes.ORG.value, 'Org'),
85
+ 'archetype': (MidpointObjectTypes.ARCHETYPE.value, 'Archetype'),
86
+ 'service': (MidpointObjectTypes.SERVICE.value, 'Service'),
87
+ }
88
+
89
+
90
+ def _assignment_parser(prog: str, description: str) -> ArgumentParser:
91
+ parser = ArgumentParser(
92
+ formatter_class=RawTextHelpFormatter,
93
+ prog=prog,
94
+ description=description,
95
+ epilog='The target is a role unless --org, --archetype or --service says otherwise.',
96
+ )
97
+ completes(parser.add_argument('user', help='User OID or name.'), USER)
98
+ completes(
99
+ parser.add_argument('target', help='Role (or org, archetype, service) OID or name.'),
100
+ ROLE,
101
+ )
102
+ target_type = parser.add_mutually_exclusive_group()
103
+ for option in ASSIGNMENT_TARGET_TYPES:
104
+ article = 'an' if option[0] in 'aeiou' else 'a'
105
+ target_type.add_argument(
106
+ f'--{option}',
107
+ dest='target_type',
108
+ action='store_const',
109
+ const=option,
110
+ help=f'The target is {article} {option}.',
111
+ )
112
+ parser.set_defaults(target_type='role')
113
+ return parser
114
+
115
+
116
+ user_assign_parser = _assignment_parser('user assign', 'Assign a role, org, archetype or service to a user.')
117
+ user_unassign_parser = _assignment_parser(
118
+ 'user unassign', 'Remove the assignment of a role, org, archetype or service from a user.'
119
+ )
120
+
121
+ user_enable_parser = ArgumentParser(
122
+ formatter_class=RawTextHelpFormatter,
123
+ prog='user enable',
124
+ description='Enable users: set their administrative status to enabled.',
125
+ )
126
+ completes(user_enable_parser.add_argument('user', help='User OID or name.', nargs='+'), USER)
127
+
128
+ user_disable_parser = ArgumentParser(
129
+ formatter_class=RawTextHelpFormatter,
130
+ prog='user disable',
131
+ description='Disable users: set their administrative status to disabled.',
132
+ )
133
+ completes(user_disable_parser.add_argument('user', help='User OID or name.', nargs='+'), USER)
134
+
135
+ user_passwd_parser = ArgumentParser(
136
+ formatter_class=RawTextHelpFormatter,
137
+ prog='user passwd',
138
+ description='Set the password of a user.',
139
+ epilog='On a terminal, the new password is asked twice and not echoed. Otherwise it is read from\n'
140
+ 'the first line of standard input, e.g. printf \'%s\\n\' "$PW" | midpoint-cli user passwd jdoe\n'
141
+ '(in a command script piped to midpoint-cli, the line after the command).\n'
142
+ 'The password is never printed or logged; the server checks it against its password policy.',
143
+ )
144
+ completes(user_passwd_parser.add_argument('user', help='User OID or name.'), USER)
145
+ user_passwd_parser.add_argument(
146
+ '--password-env',
147
+ metavar='VAR',
148
+ help='Read the new password from the environment variable VAR instead.',
149
+ )
150
+
151
+ # Each subcommand with its parser; None for those taking no arguments.
152
+ user_subparsers: dict[str, ArgumentParser | None] = {
153
+ 'ls': user_ls_parser,
154
+ 'search': user_search_parser,
155
+ 'get': user_get_parser,
156
+ 'assign': user_assign_parser,
157
+ 'unassign': user_unassign_parser,
158
+ 'enable': user_enable_parser,
159
+ 'disable': user_disable_parser,
160
+ 'passwd': user_passwd_parser,
161
+ }
162
+
163
+ # The records shown with a user, by key in the machine formats, with their table title and columns.
164
+ USER_SECTION_TITLES = {'Roles': 'Role Assignments', 'Orgs': 'Org Memberships', 'Projections': 'Projections'}
165
+ USER_SECTION_COLUMNS: dict[str, tuple[str, ...]] = {
166
+ 'Roles': ('OID', 'Name', 'Status'),
167
+ 'Orgs': ('OID', 'Name', 'Status'),
168
+ 'Projections': ('Shadow OID', 'Resource', 'Account Name', 'Kind', 'Intent'),
169
+ }
170
+ _SECTION_COLUMN_STYLES = {
171
+ 'OID': 'blue',
172
+ 'Shadow OID': 'blue',
173
+ 'Name': 'bright_cyan',
174
+ 'Resource': 'bright_cyan',
175
+ 'Account Name': 'bright_cyan',
176
+ }
177
+
178
+
179
+ def _text(value: object) -> str:
180
+ return '' if value is None else str(value)
181
+
182
+
183
+ def _assignment_row(target: MidpointObject) -> dict[str, str]:
184
+ """A role or org of a user, as a row of its section."""
185
+ return {
186
+ 'OID': _text(target.get('OID')),
187
+ 'Name': _text(target.get('DisplayName') or target.get('Name')),
188
+ 'Status': _text(target.get('Status')),
189
+ }
190
+
191
+
192
+ def _projection_row(shadow: MidpointShadow) -> dict[str, str]:
193
+ """A shadow linked to a user, as a row of the Projections section."""
194
+ return {
195
+ 'Shadow OID': _text(shadow.get('OID')),
196
+ 'Resource': _text(shadow.get('ResourceName') or shadow.get('ResourceOID')),
197
+ 'Account Name': _text(shadow.get('Name')),
198
+ 'Kind': _text(shadow.get('Kind')),
199
+ 'Intent': _text(shadow.get('Intent')),
200
+ }
201
+
202
+
203
+ def user_details(user: MidpointUser, sections: dict[str, list[dict[str, str]]]) -> dict[str, Any]:
204
+ """A user as one detail object: its OID, every attribute shown in the table, then its sections.
205
+
206
+ An attribute present several times (a multi-valued property) becomes a list of values.
207
+ """
208
+ details: dict[str, Any] = {'OID': user.get_oid()}
209
+ for name, value in user.get_all_attributes():
210
+ if name not in details:
211
+ details[name] = value
212
+ elif isinstance(details[name], list):
213
+ details[name].append(value)
214
+ else:
215
+ details[name] = [details[name], value]
216
+ details.update(sections)
217
+ return details
218
+
219
+
220
+ class UserClientPrompt(PromptBase):
221
+ subparsers_user = user_subparsers
222
+
223
+ def do_user(self, inp: str) -> None:
224
+ """Manage users (subcommands: ls, search, get, assign, unassign, enable, disable, passwd)."""
225
+ self._dispatch_subcommand(
226
+ inp,
227
+ user_parser,
228
+ user_subparsers,
229
+ {
230
+ 'ls': self._user_ls,
231
+ 'search': self._user_search,
232
+ 'get': self._user_get,
233
+ 'assign': lambda args: self._assignment(True, user_assign_parser.parse_args(args)),
234
+ 'unassign': lambda args: self._assignment(False, user_unassign_parser.parse_args(args)),
235
+ 'enable': lambda args: self._set_enabled(True, user_enable_parser.parse_args(args).user),
236
+ 'disable': lambda args: self._set_enabled(False, user_disable_parser.parse_args(args).user),
237
+ 'passwd': self._user_passwd,
238
+ },
239
+ )
240
+
241
+ def _user_ls(self, args: list[str]) -> None:
242
+ ls_ns = user_ls_parser.parse_args(args)
243
+ self.print_users(self.client.get_users(limit=ls_ns.limit, name_filter=ls_ns.filter))
244
+
245
+ def _user_search(self, args: list[str]) -> None:
246
+ search_ns = user_search_parser.parse_args(args)
247
+ self.print_users(self.client.search_users(search_ns.searchquery))
248
+
249
+ def _user_get(self, args: list[str]) -> None:
250
+ get_ns = user_get_parser.parse_args(args)
251
+ user = self.client.get_user(self.client.resolve_oid(MidpointObjectTypes.USER.value, get_ns.oid))
252
+
253
+ roles: list[MidpointRole] = []
254
+ for oid in user.get_role_assignment_oids():
255
+ try:
256
+ roles.append(self.client.get_role(oid))
257
+ except MidpointServerError:
258
+ roles.append(MidpointRole.error_stub(oid))
259
+
260
+ orgs: list[MidpointOrganization] = []
261
+ for oid in user.get_org_assignment_oids():
262
+ try:
263
+ orgs.append(self.client.get_org(oid))
264
+ except MidpointServerError:
265
+ orgs.append(MidpointOrganization.error_stub(oid))
266
+
267
+ shadows: list[MidpointShadow] = []
268
+ for oid in user.get_link_ref_oids():
269
+ try:
270
+ shadow = self.client.get_shadow(oid)
271
+ resource_oid = shadow.get('ResourceOID')
272
+ if resource_oid:
273
+ try:
274
+ resource = self.client.get_resource(resource_oid)
275
+ shadow['ResourceName'] = resource.get('Name') or resource_oid
276
+ except MidpointServerError:
277
+ shadow['ResourceName'] = resource_oid
278
+ shadows.append(shadow)
279
+ except MidpointServerError:
280
+ shadows.append(MidpointShadow.error_stub(oid))
281
+
282
+ self.print_user(user, roles=roles, orgs=orgs, shadows=shadows)
283
+
284
+ def _user_passwd(self, args: list[str]) -> None:
285
+ passwd_ns = user_passwd_parser.parse_args(args)
286
+ oid = self.client.resolve_oid(MidpointObjectTypes.USER.value, passwd_ns.user)
287
+ password = self._read_new_password(passwd_ns.password_env)
288
+ if password is None:
289
+ return
290
+ self.client.set_user_password(oid, password)
291
+ print_info(f'Password of user {passwd_ns.user} changed')
292
+
293
+ def _assignment(self, assign: bool, ns: Namespace) -> None:
294
+ """user assign/unassign: add or remove the assignment of a role, org, archetype or service."""
295
+ target_type, label = ASSIGNMENT_TARGET_TYPES[ns.target_type]
296
+ user_obj = self._require_object(MidpointObjectTypes.USER.value, ns.user, 'User')
297
+ if user_obj is None:
298
+ return
299
+ target_obj = self._require_object(target_type, ns.target, label)
300
+ if target_obj is None:
301
+ return
302
+ user_oid = user_obj.get_oid()
303
+ target_oid = target_obj.get_oid()
304
+ if user_oid is None or target_oid is None:
305
+ raise AssertionError
306
+ if assign:
307
+ self.client.assign(user_oid, target_type, target_oid)
308
+ print_info(f"{label} '{target_obj.get_name()}' assigned to '{user_obj.get_name()}'")
309
+ else:
310
+ self.client.unassign(user_oid, target_type, target_oid)
311
+ print_info(f"{label} '{target_obj.get_name()}' unassigned from '{user_obj.get_name()}'")
312
+
313
+ def _set_enabled(self, enabled: bool, references: list[str]) -> None:
314
+ """user enable/disable: set the administrative status of each user, reporting each failure."""
315
+ for reference in references:
316
+ user_obj = self._find_reference(MidpointObjectTypes.USER.value, reference, 'User')
317
+ if user_obj is None:
318
+ continue
319
+ user_oid = user_obj.get_oid()
320
+ if user_oid is None:
321
+ raise AssertionError
322
+ try:
323
+ self.client.set_user_enabled(user_oid, enabled)
324
+ print_info(f"User '{user_obj.get_name()}' {'enabled' if enabled else 'disabled'}")
325
+ except MidpointServerError as e:
326
+ self._fail()
327
+ print_error(str(e))
328
+
329
+ def _read_new_password(self, environment_variable: str | None) -> str | None:
330
+ """The new password for user passwd, or None after setting the error. It is never echoed."""
331
+ if environment_variable is not None:
332
+ password = os.environ.get(environment_variable)
333
+ if password is None:
334
+ self._fail(f'Environment variable {environment_variable} is not set; password not changed')
335
+ return None
336
+ elif sys.stdin.isatty():
337
+ password = getpass.getpass('New password: ')
338
+ if getpass.getpass('Retype new password: ') != password:
339
+ self._fail('The passwords do not match; password not changed')
340
+ return None
341
+ else:
342
+ line = sys.stdin.readline()
343
+ if not line:
344
+ self._fail('No password on standard input; password not changed')
345
+ return None
346
+ password = line.rstrip('\r\n')
347
+ if not password:
348
+ self._fail('Empty password refused; password not changed')
349
+ return None
350
+ return password
351
+
352
+ @staticmethod
353
+ def print_user(
354
+ user: MidpointUser,
355
+ roles: list[MidpointRole],
356
+ orgs: list[MidpointOrganization],
357
+ shadows: list[MidpointShadow],
358
+ ) -> None:
359
+ """Print a single user's details with related records, in the selected output format."""
360
+ sections = {
361
+ 'Roles': [_assignment_row(role) for role in roles],
362
+ 'Orgs': [_assignment_row(org) for org in orgs],
363
+ 'Projections': [_projection_row(shadow) for shadow in shadows],
364
+ }
365
+ if is_machine_output():
366
+ print_details([user_details(user, sections)], as_list=False)
367
+ return
368
+
369
+ console = get_console()
370
+ table = create_table(title='User Details')
371
+ table.add_column('Attribute', style='cyan')
372
+ table.add_column('Value', style='white')
373
+
374
+ for attr in user.get_all_attributes():
375
+ table.add_row(str(attr[0]), str(attr[1]))
376
+
377
+ console.print(table)
378
+
379
+ for key, title in USER_SECTION_TITLES.items():
380
+ section_table = create_table(title=title)
381
+ columns = USER_SECTION_COLUMNS[key]
382
+ for column in columns:
383
+ section_table.add_column(column, style=_SECTION_COLUMN_STYLES.get(column, 'white'))
384
+ for row in sections[key]:
385
+ section_table.add_row(*(row[column] for column in columns))
386
+ if not sections[key]:
387
+ # '(none)' in the name column.
388
+ section_table.add_row(*('(none)' if 'Name' in column else '' for column in columns))
389
+ console.print(section_table)
390
+
391
+ @staticmethod
392
+ def print_users(users: MidpointObjectList) -> None:
393
+ """Print a list of users in the selected output format."""
394
+ print_rows(users, title='Server Users', noun='users')
395
+
396
+ @staticmethod
397
+ def help_user() -> None:
398
+ user_parser.print_help()
399
+
400
+ def do_users(self, inp: str) -> None:
401
+ """Shortcut for 'user ls' — list all users."""
402
+ self._list_shortcut(inp, 'users', 'user', self.do_user)
403
+
404
+ @staticmethod
405
+ def help_users() -> None:
406
+ print('List all server users. This is a shortcut for "user ls"')