python4cpm 1.1.1__tar.gz → 1.1.3__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.
- {python4cpm-1.1.1/src/python4cpm.egg-info → python4cpm-1.1.3}/PKG-INFO +39 -71
- {python4cpm-1.1.1 → python4cpm-1.1.3}/README.md +38 -70
- {python4cpm-1.1.1 → python4cpm-1.1.3}/pyproject.toml +1 -1
- python4cpm-1.1.3/src/python4cpm/accounts.py +70 -0
- python4cpm-1.1.3/src/python4cpm/args.py +22 -0
- python4cpm-1.1.3/src/python4cpm/envhandler.py +32 -0
- {python4cpm-1.1.1 → python4cpm-1.1.3}/src/python4cpm/logger.py +4 -3
- python4cpm-1.1.3/src/python4cpm/nethelper.py +61 -0
- {python4cpm-1.1.1 → python4cpm-1.1.3}/src/python4cpm/python4cpm.py +17 -45
- {python4cpm-1.1.1 → python4cpm-1.1.3}/src/python4cpm/python4cpmhandler.py +1 -1
- python4cpm-1.1.3/src/python4cpm/secret.py +24 -0
- {python4cpm-1.1.1 → python4cpm-1.1.3/src/python4cpm.egg-info}/PKG-INFO +39 -71
- {python4cpm-1.1.1 → python4cpm-1.1.3}/src/python4cpm.egg-info/SOURCES.txt +1 -0
- python4cpm-1.1.1/src/python4cpm/accounts.py +0 -70
- python4cpm-1.1.1/src/python4cpm/args.py +0 -21
- python4cpm-1.1.1/src/python4cpm/nethelper.py +0 -46
- python4cpm-1.1.1/src/python4cpm/secret.py +0 -15
- {python4cpm-1.1.1 → python4cpm-1.1.3}/LICENSE +0 -0
- {python4cpm-1.1.1 → python4cpm-1.1.3}/setup.cfg +0 -0
- {python4cpm-1.1.1 → python4cpm-1.1.3}/src/python4cpm/__init__.py +0 -0
- {python4cpm-1.1.1 → python4cpm-1.1.3}/src/python4cpm/crypto.py +0 -0
- {python4cpm-1.1.1 → python4cpm-1.1.3}/src/python4cpm.egg-info/dependency_links.txt +0 -0
- {python4cpm-1.1.1 → python4cpm-1.1.3}/src/python4cpm.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python4cpm
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.3
|
|
4
4
|
Summary: Python for CPM
|
|
5
5
|
Author-email: Gonzalo Atienza Rela <gonatienza@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -11,9 +11,15 @@ Dynamic: license-file
|
|
|
11
11
|
|
|
12
12
|
# Python4CPM
|
|
13
13
|
|
|
14
|
-
A simple way of using python scripts with CyberArk CPM/SRS
|
|
14
|
+
A simple and secure way of using python scripts with CyberArk CPM/SRS password rotations.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
## How it works
|
|
17
|
+
|
|
18
|
+
This module leverages the [Credential Management .NET SDK](https://docs.cyberark.com/privilege-cloud-standard/latest/en/content/pasimp/plug-in-netinvoker.htm) from CyberArk to securely offload a password rotation logic into Python.
|
|
19
|
+
|
|
20
|
+
All objects are collected from the SDK and sent as environment context to be picked up by the `python4cpm` module during the subprocess execution of python. All secrets of such environment are protected and encrypted by [Data Protection API (DPAPI)](https://learn.microsoft.com/en-us/dotnet/standard/security/how-to-use-data-protection), until they are explicitely retrieved in your python script runtime, invoking the `Secret.get()` method. Finally, python controls the termination signal sent back to the SDK, which is consequently used as the return code to CPM/SRS. Such as a successful or failed (recoverable or not) result of the requested action.
|
|
21
|
+
|
|
22
|
+
This platform allows you to duplicate it multiple times, simply changing its settings (with regular day two operations from Privilege Cloud/PVWA) to point to different venvs and/or python scripts.
|
|
17
23
|
|
|
18
24
|
## Installation
|
|
19
25
|
|
|
@@ -61,7 +67,7 @@ This platform allows you to duplicate it multiple times, simply changing its set
|
|
|
61
67
|
from python4cpm import Python4CPMHandler
|
|
62
68
|
|
|
63
69
|
|
|
64
|
-
class MyRotator(Python4CPMHandler):
|
|
70
|
+
class MyRotator(Python4CPMHandler):
|
|
65
71
|
"""
|
|
66
72
|
These are the usable properties and methods from Python4CPMHandler:
|
|
67
73
|
|
|
@@ -98,9 +104,7 @@ class MyRotator(Python4CPMHandler): # create a subclass for the Handler
|
|
|
98
104
|
self.logger.info("this is an info message")
|
|
99
105
|
self.logger.debug("this is a debug message")
|
|
100
106
|
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
## The logging level comes from PythonLoggingLevel (platform parameters) (default is error)
|
|
107
|
+
# The logging level comes from PythonLoggingLevel (platform parameters) (default is error)
|
|
104
108
|
|
|
105
109
|
=============================
|
|
106
110
|
REQUIRED TERMINATION SIGNALS
|
|
@@ -108,75 +112,44 @@ class MyRotator(Python4CPMHandler): # create a subclass for the Handler
|
|
|
108
112
|
Terminate signals -> MUST use one of the following three signals to terminate the script:
|
|
109
113
|
|
|
110
114
|
self.close_success()
|
|
111
|
-
# terminate with success state
|
|
115
|
+
# terminate and provide CPM/SRS with a success state
|
|
112
116
|
|
|
113
117
|
self.close_fail()
|
|
114
|
-
# terminate with
|
|
118
|
+
# terminate and provide CPM/SRS with a failed recoverable state
|
|
115
119
|
|
|
116
120
|
self.close_fail(unrecoverable=True)
|
|
117
|
-
# terminate with
|
|
121
|
+
# terminate and provide CPM/SRS with a failed unrecoverable state
|
|
118
122
|
|
|
119
123
|
When calling a signal sys.exit is invoked and the script is terminated.
|
|
120
124
|
If no signal is called, and the script finishes without any exception,
|
|
121
125
|
it will behave like p4cpm.close_fail(unrecoverable=True) and log an error message.
|
|
126
|
+
|
|
122
127
|
=============================
|
|
128
|
+
REQUIRED METHODS
|
|
123
129
|
=============================
|
|
130
|
+
verify(), logon(), change(), prereconcile(), reconcile()
|
|
124
131
|
"""
|
|
125
132
|
|
|
126
|
-
# =============================
|
|
127
|
-
# REQUIRED METHODS (MUST DEFINE)
|
|
128
|
-
# =============================
|
|
129
|
-
# verify(), logon(), change(), prereconcile(), reconcile()
|
|
130
|
-
|
|
131
133
|
def verify(self):
|
|
132
|
-
|
|
133
|
-
self.log_info("verification successful")
|
|
134
|
+
# TODO: use account objects for your logic
|
|
134
135
|
self.close_success()
|
|
135
136
|
|
|
136
137
|
def logon(self):
|
|
138
|
+
# TODO: use account objects for your logic
|
|
137
139
|
self.close_success()
|
|
138
140
|
|
|
139
141
|
def change(self):
|
|
140
|
-
|
|
141
|
-
self.
|
|
142
|
-
self.close_fail()
|
|
142
|
+
# TODO: use account objects for your logic
|
|
143
|
+
self.close_success()
|
|
143
144
|
|
|
144
145
|
def prereconcile(self):
|
|
145
|
-
|
|
146
|
+
# TODO: use account objects for your logic
|
|
146
147
|
self.close_success()
|
|
147
148
|
|
|
148
149
|
def reconcile(self):
|
|
149
|
-
|
|
150
|
+
# TODO: use account objects for your logic
|
|
150
151
|
self.close_success()
|
|
151
152
|
|
|
152
|
-
def _verify(self, from_reconcile=False):
|
|
153
|
-
if from_reconcile is False:
|
|
154
|
-
pass
|
|
155
|
-
# TODO: use account objects for your logic
|
|
156
|
-
else:
|
|
157
|
-
pass
|
|
158
|
-
# TODO: use account objects for your logic
|
|
159
|
-
result = True
|
|
160
|
-
if result is True:
|
|
161
|
-
self.log_info("verification successful")
|
|
162
|
-
else:
|
|
163
|
-
self.log_error("something went wrong")
|
|
164
|
-
self.close_fail()
|
|
165
|
-
|
|
166
|
-
def _change(self, from_reconcile=False):
|
|
167
|
-
if from_reconcile is False:
|
|
168
|
-
pass
|
|
169
|
-
# TODO: use account objects for your logic
|
|
170
|
-
else:
|
|
171
|
-
pass
|
|
172
|
-
# TODO: use account objects for your logic
|
|
173
|
-
result = True
|
|
174
|
-
if result is True:
|
|
175
|
-
self.log_info("rotation successful")
|
|
176
|
-
else:
|
|
177
|
-
self.log_error("something went wrong")
|
|
178
|
-
self.close_fail()
|
|
179
|
-
|
|
180
153
|
|
|
181
154
|
if __name__ == "__main__":
|
|
182
155
|
MyRotator().run() # initializes the class and calls the action that was requested from CPM/SRS.
|
|
@@ -189,9 +162,10 @@ When doing `verify`, `change` or `reconcile` from Privilege Cloud/PVWA:
|
|
|
189
162
|
- If both actions are not terminated with `self.close_success()` and the scripts terminates without any exception, CPM/SRS will see this as a `self.close_fail(unrecoverable=True)`.
|
|
190
163
|
3. Reconcile -> the sciprt will be executed twice, running first the `MyRotator.prereconcile()` method and secondly the `MyRotator.reconcile()` method.
|
|
191
164
|
- If both actions are not terminated with `self.close_success()` and the scripts terminates without any exception, CPM/SRS will see this as a `self.close_fail(unrecoverable=True)`.
|
|
192
|
-
4. When calling `MyRotator.verify()`, `MyRotator.logon()` or `MyRotator.prereconcile()`: `self.
|
|
193
|
-
5. If a logon account is not linked, `self.
|
|
194
|
-
6. If a reconcile account is not linked, `self.
|
|
165
|
+
4. When calling `MyRotator.verify()`, `MyRotator.logon()` or `MyRotator.prereconcile()`: `self.target_account.new_password` will always return `None`.
|
|
166
|
+
5. If a logon account is not linked, `self.logon_account` will return `None`.
|
|
167
|
+
6. If a reconcile account is not linked, `self.reconcile_account` will return `None`.
|
|
168
|
+
7. The python `Logger` places its logs in the `Logs/ThirdParty` directory. The filename will be based on the name of the subclass created (e.g., `MyRotator`).
|
|
195
169
|
|
|
196
170
|
|
|
197
171
|
### Installing dependencies in python venv
|
|
@@ -205,14 +179,9 @@ As with any python venv, you can install dependencies in your venv.
|
|
|
205
179
|
|
|
206
180
|
## Dev Helper:
|
|
207
181
|
|
|
208
|
-
For dev purposes, `NETHelper` is a companion helper
|
|
209
|
-
Install this module (in a dev workstation) with:
|
|
210
|
-
|
|
211
|
-
```bash
|
|
212
|
-
pip install python4cpm
|
|
213
|
-
```
|
|
182
|
+
For dev purposes, `NETHelper` is a companion helper to test your scripts without CPM/SRS. It simplifies the instantiation of the `Python4CPM` or `Python4CPMHandler` objects by simulating how the plugin creates the environment context for the python module.
|
|
214
183
|
|
|
215
|
-
**Note**: As CPM
|
|
184
|
+
**Note**: As CPM and the SRS management agent run in Windows, the plugin was built to encrypt secrets using DPAPI (a windows only library). For dev purposes in Linux/Mac dev workstations, those secrets put in the environment context by `NETHelper` will be in plaintext. In windows dev workstations, `NETHelper` encrypts the secrets as the .NET plugin does. This is informational only, **the module will use its encryption/decryption capabilities automatically based on the platform** it is running on and you do not have to do anything specific to enable it.
|
|
216
185
|
|
|
217
186
|
### Example:
|
|
218
187
|
|
|
@@ -223,7 +192,7 @@ from python4cpm import NETHelper, Python4CPM, Python4CPMHandler
|
|
|
223
192
|
from getpass import getpass
|
|
224
193
|
|
|
225
194
|
# Get secrets for your password, logon account password, reconcile account password and new password
|
|
226
|
-
# You
|
|
195
|
+
# You may set to None any argument that does not apply or simply leaving it to its default None value.
|
|
227
196
|
target_password = getpass("password: ") # password from account
|
|
228
197
|
logon_password = getpass("logon_password: ") # password from linked logon account
|
|
229
198
|
reconcile_password = getpass("reconcile_password: ") # password from linked reconcile account
|
|
@@ -231,16 +200,16 @@ target_new_password = getpass("new_password: ") # new password for the rotation
|
|
|
231
200
|
|
|
232
201
|
NETHelper.set(
|
|
233
202
|
action=Python4CPM.ACTION_CHANGE, # use actions from Python4CPM.ACTION_*
|
|
234
|
-
target_username="jdoe",
|
|
235
|
-
target_address="myapp.corp.local",
|
|
236
|
-
target_port="8443",
|
|
237
|
-
logon_username="ldoe",
|
|
238
|
-
reconcile_username="rdoe",
|
|
203
|
+
target_username="jdoe", # -> will fall under MyRotator.target_account.username
|
|
204
|
+
target_address="myapp.corp.local", # -> will fall under MyRotator.target_account.address
|
|
205
|
+
target_port="8443", # -> will fall under MyRotator.target_account.port
|
|
206
|
+
logon_username="ldoe", # -> will fall under MyRotator.logon_account.username
|
|
207
|
+
reconcile_username="rdoe", # -> will fall under MyRotator.reconcile_account.username
|
|
239
208
|
logging_level="debug", # "critical", "error", "warning", "info" or "debug"
|
|
240
|
-
target_password=target_password,
|
|
241
|
-
logon_password=logon_password,
|
|
242
|
-
reconcile_password=reconcile_password,
|
|
243
|
-
target_new_password=target_new_password
|
|
209
|
+
target_password=target_password, # -> will fall under MyRotator.target_account.password.get()
|
|
210
|
+
logon_password=logon_password, # -> will fall under MyRotator.logon_account.password.get()
|
|
211
|
+
reconcile_password=reconcile_password, # -> will fall under MyRotator.reconcile_account.password.get()
|
|
212
|
+
target_new_password=target_new_password # -> will fall under MyRotator.target_account.new_password.get()
|
|
244
213
|
)
|
|
245
214
|
|
|
246
215
|
class MyRotator(Python4CPMHandler):
|
|
@@ -271,5 +240,4 @@ MyRotator().run()
|
|
|
271
240
|
|
|
272
241
|
- Remove the import of `NETHelper`.
|
|
273
242
|
- Remove the `NETHelper.set()` call.
|
|
274
|
-
- If applicable, change the definition of `p4cpm` from `p4cpm = NETHelper.get()` to `p4cpm = Python4CPM("MyApp")`.
|
|
275
243
|
- Remove any secrets prompting or interactive interruptions.
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# Python4CPM
|
|
2
2
|
|
|
3
|
-
A simple way of using python scripts with CyberArk CPM/SRS
|
|
3
|
+
A simple and secure way of using python scripts with CyberArk CPM/SRS password rotations.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
This module leverages the [Credential Management .NET SDK](https://docs.cyberark.com/privilege-cloud-standard/latest/en/content/pasimp/plug-in-netinvoker.htm) from CyberArk to securely offload a password rotation logic into Python.
|
|
8
|
+
|
|
9
|
+
All objects are collected from the SDK and sent as environment context to be picked up by the `python4cpm` module during the subprocess execution of python. All secrets of such environment are protected and encrypted by [Data Protection API (DPAPI)](https://learn.microsoft.com/en-us/dotnet/standard/security/how-to-use-data-protection), until they are explicitely retrieved in your python script runtime, invoking the `Secret.get()` method. Finally, python controls the termination signal sent back to the SDK, which is consequently used as the return code to CPM/SRS. Such as a successful or failed (recoverable or not) result of the requested action.
|
|
10
|
+
|
|
11
|
+
This platform allows you to duplicate it multiple times, simply changing its settings (with regular day two operations from Privilege Cloud/PVWA) to point to different venvs and/or python scripts.
|
|
6
12
|
|
|
7
13
|
## Installation
|
|
8
14
|
|
|
@@ -50,7 +56,7 @@ This platform allows you to duplicate it multiple times, simply changing its set
|
|
|
50
56
|
from python4cpm import Python4CPMHandler
|
|
51
57
|
|
|
52
58
|
|
|
53
|
-
class MyRotator(Python4CPMHandler):
|
|
59
|
+
class MyRotator(Python4CPMHandler):
|
|
54
60
|
"""
|
|
55
61
|
These are the usable properties and methods from Python4CPMHandler:
|
|
56
62
|
|
|
@@ -87,9 +93,7 @@ class MyRotator(Python4CPMHandler): # create a subclass for the Handler
|
|
|
87
93
|
self.logger.info("this is an info message")
|
|
88
94
|
self.logger.debug("this is a debug message")
|
|
89
95
|
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
## The logging level comes from PythonLoggingLevel (platform parameters) (default is error)
|
|
96
|
+
# The logging level comes from PythonLoggingLevel (platform parameters) (default is error)
|
|
93
97
|
|
|
94
98
|
=============================
|
|
95
99
|
REQUIRED TERMINATION SIGNALS
|
|
@@ -97,75 +101,44 @@ class MyRotator(Python4CPMHandler): # create a subclass for the Handler
|
|
|
97
101
|
Terminate signals -> MUST use one of the following three signals to terminate the script:
|
|
98
102
|
|
|
99
103
|
self.close_success()
|
|
100
|
-
# terminate with success state
|
|
104
|
+
# terminate and provide CPM/SRS with a success state
|
|
101
105
|
|
|
102
106
|
self.close_fail()
|
|
103
|
-
# terminate with
|
|
107
|
+
# terminate and provide CPM/SRS with a failed recoverable state
|
|
104
108
|
|
|
105
109
|
self.close_fail(unrecoverable=True)
|
|
106
|
-
# terminate with
|
|
110
|
+
# terminate and provide CPM/SRS with a failed unrecoverable state
|
|
107
111
|
|
|
108
112
|
When calling a signal sys.exit is invoked and the script is terminated.
|
|
109
113
|
If no signal is called, and the script finishes without any exception,
|
|
110
114
|
it will behave like p4cpm.close_fail(unrecoverable=True) and log an error message.
|
|
115
|
+
|
|
111
116
|
=============================
|
|
117
|
+
REQUIRED METHODS
|
|
112
118
|
=============================
|
|
119
|
+
verify(), logon(), change(), prereconcile(), reconcile()
|
|
113
120
|
"""
|
|
114
121
|
|
|
115
|
-
# =============================
|
|
116
|
-
# REQUIRED METHODS (MUST DEFINE)
|
|
117
|
-
# =============================
|
|
118
|
-
# verify(), logon(), change(), prereconcile(), reconcile()
|
|
119
|
-
|
|
120
122
|
def verify(self):
|
|
121
|
-
|
|
122
|
-
self.log_info("verification successful")
|
|
123
|
+
# TODO: use account objects for your logic
|
|
123
124
|
self.close_success()
|
|
124
125
|
|
|
125
126
|
def logon(self):
|
|
127
|
+
# TODO: use account objects for your logic
|
|
126
128
|
self.close_success()
|
|
127
129
|
|
|
128
130
|
def change(self):
|
|
129
|
-
|
|
130
|
-
self.
|
|
131
|
-
self.close_fail()
|
|
131
|
+
# TODO: use account objects for your logic
|
|
132
|
+
self.close_success()
|
|
132
133
|
|
|
133
134
|
def prereconcile(self):
|
|
134
|
-
|
|
135
|
+
# TODO: use account objects for your logic
|
|
135
136
|
self.close_success()
|
|
136
137
|
|
|
137
138
|
def reconcile(self):
|
|
138
|
-
|
|
139
|
+
# TODO: use account objects for your logic
|
|
139
140
|
self.close_success()
|
|
140
141
|
|
|
141
|
-
def _verify(self, from_reconcile=False):
|
|
142
|
-
if from_reconcile is False:
|
|
143
|
-
pass
|
|
144
|
-
# TODO: use account objects for your logic
|
|
145
|
-
else:
|
|
146
|
-
pass
|
|
147
|
-
# TODO: use account objects for your logic
|
|
148
|
-
result = True
|
|
149
|
-
if result is True:
|
|
150
|
-
self.log_info("verification successful")
|
|
151
|
-
else:
|
|
152
|
-
self.log_error("something went wrong")
|
|
153
|
-
self.close_fail()
|
|
154
|
-
|
|
155
|
-
def _change(self, from_reconcile=False):
|
|
156
|
-
if from_reconcile is False:
|
|
157
|
-
pass
|
|
158
|
-
# TODO: use account objects for your logic
|
|
159
|
-
else:
|
|
160
|
-
pass
|
|
161
|
-
# TODO: use account objects for your logic
|
|
162
|
-
result = True
|
|
163
|
-
if result is True:
|
|
164
|
-
self.log_info("rotation successful")
|
|
165
|
-
else:
|
|
166
|
-
self.log_error("something went wrong")
|
|
167
|
-
self.close_fail()
|
|
168
|
-
|
|
169
142
|
|
|
170
143
|
if __name__ == "__main__":
|
|
171
144
|
MyRotator().run() # initializes the class and calls the action that was requested from CPM/SRS.
|
|
@@ -178,9 +151,10 @@ When doing `verify`, `change` or `reconcile` from Privilege Cloud/PVWA:
|
|
|
178
151
|
- If both actions are not terminated with `self.close_success()` and the scripts terminates without any exception, CPM/SRS will see this as a `self.close_fail(unrecoverable=True)`.
|
|
179
152
|
3. Reconcile -> the sciprt will be executed twice, running first the `MyRotator.prereconcile()` method and secondly the `MyRotator.reconcile()` method.
|
|
180
153
|
- If both actions are not terminated with `self.close_success()` and the scripts terminates without any exception, CPM/SRS will see this as a `self.close_fail(unrecoverable=True)`.
|
|
181
|
-
4. When calling `MyRotator.verify()`, `MyRotator.logon()` or `MyRotator.prereconcile()`: `self.
|
|
182
|
-
5. If a logon account is not linked, `self.
|
|
183
|
-
6. If a reconcile account is not linked, `self.
|
|
154
|
+
4. When calling `MyRotator.verify()`, `MyRotator.logon()` or `MyRotator.prereconcile()`: `self.target_account.new_password` will always return `None`.
|
|
155
|
+
5. If a logon account is not linked, `self.logon_account` will return `None`.
|
|
156
|
+
6. If a reconcile account is not linked, `self.reconcile_account` will return `None`.
|
|
157
|
+
7. The python `Logger` places its logs in the `Logs/ThirdParty` directory. The filename will be based on the name of the subclass created (e.g., `MyRotator`).
|
|
184
158
|
|
|
185
159
|
|
|
186
160
|
### Installing dependencies in python venv
|
|
@@ -194,14 +168,9 @@ As with any python venv, you can install dependencies in your venv.
|
|
|
194
168
|
|
|
195
169
|
## Dev Helper:
|
|
196
170
|
|
|
197
|
-
For dev purposes, `NETHelper` is a companion helper
|
|
198
|
-
Install this module (in a dev workstation) with:
|
|
199
|
-
|
|
200
|
-
```bash
|
|
201
|
-
pip install python4cpm
|
|
202
|
-
```
|
|
171
|
+
For dev purposes, `NETHelper` is a companion helper to test your scripts without CPM/SRS. It simplifies the instantiation of the `Python4CPM` or `Python4CPMHandler` objects by simulating how the plugin creates the environment context for the python module.
|
|
203
172
|
|
|
204
|
-
**Note**: As CPM
|
|
173
|
+
**Note**: As CPM and the SRS management agent run in Windows, the plugin was built to encrypt secrets using DPAPI (a windows only library). For dev purposes in Linux/Mac dev workstations, those secrets put in the environment context by `NETHelper` will be in plaintext. In windows dev workstations, `NETHelper` encrypts the secrets as the .NET plugin does. This is informational only, **the module will use its encryption/decryption capabilities automatically based on the platform** it is running on and you do not have to do anything specific to enable it.
|
|
205
174
|
|
|
206
175
|
### Example:
|
|
207
176
|
|
|
@@ -212,7 +181,7 @@ from python4cpm import NETHelper, Python4CPM, Python4CPMHandler
|
|
|
212
181
|
from getpass import getpass
|
|
213
182
|
|
|
214
183
|
# Get secrets for your password, logon account password, reconcile account password and new password
|
|
215
|
-
# You
|
|
184
|
+
# You may set to None any argument that does not apply or simply leaving it to its default None value.
|
|
216
185
|
target_password = getpass("password: ") # password from account
|
|
217
186
|
logon_password = getpass("logon_password: ") # password from linked logon account
|
|
218
187
|
reconcile_password = getpass("reconcile_password: ") # password from linked reconcile account
|
|
@@ -220,16 +189,16 @@ target_new_password = getpass("new_password: ") # new password for the rotation
|
|
|
220
189
|
|
|
221
190
|
NETHelper.set(
|
|
222
191
|
action=Python4CPM.ACTION_CHANGE, # use actions from Python4CPM.ACTION_*
|
|
223
|
-
target_username="jdoe",
|
|
224
|
-
target_address="myapp.corp.local",
|
|
225
|
-
target_port="8443",
|
|
226
|
-
logon_username="ldoe",
|
|
227
|
-
reconcile_username="rdoe",
|
|
192
|
+
target_username="jdoe", # -> will fall under MyRotator.target_account.username
|
|
193
|
+
target_address="myapp.corp.local", # -> will fall under MyRotator.target_account.address
|
|
194
|
+
target_port="8443", # -> will fall under MyRotator.target_account.port
|
|
195
|
+
logon_username="ldoe", # -> will fall under MyRotator.logon_account.username
|
|
196
|
+
reconcile_username="rdoe", # -> will fall under MyRotator.reconcile_account.username
|
|
228
197
|
logging_level="debug", # "critical", "error", "warning", "info" or "debug"
|
|
229
|
-
target_password=target_password,
|
|
230
|
-
logon_password=logon_password,
|
|
231
|
-
reconcile_password=reconcile_password,
|
|
232
|
-
target_new_password=target_new_password
|
|
198
|
+
target_password=target_password, # -> will fall under MyRotator.target_account.password.get()
|
|
199
|
+
logon_password=logon_password, # -> will fall under MyRotator.logon_account.password.get()
|
|
200
|
+
reconcile_password=reconcile_password, # -> will fall under MyRotator.reconcile_account.password.get()
|
|
201
|
+
target_new_password=target_new_password # -> will fall under MyRotator.target_account.new_password.get()
|
|
233
202
|
)
|
|
234
203
|
|
|
235
204
|
class MyRotator(Python4CPMHandler):
|
|
@@ -260,5 +229,4 @@ MyRotator().run()
|
|
|
260
229
|
|
|
261
230
|
- Remove the import of `NETHelper`.
|
|
262
231
|
- Remove the `NETHelper.set()` call.
|
|
263
|
-
- If applicable, change the definition of `p4cpm` from `p4cpm = NETHelper.get()` to `p4cpm = Python4CPM("MyApp")`.
|
|
264
232
|
- Remove any secrets prompting or interactive interruptions.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from python4cpm.envhandler import EnvHandler, Props
|
|
2
|
+
from python4cpm.secret import Secret
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BaseAccount(EnvHandler):
|
|
6
|
+
PROPS = Props("username", "password")
|
|
7
|
+
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
username: str | None,
|
|
11
|
+
password: str | None
|
|
12
|
+
) -> None:
|
|
13
|
+
self._username = username
|
|
14
|
+
self._password = Secret.from_env_var(password)
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def get(cls) -> object | None:
|
|
18
|
+
kwargs = cls.get_kwargs()
|
|
19
|
+
if all(value is None for value in kwargs.values()):
|
|
20
|
+
return None
|
|
21
|
+
return cls(**kwargs)
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def username(self) -> str:
|
|
25
|
+
return self._username
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def password(self) -> Secret:
|
|
29
|
+
return self._password
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TargetAccount(BaseAccount):
|
|
33
|
+
OBJ_PREFIX = "target_"
|
|
34
|
+
PROPS = Props("username", "password", "address", "port", "new_password")
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
username: str | None,
|
|
39
|
+
password: str | None,
|
|
40
|
+
address: str | None,
|
|
41
|
+
port: str | None,
|
|
42
|
+
new_password: str | None
|
|
43
|
+
) -> None:
|
|
44
|
+
super().__init__(
|
|
45
|
+
username,
|
|
46
|
+
password
|
|
47
|
+
)
|
|
48
|
+
self._address = address
|
|
49
|
+
self._port = port
|
|
50
|
+
self._new_password = Secret.from_env_var(new_password)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def address(self) -> str | None:
|
|
54
|
+
return self._address
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def port(self) -> str | None:
|
|
58
|
+
return self._port
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def new_password(self) -> Secret:
|
|
62
|
+
return self._new_password
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class LogonAccount(BaseAccount):
|
|
66
|
+
OBJ_PREFIX = "logon_"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ReconcileAccount(BaseAccount):
|
|
70
|
+
OBJ_PREFIX = "reconcile_"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from python4cpm.envhandler import EnvHandler, Props
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Args(EnvHandler):
|
|
5
|
+
OBJ_PREFIX = "args_"
|
|
6
|
+
PROPS = Props("action", "logging_level")
|
|
7
|
+
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
action: str | None,
|
|
11
|
+
logging_level: str | None
|
|
12
|
+
) -> None:
|
|
13
|
+
self._action = action
|
|
14
|
+
self._logging_level = logging_level
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def action(self) -> str:
|
|
18
|
+
return self._action
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def logging_level(self) -> str | None:
|
|
22
|
+
return self._logging_level
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Props:
|
|
5
|
+
def __init__(self, *props):
|
|
6
|
+
for prop in props:
|
|
7
|
+
setattr(self, prop, prop)
|
|
8
|
+
|
|
9
|
+
def __iter__(self) -> iter:
|
|
10
|
+
return iter(self.__dict__.values())
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EnvHandler:
|
|
14
|
+
PREFIX = "python4cpm_"
|
|
15
|
+
OBJ_PREFIX = ""
|
|
16
|
+
PROPS = Props()
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def get_key(cls, key: str) -> str:
|
|
20
|
+
env_key = f"{cls.PREFIX}{cls.OBJ_PREFIX}{key}"
|
|
21
|
+
return env_key.upper()
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def get_kwargs(cls) -> dict:
|
|
25
|
+
return {
|
|
26
|
+
prop: os.environ.get(cls.get_key(prop))
|
|
27
|
+
for prop in cls.PROPS
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def get(cls) -> object:
|
|
32
|
+
return cls(**cls.get_kwargs())
|
|
@@ -18,14 +18,15 @@ class Logger:
|
|
|
18
18
|
def get_logger(
|
|
19
19
|
cls,
|
|
20
20
|
name: str,
|
|
21
|
-
|
|
21
|
+
logging_level: str | None
|
|
22
22
|
) -> logging.Logger:
|
|
23
23
|
os.makedirs(cls._LOGS_DIR, exist_ok=True)
|
|
24
24
|
logs_file = os.path.join(cls._LOGS_DIR, f"{__name__}-{name}.log")
|
|
25
25
|
_id = os.urandom(4).hex()
|
|
26
26
|
logger = logging.getLogger(_id)
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
is_logging_level_str = isinstance(logging_level, str)
|
|
28
|
+
if is_logging_level_str and logging_level.lower() in cls._LOGGING_LEVELS:
|
|
29
|
+
logger.setLevel(cls._LOGGING_LEVELS[logging_level.lower()])
|
|
29
30
|
else:
|
|
30
31
|
logger.setLevel(cls._DEFAULT_LEVEL)
|
|
31
32
|
handler = RotatingFileHandler(
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from python4cpm.python4cpm import Python4CPM
|
|
2
|
+
from python4cpm.args import Args
|
|
3
|
+
from python4cpm.accounts import TargetAccount, LogonAccount, ReconcileAccount
|
|
4
|
+
from python4cpm.crypto import Crypto
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NETHelper:
|
|
9
|
+
@classmethod
|
|
10
|
+
def set(
|
|
11
|
+
cls,
|
|
12
|
+
action: str | None = None,
|
|
13
|
+
logging_level: str | None = None,
|
|
14
|
+
target_username: str | None = None,
|
|
15
|
+
target_address: str | None = None,
|
|
16
|
+
target_port: str | None = None,
|
|
17
|
+
logon_username: str | None = None,
|
|
18
|
+
reconcile_username: str | None = None,
|
|
19
|
+
target_password: str | None = None,
|
|
20
|
+
logon_password: str | None = None,
|
|
21
|
+
reconcile_password: str | None = None,
|
|
22
|
+
target_new_password: str | None = None
|
|
23
|
+
) -> None:
|
|
24
|
+
if Crypto.ENABLED:
|
|
25
|
+
target_password = Crypto.encrypt(target_password)
|
|
26
|
+
logon_password = Crypto.encrypt(logon_password)
|
|
27
|
+
reconcile_password = Crypto.encrypt(reconcile_password)
|
|
28
|
+
target_new_password = Crypto.encrypt(target_new_password)
|
|
29
|
+
keys = (
|
|
30
|
+
Args.get_key(Args.PROPS.action),
|
|
31
|
+
Args.get_key(Args.PROPS.logging_level),
|
|
32
|
+
TargetAccount.get_key(TargetAccount.PROPS.username),
|
|
33
|
+
TargetAccount.get_key(TargetAccount.PROPS.address),
|
|
34
|
+
TargetAccount.get_key(TargetAccount.PROPS.port),
|
|
35
|
+
LogonAccount.get_key(LogonAccount.PROPS.username),
|
|
36
|
+
ReconcileAccount.get_key(ReconcileAccount.PROPS.username),
|
|
37
|
+
TargetAccount.get_key(TargetAccount.PROPS.password),
|
|
38
|
+
LogonAccount.get_key(LogonAccount.PROPS.password),
|
|
39
|
+
ReconcileAccount.get_key(ReconcileAccount.PROPS.password),
|
|
40
|
+
TargetAccount.get_key(TargetAccount.PROPS.new_password)
|
|
41
|
+
)
|
|
42
|
+
values = (
|
|
43
|
+
action,
|
|
44
|
+
logging_level,
|
|
45
|
+
target_username,
|
|
46
|
+
target_address,
|
|
47
|
+
target_port,
|
|
48
|
+
logon_username,
|
|
49
|
+
reconcile_username,
|
|
50
|
+
target_password,
|
|
51
|
+
logon_password,
|
|
52
|
+
reconcile_password,
|
|
53
|
+
target_new_password
|
|
54
|
+
)
|
|
55
|
+
for i, key in enumerate(keys):
|
|
56
|
+
if values[i] is not None:
|
|
57
|
+
os.environ.update({key: values[i]})
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def get(cls) -> Python4CPM:
|
|
61
|
+
return Python4CPM(cls.__name__)
|
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
import os
|
|
2
1
|
import sys
|
|
3
2
|
import atexit
|
|
4
3
|
import logging
|
|
5
4
|
from python4cpm.secret import Secret
|
|
6
5
|
from python4cpm.args import Args
|
|
7
|
-
from python4cpm.crypto import Crypto
|
|
8
6
|
from python4cpm.logger import Logger
|
|
9
|
-
from python4cpm.accounts import
|
|
10
|
-
|
|
11
|
-
TargetAccount,
|
|
12
|
-
LogonAccount,
|
|
13
|
-
ReconcileAccount
|
|
14
|
-
)
|
|
7
|
+
from python4cpm.accounts import TargetAccount, LogonAccount, ReconcileAccount
|
|
8
|
+
|
|
15
9
|
|
|
16
10
|
class Python4CPM:
|
|
17
11
|
ACTION_VERIFY = "verifypass"
|
|
@@ -29,18 +23,17 @@ class Python4CPM:
|
|
|
29
23
|
_SUCCESS_CODE = 10
|
|
30
24
|
_FAILED_RECOVERABLE_CODE = 81
|
|
31
25
|
_FAILED_UNRECOVERABLE_CODE = 89
|
|
32
|
-
_ENV_PREFIX = "PYTHON4CPM_"
|
|
33
26
|
|
|
34
27
|
def __init__(self, name: str) -> None:
|
|
35
28
|
self._name = name
|
|
36
|
-
self._args =
|
|
29
|
+
self._args = Args.get()
|
|
30
|
+
self._target_account = TargetAccount.get()
|
|
31
|
+
self._logon_account = LogonAccount.get()
|
|
32
|
+
self._reconcile_account = ReconcileAccount.get()
|
|
37
33
|
self._logger = Logger.get_logger(self._name, self._args.logging_level)
|
|
38
34
|
self._logger.debug("Initiating...")
|
|
39
35
|
self._log_obj(self._args)
|
|
40
36
|
self._verify_action()
|
|
41
|
-
self._target_account = self._get_account(TargetAccount)
|
|
42
|
-
self._logon_account = self._get_account(LogonAccount)
|
|
43
|
-
self._reconcile_account = self._get_account(ReconcileAccount)
|
|
44
37
|
self._log_obj(self._target_account)
|
|
45
38
|
self._log_obj(self._logon_account)
|
|
46
39
|
self._log_obj(self._reconcile_account)
|
|
@@ -67,43 +60,22 @@ class Python4CPM:
|
|
|
67
60
|
def reconcile_account(self) -> ReconcileAccount:
|
|
68
61
|
return self._reconcile_account
|
|
69
62
|
|
|
70
|
-
@classmethod
|
|
71
|
-
def _get_env_key(cls, key: str) -> str:
|
|
72
|
-
return f"{cls._ENV_PREFIX}{key.upper()}"
|
|
73
|
-
|
|
74
|
-
@classmethod
|
|
75
|
-
def _get_args(cls) -> Args:
|
|
76
|
-
kwargs = {}
|
|
77
|
-
for kwarg in Args.ARGS:
|
|
78
|
-
_kwarg = os.environ.get(cls._get_env_key(kwarg))
|
|
79
|
-
kwargs[kwarg] = _kwarg if _kwarg is not None else ""
|
|
80
|
-
return Args(**kwargs)
|
|
81
|
-
|
|
82
|
-
def _get_account(self, account_class: BaseAccount) -> BaseAccount:
|
|
83
|
-
args = []
|
|
84
|
-
for arg in account_class.ENV_VARS:
|
|
85
|
-
_arg = os.environ.get(self._get_env_key(arg))
|
|
86
|
-
args.append(_arg if _arg is not None else "")
|
|
87
|
-
return account_class(*args)
|
|
88
|
-
|
|
89
63
|
def _verify_action(self) -> None:
|
|
90
64
|
if self._args.action not in self._VALID_ACTIONS:
|
|
91
65
|
self._logger.warning(f"Unkonwn action -> '{self._args.action}'")
|
|
92
66
|
|
|
93
|
-
def _log_obj(self, obj: object) -> None:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if not
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if Crypto.ENABLED is True:
|
|
101
|
-
logging_value = "[ENCRYPTED]"
|
|
67
|
+
def _log_obj(self, obj: object | None) -> None:
|
|
68
|
+
if obj is not None:
|
|
69
|
+
for key, value in vars(obj).items():
|
|
70
|
+
_key = f"{obj.__class__.__name__}.{key.removeprefix('_')}"
|
|
71
|
+
if value is not None:
|
|
72
|
+
if not isinstance(value, Secret):
|
|
73
|
+
logging_value = f"'{value}'"
|
|
102
74
|
else:
|
|
103
|
-
logging_value =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
75
|
+
logging_value = str(value)
|
|
76
|
+
else:
|
|
77
|
+
logging_value = "[NOT SET]"
|
|
78
|
+
self._logger.debug(f"{_key} -> {logging_value}")
|
|
107
79
|
|
|
108
80
|
def close_fail(self, unrecoverable: bool = False) -> None:
|
|
109
81
|
if unrecoverable is False:
|
|
@@ -18,7 +18,7 @@ class Python4CPMHandler(ABC, Python4CPM):
|
|
|
18
18
|
if action is not None:
|
|
19
19
|
action()
|
|
20
20
|
else:
|
|
21
|
-
raise ValueError(f"Unknown action: {self._args.action}")
|
|
21
|
+
raise ValueError(f"Unknown action: '{self._args.action}'")
|
|
22
22
|
|
|
23
23
|
@abstractmethod
|
|
24
24
|
def verify(self) -> None:
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from python4cpm.crypto import Crypto
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Secret:
|
|
5
|
+
def __init__(self, secret: str) -> None:
|
|
6
|
+
self._secret = secret
|
|
7
|
+
|
|
8
|
+
@classmethod
|
|
9
|
+
def from_env_var(cls, secret: str | None) -> object | None:
|
|
10
|
+
if secret is not None:
|
|
11
|
+
return cls(secret)
|
|
12
|
+
return None
|
|
13
|
+
|
|
14
|
+
def __str__(self) -> str:
|
|
15
|
+
if Crypto.ENABLED:
|
|
16
|
+
return "[ENCRYPTED]"
|
|
17
|
+
else:
|
|
18
|
+
return "[SET]"
|
|
19
|
+
|
|
20
|
+
def get(self) -> str:
|
|
21
|
+
if Crypto.ENABLED:
|
|
22
|
+
return Crypto.decrypt(self._secret)
|
|
23
|
+
else:
|
|
24
|
+
return self._secret
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python4cpm
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.3
|
|
4
4
|
Summary: Python for CPM
|
|
5
5
|
Author-email: Gonzalo Atienza Rela <gonatienza@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -11,9 +11,15 @@ Dynamic: license-file
|
|
|
11
11
|
|
|
12
12
|
# Python4CPM
|
|
13
13
|
|
|
14
|
-
A simple way of using python scripts with CyberArk CPM/SRS
|
|
14
|
+
A simple and secure way of using python scripts with CyberArk CPM/SRS password rotations.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
## How it works
|
|
17
|
+
|
|
18
|
+
This module leverages the [Credential Management .NET SDK](https://docs.cyberark.com/privilege-cloud-standard/latest/en/content/pasimp/plug-in-netinvoker.htm) from CyberArk to securely offload a password rotation logic into Python.
|
|
19
|
+
|
|
20
|
+
All objects are collected from the SDK and sent as environment context to be picked up by the `python4cpm` module during the subprocess execution of python. All secrets of such environment are protected and encrypted by [Data Protection API (DPAPI)](https://learn.microsoft.com/en-us/dotnet/standard/security/how-to-use-data-protection), until they are explicitely retrieved in your python script runtime, invoking the `Secret.get()` method. Finally, python controls the termination signal sent back to the SDK, which is consequently used as the return code to CPM/SRS. Such as a successful or failed (recoverable or not) result of the requested action.
|
|
21
|
+
|
|
22
|
+
This platform allows you to duplicate it multiple times, simply changing its settings (with regular day two operations from Privilege Cloud/PVWA) to point to different venvs and/or python scripts.
|
|
17
23
|
|
|
18
24
|
## Installation
|
|
19
25
|
|
|
@@ -61,7 +67,7 @@ This platform allows you to duplicate it multiple times, simply changing its set
|
|
|
61
67
|
from python4cpm import Python4CPMHandler
|
|
62
68
|
|
|
63
69
|
|
|
64
|
-
class MyRotator(Python4CPMHandler):
|
|
70
|
+
class MyRotator(Python4CPMHandler):
|
|
65
71
|
"""
|
|
66
72
|
These are the usable properties and methods from Python4CPMHandler:
|
|
67
73
|
|
|
@@ -98,9 +104,7 @@ class MyRotator(Python4CPMHandler): # create a subclass for the Handler
|
|
|
98
104
|
self.logger.info("this is an info message")
|
|
99
105
|
self.logger.debug("this is a debug message")
|
|
100
106
|
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
## The logging level comes from PythonLoggingLevel (platform parameters) (default is error)
|
|
107
|
+
# The logging level comes from PythonLoggingLevel (platform parameters) (default is error)
|
|
104
108
|
|
|
105
109
|
=============================
|
|
106
110
|
REQUIRED TERMINATION SIGNALS
|
|
@@ -108,75 +112,44 @@ class MyRotator(Python4CPMHandler): # create a subclass for the Handler
|
|
|
108
112
|
Terminate signals -> MUST use one of the following three signals to terminate the script:
|
|
109
113
|
|
|
110
114
|
self.close_success()
|
|
111
|
-
# terminate with success state
|
|
115
|
+
# terminate and provide CPM/SRS with a success state
|
|
112
116
|
|
|
113
117
|
self.close_fail()
|
|
114
|
-
# terminate with
|
|
118
|
+
# terminate and provide CPM/SRS with a failed recoverable state
|
|
115
119
|
|
|
116
120
|
self.close_fail(unrecoverable=True)
|
|
117
|
-
# terminate with
|
|
121
|
+
# terminate and provide CPM/SRS with a failed unrecoverable state
|
|
118
122
|
|
|
119
123
|
When calling a signal sys.exit is invoked and the script is terminated.
|
|
120
124
|
If no signal is called, and the script finishes without any exception,
|
|
121
125
|
it will behave like p4cpm.close_fail(unrecoverable=True) and log an error message.
|
|
126
|
+
|
|
122
127
|
=============================
|
|
128
|
+
REQUIRED METHODS
|
|
123
129
|
=============================
|
|
130
|
+
verify(), logon(), change(), prereconcile(), reconcile()
|
|
124
131
|
"""
|
|
125
132
|
|
|
126
|
-
# =============================
|
|
127
|
-
# REQUIRED METHODS (MUST DEFINE)
|
|
128
|
-
# =============================
|
|
129
|
-
# verify(), logon(), change(), prereconcile(), reconcile()
|
|
130
|
-
|
|
131
133
|
def verify(self):
|
|
132
|
-
|
|
133
|
-
self.log_info("verification successful")
|
|
134
|
+
# TODO: use account objects for your logic
|
|
134
135
|
self.close_success()
|
|
135
136
|
|
|
136
137
|
def logon(self):
|
|
138
|
+
# TODO: use account objects for your logic
|
|
137
139
|
self.close_success()
|
|
138
140
|
|
|
139
141
|
def change(self):
|
|
140
|
-
|
|
141
|
-
self.
|
|
142
|
-
self.close_fail()
|
|
142
|
+
# TODO: use account objects for your logic
|
|
143
|
+
self.close_success()
|
|
143
144
|
|
|
144
145
|
def prereconcile(self):
|
|
145
|
-
|
|
146
|
+
# TODO: use account objects for your logic
|
|
146
147
|
self.close_success()
|
|
147
148
|
|
|
148
149
|
def reconcile(self):
|
|
149
|
-
|
|
150
|
+
# TODO: use account objects for your logic
|
|
150
151
|
self.close_success()
|
|
151
152
|
|
|
152
|
-
def _verify(self, from_reconcile=False):
|
|
153
|
-
if from_reconcile is False:
|
|
154
|
-
pass
|
|
155
|
-
# TODO: use account objects for your logic
|
|
156
|
-
else:
|
|
157
|
-
pass
|
|
158
|
-
# TODO: use account objects for your logic
|
|
159
|
-
result = True
|
|
160
|
-
if result is True:
|
|
161
|
-
self.log_info("verification successful")
|
|
162
|
-
else:
|
|
163
|
-
self.log_error("something went wrong")
|
|
164
|
-
self.close_fail()
|
|
165
|
-
|
|
166
|
-
def _change(self, from_reconcile=False):
|
|
167
|
-
if from_reconcile is False:
|
|
168
|
-
pass
|
|
169
|
-
# TODO: use account objects for your logic
|
|
170
|
-
else:
|
|
171
|
-
pass
|
|
172
|
-
# TODO: use account objects for your logic
|
|
173
|
-
result = True
|
|
174
|
-
if result is True:
|
|
175
|
-
self.log_info("rotation successful")
|
|
176
|
-
else:
|
|
177
|
-
self.log_error("something went wrong")
|
|
178
|
-
self.close_fail()
|
|
179
|
-
|
|
180
153
|
|
|
181
154
|
if __name__ == "__main__":
|
|
182
155
|
MyRotator().run() # initializes the class and calls the action that was requested from CPM/SRS.
|
|
@@ -189,9 +162,10 @@ When doing `verify`, `change` or `reconcile` from Privilege Cloud/PVWA:
|
|
|
189
162
|
- If both actions are not terminated with `self.close_success()` and the scripts terminates without any exception, CPM/SRS will see this as a `self.close_fail(unrecoverable=True)`.
|
|
190
163
|
3. Reconcile -> the sciprt will be executed twice, running first the `MyRotator.prereconcile()` method and secondly the `MyRotator.reconcile()` method.
|
|
191
164
|
- If both actions are not terminated with `self.close_success()` and the scripts terminates without any exception, CPM/SRS will see this as a `self.close_fail(unrecoverable=True)`.
|
|
192
|
-
4. When calling `MyRotator.verify()`, `MyRotator.logon()` or `MyRotator.prereconcile()`: `self.
|
|
193
|
-
5. If a logon account is not linked, `self.
|
|
194
|
-
6. If a reconcile account is not linked, `self.
|
|
165
|
+
4. When calling `MyRotator.verify()`, `MyRotator.logon()` or `MyRotator.prereconcile()`: `self.target_account.new_password` will always return `None`.
|
|
166
|
+
5. If a logon account is not linked, `self.logon_account` will return `None`.
|
|
167
|
+
6. If a reconcile account is not linked, `self.reconcile_account` will return `None`.
|
|
168
|
+
7. The python `Logger` places its logs in the `Logs/ThirdParty` directory. The filename will be based on the name of the subclass created (e.g., `MyRotator`).
|
|
195
169
|
|
|
196
170
|
|
|
197
171
|
### Installing dependencies in python venv
|
|
@@ -205,14 +179,9 @@ As with any python venv, you can install dependencies in your venv.
|
|
|
205
179
|
|
|
206
180
|
## Dev Helper:
|
|
207
181
|
|
|
208
|
-
For dev purposes, `NETHelper` is a companion helper
|
|
209
|
-
Install this module (in a dev workstation) with:
|
|
210
|
-
|
|
211
|
-
```bash
|
|
212
|
-
pip install python4cpm
|
|
213
|
-
```
|
|
182
|
+
For dev purposes, `NETHelper` is a companion helper to test your scripts without CPM/SRS. It simplifies the instantiation of the `Python4CPM` or `Python4CPMHandler` objects by simulating how the plugin creates the environment context for the python module.
|
|
214
183
|
|
|
215
|
-
**Note**: As CPM
|
|
184
|
+
**Note**: As CPM and the SRS management agent run in Windows, the plugin was built to encrypt secrets using DPAPI (a windows only library). For dev purposes in Linux/Mac dev workstations, those secrets put in the environment context by `NETHelper` will be in plaintext. In windows dev workstations, `NETHelper` encrypts the secrets as the .NET plugin does. This is informational only, **the module will use its encryption/decryption capabilities automatically based on the platform** it is running on and you do not have to do anything specific to enable it.
|
|
216
185
|
|
|
217
186
|
### Example:
|
|
218
187
|
|
|
@@ -223,7 +192,7 @@ from python4cpm import NETHelper, Python4CPM, Python4CPMHandler
|
|
|
223
192
|
from getpass import getpass
|
|
224
193
|
|
|
225
194
|
# Get secrets for your password, logon account password, reconcile account password and new password
|
|
226
|
-
# You
|
|
195
|
+
# You may set to None any argument that does not apply or simply leaving it to its default None value.
|
|
227
196
|
target_password = getpass("password: ") # password from account
|
|
228
197
|
logon_password = getpass("logon_password: ") # password from linked logon account
|
|
229
198
|
reconcile_password = getpass("reconcile_password: ") # password from linked reconcile account
|
|
@@ -231,16 +200,16 @@ target_new_password = getpass("new_password: ") # new password for the rotation
|
|
|
231
200
|
|
|
232
201
|
NETHelper.set(
|
|
233
202
|
action=Python4CPM.ACTION_CHANGE, # use actions from Python4CPM.ACTION_*
|
|
234
|
-
target_username="jdoe",
|
|
235
|
-
target_address="myapp.corp.local",
|
|
236
|
-
target_port="8443",
|
|
237
|
-
logon_username="ldoe",
|
|
238
|
-
reconcile_username="rdoe",
|
|
203
|
+
target_username="jdoe", # -> will fall under MyRotator.target_account.username
|
|
204
|
+
target_address="myapp.corp.local", # -> will fall under MyRotator.target_account.address
|
|
205
|
+
target_port="8443", # -> will fall under MyRotator.target_account.port
|
|
206
|
+
logon_username="ldoe", # -> will fall under MyRotator.logon_account.username
|
|
207
|
+
reconcile_username="rdoe", # -> will fall under MyRotator.reconcile_account.username
|
|
239
208
|
logging_level="debug", # "critical", "error", "warning", "info" or "debug"
|
|
240
|
-
target_password=target_password,
|
|
241
|
-
logon_password=logon_password,
|
|
242
|
-
reconcile_password=reconcile_password,
|
|
243
|
-
target_new_password=target_new_password
|
|
209
|
+
target_password=target_password, # -> will fall under MyRotator.target_account.password.get()
|
|
210
|
+
logon_password=logon_password, # -> will fall under MyRotator.logon_account.password.get()
|
|
211
|
+
reconcile_password=reconcile_password, # -> will fall under MyRotator.reconcile_account.password.get()
|
|
212
|
+
target_new_password=target_new_password # -> will fall under MyRotator.target_account.new_password.get()
|
|
244
213
|
)
|
|
245
214
|
|
|
246
215
|
class MyRotator(Python4CPMHandler):
|
|
@@ -271,5 +240,4 @@ MyRotator().run()
|
|
|
271
240
|
|
|
272
241
|
- Remove the import of `NETHelper`.
|
|
273
242
|
- Remove the `NETHelper.set()` call.
|
|
274
|
-
- If applicable, change the definition of `p4cpm` from `p4cpm = NETHelper.get()` to `p4cpm = Python4CPM("MyApp")`.
|
|
275
243
|
- Remove any secrets prompting or interactive interruptions.
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
from python4cpm.secret import Secret
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class BaseAccount:
|
|
5
|
-
def __init__(
|
|
6
|
-
self: str,
|
|
7
|
-
username: str,
|
|
8
|
-
password: str
|
|
9
|
-
) -> None:
|
|
10
|
-
self._username = username
|
|
11
|
-
self._password = Secret(password)
|
|
12
|
-
|
|
13
|
-
@property
|
|
14
|
-
def username(self) -> str:
|
|
15
|
-
return self._username
|
|
16
|
-
|
|
17
|
-
@property
|
|
18
|
-
def password(self) -> Secret:
|
|
19
|
-
return self._password
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class TargetAccount(BaseAccount):
|
|
23
|
-
ENV_VARS = (
|
|
24
|
-
"target_username",
|
|
25
|
-
"target_password",
|
|
26
|
-
"target_address",
|
|
27
|
-
"target_port",
|
|
28
|
-
"target_new_password"
|
|
29
|
-
)
|
|
30
|
-
def __init__(
|
|
31
|
-
self: str,
|
|
32
|
-
username: str,
|
|
33
|
-
password: str,
|
|
34
|
-
address: str,
|
|
35
|
-
port: str,
|
|
36
|
-
new_password: str
|
|
37
|
-
) -> None:
|
|
38
|
-
super().__init__(
|
|
39
|
-
username,
|
|
40
|
-
password
|
|
41
|
-
)
|
|
42
|
-
self._address = address
|
|
43
|
-
self._port = port
|
|
44
|
-
self._new_password = Secret(new_password)
|
|
45
|
-
|
|
46
|
-
@property
|
|
47
|
-
def address(self) -> str:
|
|
48
|
-
return self._address
|
|
49
|
-
|
|
50
|
-
@property
|
|
51
|
-
def port(self) -> str:
|
|
52
|
-
return self._port
|
|
53
|
-
|
|
54
|
-
@property
|
|
55
|
-
def new_password(self) -> Secret:
|
|
56
|
-
return self._new_password
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class LogonAccount(BaseAccount):
|
|
60
|
-
ENV_VARS = (
|
|
61
|
-
"logon_username",
|
|
62
|
-
"logon_password"
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
class ReconcileAccount(BaseAccount):
|
|
67
|
-
ENV_VARS = (
|
|
68
|
-
"reconcile_username",
|
|
69
|
-
"reconcile_password"
|
|
70
|
-
)
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
class Args:
|
|
2
|
-
ARGS = (
|
|
3
|
-
"action",
|
|
4
|
-
"logging_level"
|
|
5
|
-
)
|
|
6
|
-
|
|
7
|
-
def __init__(
|
|
8
|
-
self: str,
|
|
9
|
-
action: str,
|
|
10
|
-
logging_level: str
|
|
11
|
-
) -> None:
|
|
12
|
-
self._action = action
|
|
13
|
-
self._logging_level = logging_level
|
|
14
|
-
|
|
15
|
-
@property
|
|
16
|
-
def action(self) -> str:
|
|
17
|
-
return self._action
|
|
18
|
-
|
|
19
|
-
@property
|
|
20
|
-
def logging_level(self) -> str:
|
|
21
|
-
return self._logging_level
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
from python4cpm.python4cpm import Python4CPM
|
|
2
|
-
from python4cpm.crypto import Crypto
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class NETHelper:
|
|
7
|
-
@classmethod
|
|
8
|
-
def set(
|
|
9
|
-
cls,
|
|
10
|
-
action: str = "",
|
|
11
|
-
logging_level: str = "",
|
|
12
|
-
target_username: str = "",
|
|
13
|
-
target_address: str = "",
|
|
14
|
-
target_port: str = "",
|
|
15
|
-
logon_username: str = "",
|
|
16
|
-
reconcile_username: str = "",
|
|
17
|
-
target_password: str = "",
|
|
18
|
-
logon_password: str = "",
|
|
19
|
-
reconcile_password: str = "",
|
|
20
|
-
target_new_password: str = ""
|
|
21
|
-
) -> None:
|
|
22
|
-
if Crypto.ENABLED:
|
|
23
|
-
target_password = Crypto.encrypt(target_password)
|
|
24
|
-
logon_password = Crypto.encrypt(logon_password)
|
|
25
|
-
reconcile_password = Crypto.encrypt(reconcile_password)
|
|
26
|
-
target_new_password = Crypto.encrypt(target_new_password)
|
|
27
|
-
env = {
|
|
28
|
-
"ACTION": action,
|
|
29
|
-
"LOGGING_LEVEL": logging_level,
|
|
30
|
-
"TARGET_USERNAME": target_username,
|
|
31
|
-
"TARGET_ADDRESS": target_address,
|
|
32
|
-
"TARGET_PORT": target_port,
|
|
33
|
-
"LOGON_USERNAME": logon_username,
|
|
34
|
-
"RECONCILE_USERNAME": reconcile_username,
|
|
35
|
-
"TARGET_PASSWORD": target_password,
|
|
36
|
-
"LOGON_PASSWORD": logon_password,
|
|
37
|
-
"RECONCILE_PASSWORD": reconcile_password,
|
|
38
|
-
"TARGET_NEW_PASSWORD": target_new_password
|
|
39
|
-
}
|
|
40
|
-
for key, value in env.items():
|
|
41
|
-
env_var = Python4CPM._ENV_PREFIX + key
|
|
42
|
-
os.environ[env_var] = value
|
|
43
|
-
|
|
44
|
-
@classmethod
|
|
45
|
-
def get(cls) -> Python4CPM:
|
|
46
|
-
return Python4CPM(cls.__name__)
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
from python4cpm.crypto import Crypto
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class Secret:
|
|
5
|
-
def __init__(self, secret: str) -> None:
|
|
6
|
-
self._secret = secret
|
|
7
|
-
|
|
8
|
-
def __bool__(self) -> bool:
|
|
9
|
-
return bool(self._secret)
|
|
10
|
-
|
|
11
|
-
def get(self) -> str:
|
|
12
|
-
if Crypto.ENABLED and self._secret:
|
|
13
|
-
return Crypto.decrypt(self._secret)
|
|
14
|
-
else:
|
|
15
|
-
return self._secret
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|