python4cpm 1.0.10__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.
- python4cpm/__init__.py +13 -0
- python4cpm/python4cpm.py +257 -0
- python4cpm/tpchelper.py +44 -0
- python4cpm-1.0.10.dist-info/METADATA +243 -0
- python4cpm-1.0.10.dist-info/RECORD +8 -0
- python4cpm-1.0.10.dist-info/WHEEL +5 -0
- python4cpm-1.0.10.dist-info/licenses/LICENSE +21 -0
- python4cpm-1.0.10.dist-info/top_level.txt +1 -0
python4cpm/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from python4cpm.python4cpm import Args, Secret, Secrets, Python4CPM
|
|
2
|
+
from python4cpm.tpchelper import TPCHelper
|
|
3
|
+
from importlib.metadata import version as __version
|
|
4
|
+
|
|
5
|
+
__version__ = __version(__name__)
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
Args,
|
|
9
|
+
Secret,
|
|
10
|
+
Secrets,
|
|
11
|
+
Python4CPM,
|
|
12
|
+
TPCHelper
|
|
13
|
+
]
|
python4cpm/python4cpm.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import logging
|
|
4
|
+
from logging.handlers import RotatingFileHandler
|
|
5
|
+
from argparse import ArgumentParser
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Args:
|
|
9
|
+
ARGS = (
|
|
10
|
+
"action",
|
|
11
|
+
"address",
|
|
12
|
+
"username",
|
|
13
|
+
"logon_username",
|
|
14
|
+
"reconcile_username",
|
|
15
|
+
"logging",
|
|
16
|
+
"logging_level"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self: str,
|
|
21
|
+
action: str,
|
|
22
|
+
address: str,
|
|
23
|
+
username: str,
|
|
24
|
+
reconcile_username: str,
|
|
25
|
+
logon_username: str,
|
|
26
|
+
logging: str,
|
|
27
|
+
logging_level: str
|
|
28
|
+
) -> None:
|
|
29
|
+
self._action = action
|
|
30
|
+
self._address = address
|
|
31
|
+
self._username = username
|
|
32
|
+
self._reconcile_username = reconcile_username
|
|
33
|
+
self._logon_username = logon_username
|
|
34
|
+
self._logging = logging
|
|
35
|
+
self._logging_level = logging_level
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def action(self) -> str:
|
|
39
|
+
return self._action
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def address(self) -> str:
|
|
43
|
+
return self._address
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def username(self) -> str:
|
|
47
|
+
return self._username
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def reconcile_username(self) -> str:
|
|
51
|
+
return self._reconcile_username
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def logon_username(self) -> str:
|
|
55
|
+
return self._logon_username
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def logging(self) -> str:
|
|
59
|
+
return self._logging
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def logging_level(self) -> str:
|
|
63
|
+
return self._logging_level
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Secret:
|
|
67
|
+
def __init__(self, value: str) -> None:
|
|
68
|
+
self._secret = value
|
|
69
|
+
|
|
70
|
+
def __repr__(self) -> str:
|
|
71
|
+
return f"{self.__class__.__name__}('***')"
|
|
72
|
+
|
|
73
|
+
def get(self) -> str:
|
|
74
|
+
return self._secret
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Secrets:
|
|
78
|
+
SECRETS = (
|
|
79
|
+
"password",
|
|
80
|
+
"logon_password",
|
|
81
|
+
"reconcile_password",
|
|
82
|
+
"new_password"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self: str,
|
|
87
|
+
password: str,
|
|
88
|
+
logon_password: str,
|
|
89
|
+
reconcile_password: str,
|
|
90
|
+
new_password: str
|
|
91
|
+
) -> None:
|
|
92
|
+
self._password = Secret(password)
|
|
93
|
+
self._logon_password = Secret(logon_password)
|
|
94
|
+
self._reconcile_password = Secret(reconcile_password)
|
|
95
|
+
self._new_password = Secret(new_password)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def password(self) -> str:
|
|
99
|
+
return self._password
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def new_password(self) -> str:
|
|
103
|
+
return self._new_password
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def logon_password(self) -> str:
|
|
107
|
+
return self._logon_password
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def reconcile_password(self) -> str:
|
|
111
|
+
return self._reconcile_password
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class Python4CPM:
|
|
115
|
+
ACTION_VERIFY = "verifypass"
|
|
116
|
+
ACTION_LOGON = "logon"
|
|
117
|
+
ACTION_CHANGE = "changepass"
|
|
118
|
+
ACTION_PRERECONCILE = "prereconcilepass"
|
|
119
|
+
ACTION_RECONCILE = "reconcilepass"
|
|
120
|
+
_VALID_ACTIONS = (
|
|
121
|
+
ACTION_VERIFY,
|
|
122
|
+
ACTION_LOGON,
|
|
123
|
+
ACTION_CHANGE,
|
|
124
|
+
ACTION_PRERECONCILE,
|
|
125
|
+
ACTION_RECONCILE,
|
|
126
|
+
)
|
|
127
|
+
_LOGS_DIR = os.path.join("Logs", "ThirdParty", "Python4CPM")
|
|
128
|
+
_CPM_ROOT_DIR = "C:\\Program Files (x86)\\CyberArk\\Password Manager"
|
|
129
|
+
if os.path.exists(_CPM_ROOT_DIR):
|
|
130
|
+
_LOGS_DIR = os.path.join(_CPM_ROOT_DIR, _LOGS_DIR)
|
|
131
|
+
_LOGGING_ENABLED_VALUE = "yes"
|
|
132
|
+
_LOGGING_LEVELS = {
|
|
133
|
+
"info": logging.INFO,
|
|
134
|
+
"debug": logging.DEBUG
|
|
135
|
+
}
|
|
136
|
+
_SECRETS_PADDING = "@@@"
|
|
137
|
+
_SUCCESS_PROMPT = "~~~SUCCESS~~~"
|
|
138
|
+
_FAILED_RECOVERABLE_PROMPT = "~~~FAILED_RECOVERABLE~~~"
|
|
139
|
+
_FAILED_UNRECOVERABLE_PROMPT = "~~~FAILED_UNRECOVERABLE~~~"
|
|
140
|
+
|
|
141
|
+
def __init__(self, name: str) -> None:
|
|
142
|
+
self._name = name
|
|
143
|
+
args = self._get_args()
|
|
144
|
+
self._args = Args(**args)
|
|
145
|
+
self._logger = self._get_logger(self._name)
|
|
146
|
+
self.log_info("Python4CPM.__init__: initiating...")
|
|
147
|
+
self._log_args()
|
|
148
|
+
self._verify_action()
|
|
149
|
+
secrets = self._get_secrets()
|
|
150
|
+
self._secrets = Secrets(**secrets)
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def args(self) -> Args:
|
|
154
|
+
return self._args
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def secrets(self) -> Secrets:
|
|
158
|
+
return self._secrets
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def logger(self) -> logging.Logger:
|
|
162
|
+
return self._logger
|
|
163
|
+
|
|
164
|
+
def log_debug(self, message: str) -> None:
|
|
165
|
+
if self._logger is not None:
|
|
166
|
+
self._logger.debug(message)
|
|
167
|
+
|
|
168
|
+
def log_info(self, message: str) -> None:
|
|
169
|
+
if self._logger is not None:
|
|
170
|
+
self._logger.info(message)
|
|
171
|
+
|
|
172
|
+
def log_warning(self, message: str) -> None:
|
|
173
|
+
if self._logger is not None:
|
|
174
|
+
self._logger.warning(message)
|
|
175
|
+
|
|
176
|
+
def log_error(self, message: str) -> None:
|
|
177
|
+
if self._logger is not None:
|
|
178
|
+
self._logger.error(message)
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def _get_args() -> dict:
|
|
182
|
+
parser = ArgumentParser()
|
|
183
|
+
for arg in Args.ARGS:
|
|
184
|
+
parser.add_argument(f"--{arg}")
|
|
185
|
+
args = parser.parse_args()
|
|
186
|
+
return dict(vars(args))
|
|
187
|
+
|
|
188
|
+
def _get_secrets(self) -> dict:
|
|
189
|
+
secrets = {}
|
|
190
|
+
try:
|
|
191
|
+
for secret in Secrets.SECRETS:
|
|
192
|
+
prompt = self._SECRETS_PADDING + secret + self._SECRETS_PADDING
|
|
193
|
+
secrets[secret] = input(prompt)
|
|
194
|
+
common_message = f"Python4CPM._get_secrets: {secret} ->"
|
|
195
|
+
if secrets[secret]:
|
|
196
|
+
self.log_info(f"{common_message} [*******]")
|
|
197
|
+
else:
|
|
198
|
+
self.log_info(f"{common_message} [NOT SET]")
|
|
199
|
+
except Exception as e:
|
|
200
|
+
self.log_error(f"Python4CPM._get_secrets: {type(e).__name__}: {e}")
|
|
201
|
+
self.close_fail()
|
|
202
|
+
return secrets
|
|
203
|
+
|
|
204
|
+
def _verify_action(self) -> None:
|
|
205
|
+
if self.args.action not in self._VALID_ACTIONS:
|
|
206
|
+
self.log_warning(
|
|
207
|
+
f"Python4CPM._verify_action: unkonwn action -> {self.args.action}"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def _log_args(self) -> None:
|
|
211
|
+
for key, value in vars(self._args).items():
|
|
212
|
+
common_message = f"Python4CPM._log_args: {key.strip('_')} ->"
|
|
213
|
+
if value:
|
|
214
|
+
self.log_info(f"{common_message} {value}")
|
|
215
|
+
else:
|
|
216
|
+
self.log_info(f"{common_message} [NOT SET]")
|
|
217
|
+
|
|
218
|
+
def _get_logger(self, name: str) -> logging.Logger:
|
|
219
|
+
if self._args.logging is None:
|
|
220
|
+
return None
|
|
221
|
+
if self._args.logging.lower() != self._LOGGING_ENABLED_VALUE:
|
|
222
|
+
return None
|
|
223
|
+
os.makedirs(self._LOGS_DIR, exist_ok=True)
|
|
224
|
+
logs_file = os.path.join(self._LOGS_DIR, f"{name}.log")
|
|
225
|
+
logger = logging.getLogger(name)
|
|
226
|
+
logging_level = self._args.logging_level.lower()
|
|
227
|
+
if logging_level in self._LOGGING_LEVELS:
|
|
228
|
+
logger.setLevel(self._LOGGING_LEVELS[logging_level])
|
|
229
|
+
else:
|
|
230
|
+
logger.setLevel(self._LOGGING_LEVELS["info"])
|
|
231
|
+
handler = RotatingFileHandler(
|
|
232
|
+
filename=logs_file,
|
|
233
|
+
maxBytes=1 * 1024 * 1024,
|
|
234
|
+
backupCount=2
|
|
235
|
+
)
|
|
236
|
+
formatter = logging.Formatter(
|
|
237
|
+
fmt="%(asctime)s | %(levelname)s | %(message)s",
|
|
238
|
+
datefmt="%Y-%m-%d %H:%M:%S"
|
|
239
|
+
)
|
|
240
|
+
handler.setFormatter(formatter)
|
|
241
|
+
logger.addHandler(handler)
|
|
242
|
+
return logger
|
|
243
|
+
|
|
244
|
+
def close_fail(self, unrecoverable: bool = False) -> None:
|
|
245
|
+
if unrecoverable is False:
|
|
246
|
+
prompt = self._FAILED_RECOVERABLE_PROMPT
|
|
247
|
+
else:
|
|
248
|
+
prompt = self._FAILED_UNRECOVERABLE_PROMPT
|
|
249
|
+
self.log_error(f"Python4CPM.close_fail: closing with {prompt}")
|
|
250
|
+
print(prompt)
|
|
251
|
+
sys.exit(1)
|
|
252
|
+
|
|
253
|
+
def close_success(self) -> None:
|
|
254
|
+
prompt = self._SUCCESS_PROMPT
|
|
255
|
+
self.log_info(f"Python4CPM.close_success: closing with {prompt}")
|
|
256
|
+
print(prompt)
|
|
257
|
+
sys.exit(0)
|
python4cpm/tpchelper.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from .python4cpm import Python4CPM, Args
|
|
2
|
+
from unittest import mock
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TPCHelper:
|
|
6
|
+
@classmethod
|
|
7
|
+
def run(
|
|
8
|
+
cls,
|
|
9
|
+
action: str = "",
|
|
10
|
+
address: str = "",
|
|
11
|
+
username: str = "",
|
|
12
|
+
logon_username: str = "",
|
|
13
|
+
reconcile_username: str = "",
|
|
14
|
+
logging: str = "",
|
|
15
|
+
logging_level: str = "",
|
|
16
|
+
password: str = "",
|
|
17
|
+
logon_password: str = "",
|
|
18
|
+
reconcile_password: str = "",
|
|
19
|
+
new_password: str = ""
|
|
20
|
+
) -> Python4CPM:
|
|
21
|
+
args = [
|
|
22
|
+
"", # sys.argv[0] is ignored by argparse
|
|
23
|
+
f"--{Args.ARGS[0]}={action}",
|
|
24
|
+
f"--{Args.ARGS[1]}={address}",
|
|
25
|
+
f"--{Args.ARGS[2]}={username}",
|
|
26
|
+
f"--{Args.ARGS[3]}={logon_username}",
|
|
27
|
+
f"--{Args.ARGS[4]}={reconcile_username}",
|
|
28
|
+
f"--{Args.ARGS[5]}={logging}",
|
|
29
|
+
f"--{Args.ARGS[6]}={logging_level}"
|
|
30
|
+
]
|
|
31
|
+
secrets = (
|
|
32
|
+
password,
|
|
33
|
+
logon_password,
|
|
34
|
+
reconcile_password,
|
|
35
|
+
new_password
|
|
36
|
+
)
|
|
37
|
+
with mock.patch(
|
|
38
|
+
"sys.argv",
|
|
39
|
+
args
|
|
40
|
+
), mock.patch(
|
|
41
|
+
"builtins.input",
|
|
42
|
+
side_effect=secrets
|
|
43
|
+
):
|
|
44
|
+
return Python4CPM(cls.__name__)
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python4cpm
|
|
3
|
+
Version: 1.0.10
|
|
4
|
+
Summary: Python for CPM
|
|
5
|
+
Author-email: Gonzalo Atienza Rela <gonatienza@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Gonzalo Atienza Rela
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in
|
|
18
|
+
all copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
26
|
+
THE SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Requires-Python: >=3.8
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: ruff; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# Python4CPM
|
|
36
|
+
|
|
37
|
+
A simple way of using python scripts with CyberArk CPM rotations. This module levereages the [Terminal Plugin Controller](https://docs.cyberark.com/pam-self-hosted/latest/en/content/pasimp/plug-in-terminal-plugin-controller.htm) (TPC) in CPM to offload a password rotation logic into a script.
|
|
38
|
+
|
|
39
|
+
This platform allows you to duplicate it multiple times, simply changing its settings from Privilege Cloud/PVWA to point to different python scripts leveraging the module included with the base platform.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
### Preparing Python
|
|
44
|
+
|
|
45
|
+
1. Install Python in CPM. **Python must be installed for all users when running the install wizard**.
|
|
46
|
+
2. Create a venv in CPM, by running `py -m venv c:\venv`. Use the default location `c:\venv` or a custom one (e.g., `c:\my-venv-path`).
|
|
47
|
+
3. Install `python4cpm` in your venv:
|
|
48
|
+
- If your CPM can connect to the internet, install with `c:\venv\Scripts\pip install python4cpm`.
|
|
49
|
+
- If your CPM cannot connect to the internet:
|
|
50
|
+
- Download the [latest wheel](https://github.com/gonatienza/python4cpm/releases/download/latest/python4cpm-wheel.zip).
|
|
51
|
+
- Copy the file to CPM, extract to a temporary location.
|
|
52
|
+
- From the temporary location run `c:\venv\Scripts\pip install --no-index --find-links=.\python4cpm-wheel python4cpm`.
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
### Importing the platform
|
|
56
|
+
|
|
57
|
+
1. Download the [latest platform zip file](https://github.com/gonatienza/python4cpm/releases/download/latest/python4cpm-platform.zip).
|
|
58
|
+
2. Import the platform zip file into Privilege Cloud/PVWA `(Administration -> Platform Management -> Import platform)`.
|
|
59
|
+
3. Craft your python script and place it within the bin folder of CPM (`C:\Program Files (x86)\CyberArk\Password Manager\bin`).
|
|
60
|
+
4. Duplicate the imported platform in Privilege Cloud/PVWA `(Administration -> Platform Management -> Application -> Python for CPM)` and name it after your application (e.g., My App).
|
|
61
|
+
5. Edit the duplicated platform and specify the path of your placed script in the bin folder of CPM, under `Target Account Platform -> Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonScriptPath -> Value` (e.g., `bin\myapp.py`).
|
|
62
|
+
6. If you used a custom venv location, also update `Target Account Platform -> Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonExePath -> Value` with the custom path for the venv's `python.exe` file (e.g., `c:\my-venv-path\Scripts\python.exe`).
|
|
63
|
+
7. If you want to disable logging, update `Target Account Platform -> Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonLogging -> Value` to `no`.
|
|
64
|
+
8. If you want to change the logging level to `debug`, update `Target Account Platform -> Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonLoggingLevel -> Value` to `debug`.
|
|
65
|
+
9. For new applications repeat steps from 3 to 8.
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## Python Script
|
|
69
|
+
|
|
70
|
+
### Example:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from python4cpm import Python4CPM
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
p4cpm = Python4CPM("MyApp") # this instantiates the object and grabs all arguments and secrets shared by TPC
|
|
77
|
+
|
|
78
|
+
# These are the usable properties and related methods from the object:
|
|
79
|
+
p4cpm.args.action # action requested from CPM
|
|
80
|
+
p4cpm.args.address # address from the account address field
|
|
81
|
+
p4cpm.args.username # username from the account username field
|
|
82
|
+
p4cpm.args.reconcile_username # reconcile username from the linked reconcile account
|
|
83
|
+
p4cpm.args.logon_username # logon username from the linked logon account
|
|
84
|
+
p4cpm.args.logging # used to carry the platform logging settings for python
|
|
85
|
+
p4cpm.secrets.password.get() # get str from password received from the vault
|
|
86
|
+
p4cpm.secrets.new_password.get() # get str from new password in case of a rotation
|
|
87
|
+
p4cpm.secrets.logon_password.get() # get str from linked logon account password
|
|
88
|
+
p4cpm.secrets.reconcile_password.get() # get str from linked reconcile account password
|
|
89
|
+
|
|
90
|
+
# Logging methods -> Will only log if Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonLogging is set to yes (default is yes)
|
|
91
|
+
p4cpm.log_error("this is an error message") # logs error into Logs/ThirdParty/Python4CPM/MyApp.log
|
|
92
|
+
p4cpm.log_warning("this is a warning message") # logs warning into Logs/ThirdParty/Python4CPM/MyApp.log
|
|
93
|
+
p4cpm.log_info("this is an info message") # logs info into Logs/ThirdParty/Python4CPM/MyApp.log
|
|
94
|
+
# Logging level -> Will only log debug messages if Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonLoggingLevel is set to debug (default is info)
|
|
95
|
+
p4cpm.log_debug("this is an debug message") # logs info into Logs/ThirdParty/Python4CPM/MyApp.log if logging level is set to debug
|
|
96
|
+
|
|
97
|
+
# Terminate signals -> ALWAYS use one of the following three signals to terminate the script
|
|
98
|
+
## p4cpm.close_success() # terminate with success state
|
|
99
|
+
## p4cpm.close_fail() # terminate with recoverable failed state
|
|
100
|
+
## p4cpm.close_fail(unrecoverable=True) # terminate with unrecoverable failed state
|
|
101
|
+
# If no signal is call, CPM will not know if the action was successful and display an error
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Verification example -> verify the username and password are valid
|
|
105
|
+
def verify(from_reconcile=False):
|
|
106
|
+
if from_reconcile is False:
|
|
107
|
+
pass
|
|
108
|
+
# Use p4cpm.args.address, p4cpm.args.username, p4cpm.secrets.password.get()
|
|
109
|
+
# for your logic in a verification
|
|
110
|
+
else:
|
|
111
|
+
pass
|
|
112
|
+
# Use p4cpm.args.address, p4cpm.args.reconcile_username, p4cpm.secrets.reconcile_password.get()
|
|
113
|
+
# for your logic in a verification
|
|
114
|
+
result = True
|
|
115
|
+
if result is True:
|
|
116
|
+
p4cpm.log_info("verification successful") # logs info message into Logs/ThirdParty/Python4CPM/MyApp.log
|
|
117
|
+
else:
|
|
118
|
+
p4cpm.log_error("something went wrong") # logs error message Logs/ThirdParty/Python4CPM/MyApp.log
|
|
119
|
+
raise Exception("verify failed") # raise to trigger failed termination signal
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# Rotation example -> rotate the password of the account
|
|
123
|
+
def change(from_reconcile=False):
|
|
124
|
+
if from_reconcile is False:
|
|
125
|
+
pass
|
|
126
|
+
# Use p4cpm.args.address, p4cpm.args.username, p4cpm.secrets.password.get()
|
|
127
|
+
# and p4cpm.secrets.new_password.get() for your logic in a rotation
|
|
128
|
+
else:
|
|
129
|
+
pass
|
|
130
|
+
# Use p4cpm.args.address, p4cpm.args.username, p4cpm.args.reconcile_username,
|
|
131
|
+
# p4cpm.secrets.reconcile_password.get() and p4cpm.secrets.new_password.get() for your logic in a reconciliation
|
|
132
|
+
result = True
|
|
133
|
+
if result is True:
|
|
134
|
+
p4cpm.log_info("rotation successful") # logs info message into Logs/ThirdParty/Python4CPM/MyApp.log
|
|
135
|
+
else:
|
|
136
|
+
p4cpm.log_error("something went wrong") # logs error message Logs/ThirdParty/Python4CPM/MyApp.log
|
|
137
|
+
raise Exception("change failed") # raise to trigger failed termination signal
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
try:
|
|
142
|
+
if action == Python4CPM.ACTION_VERIFY: # class attribute ACTION_VERIFY holds the verify action value
|
|
143
|
+
verify()
|
|
144
|
+
p4cpm.close_success() # terminate with success state
|
|
145
|
+
elif p4cpm.args.action == Python4CPM.ACTION_LOGON: # class attribute ACTION_LOGON holds the logon action value
|
|
146
|
+
verify()
|
|
147
|
+
p4cpm.close_success() # terminate with success state
|
|
148
|
+
elif p4cpm.args.action == Python4CPM.ACTION_CHANGE: # class attribute ACTION_CHANGE holds the password change action value
|
|
149
|
+
change()
|
|
150
|
+
p4cpm.close_success() # terminate with success state
|
|
151
|
+
elif p4cpm.args.action == Python4CPM.ACTION_PRERECONCILE: # class attribute ACTION_PRERECONCILE holds the pre-reconcile action value
|
|
152
|
+
verify(from_reconcile=True)
|
|
153
|
+
p4cpm.close_success() # terminate with success state
|
|
154
|
+
# Alternatively ->
|
|
155
|
+
## p4cpm.log_error("reconciliation is not supported") # let the logs know that reconciliation is not supported
|
|
156
|
+
## p4cpm.close_fail() # let CPM know to check the logs
|
|
157
|
+
elif p4cpm.args.action == Python4CPM.ACTION_RECONCILE: # class attribute ACTION_RECONCILE holds the reconcile action value
|
|
158
|
+
change(from_reconcile=True)
|
|
159
|
+
p4cpm.close_success() # terminate with success state
|
|
160
|
+
# Alternatively ->
|
|
161
|
+
## p4cpm.log_error("reconciliation is not supported") # let the logs know that reconciliation is not supported
|
|
162
|
+
## p4cpm.close_fail() # let CPM know to check the logs
|
|
163
|
+
else:
|
|
164
|
+
p4cpm.log_error(f"invalid action: '{action}'") # logs into Logs/ThirdParty/Python4CPM/MyApp.log
|
|
165
|
+
p4cpm.close_fail(unrecoverable=True) # terminate with unrecoverable failed state
|
|
166
|
+
except Exception as e:
|
|
167
|
+
p4cpm.log_error(f"{type(e).__name__}: {e}")
|
|
168
|
+
p4cpm.close_fail()
|
|
169
|
+
```
|
|
170
|
+
(*) a more realistic examples can be found [here](https://github.com/gonatienza/python4cpm/blob/main/examples).
|
|
171
|
+
|
|
172
|
+
When doing `verify`, `change` or `reconcile` from Privilege Cloud/PVWA:
|
|
173
|
+
1. Verify -> the sciprt will be executed once with the `p4cpm.args.action` as `Python4CPM.ACTION_VERIFY`.
|
|
174
|
+
2. Change -> the sciprt will be executed twice, once with the action `p4cpm.args.action` as `Python4CPM.ACTION_LOGON` and once as `Python4CPM.ACTION_CHANGE`.
|
|
175
|
+
- If all actions are not terminated with `p4cpm.close_success()` the overall change will fail.
|
|
176
|
+
3. Reconcile -> the sciprt will be executed twice, once with the `p4cpm.args.action` as `Python4CPM.ACTION_PRERECONCILE` and once as `Python4CPM.ACTION_RECONCILE`.
|
|
177
|
+
- If all actions are not terminated with `p4cpm.close_success()` the overall reconcile will fail.
|
|
178
|
+
4. When `p4cpm.args.action` comes as `Python4CPM.ACTION_VERIFY`, `Python4CPM.ACTION_LOGON` or `Python4CPM.ACTION_PRERECONCILE`: `p4cpm.secrets.new_password.get()` will always return an empty string.
|
|
179
|
+
5. If a logon account is not linked, `p4cpm.args.logon_username` and `p4cpm.secrets.logon_password.get()` will return an empty string.
|
|
180
|
+
6. If a reconcile account is not linked, `p4cpm.args.reconcile_username` and `p4cpm.secrets.reconcile_password.get()` will return an empty string.
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
### Installing dependancies in python venv
|
|
184
|
+
|
|
185
|
+
As with any python venv, you can install dependancies in your venv.
|
|
186
|
+
1. If your CPM can connect to the internet:
|
|
187
|
+
- You can use regular pip install commands (e.g., `c:\venv\Scripts\pip.exe install requests`).
|
|
188
|
+
2. If your CPM cannot connect to the internet:
|
|
189
|
+
- You can download packages for an offline install. More info [here](https://pip.pypa.io/en/stable/cli/pip_download/).
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
## Dev Helper:
|
|
193
|
+
|
|
194
|
+
TPC is a binary Terminal Plugin Controller in CPM. It passes information to Python4CPM through arguments and prompts when calling the script.
|
|
195
|
+
For dev purposes, `TPCHelper` is a companion helper that simplifies the instantiation of the `Python4CPM` object by simulating how TPC passes those arguments and prompts.
|
|
196
|
+
Install this module (in a dev workstation) with:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
pip install https://github.com/gonatienza/python4cpm/releases/download/latest/python4cpm-latest-py3-none-any.whl
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Example:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from python4cpm import TPCHelper, Python4CPM
|
|
206
|
+
from getpass import getpass
|
|
207
|
+
|
|
208
|
+
# Get secrets for your password, logon account password, reconcile account password and new password
|
|
209
|
+
# You can use an empty string if it does not apply
|
|
210
|
+
password = getpass("password: ") # password from account
|
|
211
|
+
logon_password = getpass("logon_password: ") # password from linked logon account
|
|
212
|
+
reconcile_password = getpass("reconcile_password: ") # password from linked reconcile account
|
|
213
|
+
new_password = getpass("new_password: ") # new password for the rotation
|
|
214
|
+
|
|
215
|
+
p4cpm = TPCHelper.run(
|
|
216
|
+
action=Python4CPM.ACTION_LOGON, # use actions from Python4CPM.ACTION_*
|
|
217
|
+
address="myapp.corp.local", # populate with the address from your account properties
|
|
218
|
+
username="jdoe", # populate with the username from your account properties
|
|
219
|
+
logon_username="ldoe", # populate with the logon account username from your linked logon account
|
|
220
|
+
reconcile_username="rdoe", # ppopulate with the reconcile account username from your linked logon account
|
|
221
|
+
logging="yes", # populate with the PythonLogging parameter from the platform: "yes" or "no"
|
|
222
|
+
logging_level="info", # populate with the PythonLoggingLevel parameter from the platform: "info" or "debug"
|
|
223
|
+
password=password,
|
|
224
|
+
logon_password=logon_password,
|
|
225
|
+
reconcile_password=reconcile_password,
|
|
226
|
+
new_password=new_password
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Use the p4cpm object during dev to build your script logic
|
|
230
|
+
assert password == p4cpm.secrets.password.get()
|
|
231
|
+
p4cpm.log_info("success!")
|
|
232
|
+
p4cpm.close_success()
|
|
233
|
+
|
|
234
|
+
# Remember for your final script:
|
|
235
|
+
## changing the definition of p4cpm from TPCHelper.run() to Python4CPM("MyApp")
|
|
236
|
+
## remove any secrets prompting
|
|
237
|
+
## remove the TPCHelper import
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Remember for your final script:
|
|
241
|
+
- Change the definition of `p4cpm` from `p4cpm = TPCHelper.run(**kwargs)` to `p4cpm = Python4CPM("MyApp")`.
|
|
242
|
+
- Remove any secrets prompting or interactive interruptions.
|
|
243
|
+
- Remove the import of `TPCHelper`.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
python4cpm/__init__.py,sha256=hnAjx3Oc7oqPuDXuPv19NpOUCz3CELi9SuCHMEYPHO8,278
|
|
2
|
+
python4cpm/python4cpm.py,sha256=oaA5v66pc1f1Op23RTlpBtnoYktuod9t5iYJXOhRnDc,7442
|
|
3
|
+
python4cpm/tpchelper.py,sha256=1g7uNs1W8Rgb84-x3MMT_e_iXhnX3v2xjXKR8ebsY84,1241
|
|
4
|
+
python4cpm-1.0.10.dist-info/licenses/LICENSE,sha256=vF2wZA3GchZjH5lUmhonEwd4snuBc6o0ytpZNxVFyCw,1077
|
|
5
|
+
python4cpm-1.0.10.dist-info/METADATA,sha256=p-ZBO0CYNIh5xIriD3_XMuaB-4tEVpgLGrprYAM93-U,14360
|
|
6
|
+
python4cpm-1.0.10.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
7
|
+
python4cpm-1.0.10.dist-info/top_level.txt,sha256=EhbshwpANmL0wYXaET8_6V9ZENtSwIyAd-kM3jGhUlE,11
|
|
8
|
+
python4cpm-1.0.10.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gonzalo Atienza Rela
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
python4cpm
|