midpoint-cli 1.6.0__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.dist-info/METADATA +1334 -0
  44. midpoint_cli-1.6.0.dist-info/RECORD +47 -0
  45. midpoint_cli-1.6.0.dist-info/WHEEL +4 -0
  46. midpoint_cli-1.6.0.dist-info/entry_points.txt +3 -0
  47. midpoint_cli-1.6.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,29 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class MidpointCommunicationObserver(ABC):
5
+ @abstractmethod
6
+ def on_http_call(self):
7
+ pass
8
+
9
+ @abstractmethod
10
+ def on_http_error(self):
11
+ pass
12
+
13
+ @abstractmethod
14
+ def on_http_success(self):
15
+ pass
16
+
17
+ def on_http_failure(self): # noqa: B027
18
+ """A request failed for good: no answer once the retries, if any, were exhausted."""
19
+
20
+
21
+ class NopMidpointCommunicationObserver(MidpointCommunicationObserver):
22
+ def on_http_call(self):
23
+ pass
24
+
25
+ def on_http_error(self):
26
+ pass
27
+
28
+ def on_http_success(self):
29
+ pass
@@ -0,0 +1,113 @@
1
+ import contextlib
2
+ import errno
3
+ import os
4
+ import re
5
+ import sys
6
+
7
+ FOLLOW_SYMLINKS_ENV_VAR = 'MIDPOINT_CLI_FOLLOW_SYMLINKS'
8
+ _TRUE_VALUES = ('1', 'true', 'yes', 'on')
9
+
10
+
11
+ def _follow_symlinks_allowed() -> bool:
12
+ """Whether the user opted in to writing exports through a symbolic link."""
13
+ return os.environ.get(FOLLOW_SYMLINKS_ENV_VAR, '').strip().lower() in _TRUE_VALUES
14
+
15
+
16
+ def _symlink_refused(path: str) -> OSError:
17
+ message = (
18
+ f'{path} is a symbolic link; refusing to write through it '
19
+ f'(write to the link target instead, or set {FOLLOW_SYMLINKS_ENV_VAR}=1)'
20
+ )
21
+ return OSError(errno.ELOOP, message, path)
22
+
23
+
24
+ def private_file_opener(path: str, flags: int) -> int:
25
+ """Opener for open() that keeps the file readable by its owner only.
26
+
27
+ Exported and patched object XML can carry connector passwords and PII. A new file is
28
+ created with mode 0600 directly, so there is no window where it is world-readable; an
29
+ existing file is tightened to 0600 before anything is written to it. Owners who cannot
30
+ change the mode of an existing file (e.g. one owned by someone else) still get it written.
31
+
32
+ The file itself must not be a symbolic link: a link planted in a shared directory would
33
+ otherwise redirect the export to, and truncate, whatever file it points to. O_NOFOLLOW
34
+ enforces this atomically where the platform has it; elsewhere the path is checked just
35
+ before opening. Only the last path component is checked, so linked directories still work.
36
+ Setting MIDPOINT_CLI_FOLLOW_SYMLINKS=1 restores the old behaviour for deliberate links.
37
+ """
38
+ refuse_links = not _follow_symlinks_allowed()
39
+ if refuse_links:
40
+ nofollow = getattr(os, 'O_NOFOLLOW', 0)
41
+ if nofollow:
42
+ flags |= nofollow
43
+ elif os.path.islink(path):
44
+ raise _symlink_refused(path)
45
+ try:
46
+ fd = os.open(path, flags, 0o600)
47
+ except OSError as e:
48
+ # O_NOFOLLOW reports a link as ELOOP (EMLINK on some BSDs); name the actual cause.
49
+ if refuse_links and e.errno in (errno.ELOOP, errno.EMLINK) and os.path.islink(path):
50
+ raise _symlink_refused(path) from e
51
+ raise
52
+ if hasattr(os, 'fchmod'):
53
+ with contextlib.suppress(OSError):
54
+ os.fchmod(fd, 0o600)
55
+ return fd
56
+
57
+
58
+ def write_private_file(path: str, text: str) -> None:
59
+ """Write text to path, creating or truncating the file with mode 0600.
60
+
61
+ Raises OSError (with a descriptive strerror) when path is a symbolic link, unless
62
+ MIDPOINT_CLI_FOLLOW_SYMLINKS is set.
63
+ """
64
+ with open(path, 'w', opener=private_file_opener) as f:
65
+ f.write(text)
66
+
67
+
68
+ class PatchError(ValueError):
69
+ """A patch file line cannot be applied (e.g. it is not a valid regular expression)."""
70
+
71
+
72
+ def patch_from_string(body: str, patch: str) -> str:
73
+ """Apply a line-oriented patch to body and return the result.
74
+
75
+ Each non-blank line not starting with '#' is ``PATTERN => REPLACEMENT``; a line without
76
+ ' =>' deletes every match of PATTERN. Only the first ' =>' separates the two sides, so a
77
+ replacement may itself contain '=>'. Both sides are stripped of surrounding whitespace.
78
+
79
+ PATTERN is a Python regular expression and REPLACEMENT a re.sub template, on purpose:
80
+ patches can use constructs such as ``<!--.*`` and ``\\1``. Regex metacharacters ('.', '(',
81
+ '*', '\\', ...) therefore keep their regex meaning and must be escaped to match literally;
82
+ a backslash in a replacement value (e.g. a password) must be doubled. A line that matches
83
+ nothing prints a warning and leaves body unchanged. An invalid pattern or template raises
84
+ PatchError naming the offending line.
85
+ """
86
+ for line in patch.splitlines():
87
+ line = line.strip()
88
+ if line != '' and line[0] != '#':
89
+ items = line.split(' =>', 1)
90
+ if len(items) == 1:
91
+ pattern = items[0]
92
+ replacement = ''
93
+ else:
94
+ pattern, replacement = items
95
+ try:
96
+ body, subs = re.subn(pattern.strip(), replacement.strip(), body)
97
+ except re.error as e:
98
+ raise PatchError(f'invalid patch line {line!r}: {e}') from e
99
+
100
+ if subs == 0:
101
+ print('Warning: replacement not applied:', line, file=sys.stderr)
102
+
103
+ return body
104
+
105
+
106
+ def patch_from_file(xml_body: str, patch_file: str, patch_write: bool) -> str:
107
+ with open(patch_file) as f:
108
+ patch_text = f.read()
109
+ xml_body = patch_from_string(xml_body, patch_text)
110
+ if patch_write:
111
+ write_private_file(patch_file + '.xml', xml_body)
112
+
113
+ return xml_body
@@ -0,0 +1,80 @@
1
+ from abc import ABC, abstractmethod
2
+ from datetime import datetime
3
+
4
+
5
+ class ProgressMonitor(ABC):
6
+ """Abstract base class for task progress monitors."""
7
+
8
+ @abstractmethod
9
+ def update(self, progress: int) -> None:
10
+ """Update the progress display with the current progress value."""
11
+ pass
12
+
13
+ def update_with_stats(self, progress: int, success_count: int = 0, failure_count: int = 0) -> None:
14
+ """
15
+ Update the progress display with current progress and success/failure statistics.
16
+
17
+ This is an optional method that implementations can override to display
18
+ detailed statistics. Default implementation falls back to simple update().
19
+
20
+ Args:
21
+ progress: Current progress value (total items processed)
22
+ success_count: Number of successfully processed items
23
+ failure_count: Number of failed items
24
+ """
25
+ self.update(progress)
26
+
27
+ def set_expected_total(self, total: int | None) -> None: # noqa: B027
28
+ """
29
+ Set the number of items the task expects to process, when the server knows it.
30
+
31
+ Optional: monitors that draw a bar can show the progress against it. None means
32
+ the total is unknown.
33
+ """
34
+ ...
35
+
36
+ def log_event(self, event) -> None: # noqa: B027
37
+ """
38
+ Render a single audit event inline alongside the progress display.
39
+
40
+ Default is a no-op so monitors that don't care about audit (e.g. NopProgressMonitor)
41
+ need not implement this. TTY monitors should print above the progress bar; non-TTY
42
+ monitors should print on a fresh line so they don't fuse with batched output.
43
+ """
44
+ ...
45
+
46
+ def set_task_start_time(self, start_time: datetime | None) -> None: # noqa: B027
47
+ """
48
+ Set the task's original start time for elapsed time calculation.
49
+
50
+ This allows the progress monitor to show elapsed time from when the task
51
+ originally started, not from when monitoring began. Optional method that
52
+ implementations can override if they support elapsed time display.
53
+
54
+ Args:
55
+ start_time: The datetime when the task originally started, or None
56
+ """
57
+ ...
58
+
59
+ @abstractmethod
60
+ def __enter__(self):
61
+ """Enter context manager."""
62
+ pass
63
+
64
+ @abstractmethod
65
+ def __exit__(self, exc_type, exc_val, exc_tb):
66
+ """Exit context manager."""
67
+ pass
68
+
69
+
70
+ class NopProgressMonitor(ProgressMonitor):
71
+ """No-op implementation."""
72
+
73
+ def update(self, progress: int) -> None:
74
+ pass
75
+
76
+ def __enter__(self):
77
+ return self
78
+
79
+ def __exit__(self, exc_type, exc_val, exc_tb):
80
+ pass