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.
- midpoint_cli/__init__.py +234 -0
- midpoint_cli/client/__init__.py +199 -0
- midpoint_cli/client/audit.py +178 -0
- midpoint_cli/client/audit_query.py +247 -0
- midpoint_cli/client/configuration.py +15 -0
- midpoint_cli/client/constants.py +37 -0
- midpoint_cli/client/errors.py +74 -0
- midpoint_cli/client/logging_config.py +122 -0
- midpoint_cli/client/midpoint_client.py +866 -0
- midpoint_cli/client/object_query.py +230 -0
- midpoint_cli/client/objects.py +814 -0
- midpoint_cli/client/observer.py +29 -0
- midpoint_cli/client/patch.py +113 -0
- midpoint_cli/client/progress.py +80 -0
- midpoint_cli/client/rest.py +490 -0
- midpoint_cli/client/safe_xml.py +24 -0
- midpoint_cli/client/server_info.py +130 -0
- midpoint_cli/client/server_version.py +74 -0
- midpoint_cli/client/session.py +142 -0
- midpoint_cli/client/task_definitions.py +110 -0
- midpoint_cli/prompt/__init__.py +196 -0
- midpoint_cli/prompt/audit.py +253 -0
- midpoint_cli/prompt/base.py +218 -0
- midpoint_cli/prompt/complete.py +291 -0
- midpoint_cli/prompt/configuration.py +258 -0
- midpoint_cli/prompt/connector.py +45 -0
- midpoint_cli/prompt/console.py +818 -0
- midpoint_cli/prompt/delete.py +89 -0
- midpoint_cli/prompt/get.py +66 -0
- midpoint_cli/prompt/info.py +53 -0
- midpoint_cli/prompt/log.py +98 -0
- midpoint_cli/prompt/org.py +122 -0
- midpoint_cli/prompt/output.py +33 -0
- midpoint_cli/prompt/put.py +71 -0
- midpoint_cli/prompt/report.py +69 -0
- midpoint_cli/prompt/resource.py +217 -0
- midpoint_cli/prompt/role.py +96 -0
- midpoint_cli/prompt/script.py +154 -0
- midpoint_cli/prompt/search.py +51 -0
- midpoint_cli/prompt/shadow.py +87 -0
- midpoint_cli/prompt/task.py +296 -0
- midpoint_cli/prompt/user.py +406 -0
- midpoint_cli-1.6.0.dist-info/METADATA +1334 -0
- midpoint_cli-1.6.0.dist-info/RECORD +47 -0
- midpoint_cli-1.6.0.dist-info/WHEEL +4 -0
- midpoint_cli-1.6.0.dist-info/entry_points.txt +3 -0
- 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
|