python4cpm 1.1.2__tar.gz → 1.1.4__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.2/src/python4cpm.egg-info → python4cpm-1.1.4}/PKG-INFO +72 -131
- {python4cpm-1.1.2 → python4cpm-1.1.4}/README.md +71 -130
- {python4cpm-1.1.2 → python4cpm-1.1.4}/pyproject.toml +1 -1
- python4cpm-1.1.4/src/python4cpm/accounts.py +90 -0
- {python4cpm-1.1.2 → python4cpm-1.1.4}/src/python4cpm/args.py +4 -4
- {python4cpm-1.1.2 → python4cpm-1.1.4}/src/python4cpm/envhandler.py +8 -5
- {python4cpm-1.1.2 → python4cpm-1.1.4}/src/python4cpm/logger.py +10 -9
- {python4cpm-1.1.2 → python4cpm-1.1.4}/src/python4cpm/nethelper.py +20 -13
- {python4cpm-1.1.2 → python4cpm-1.1.4}/src/python4cpm/python4cpm.py +16 -17
- {python4cpm-1.1.2 → python4cpm-1.1.4}/src/python4cpm/python4cpmhandler.py +1 -4
- python4cpm-1.1.4/src/python4cpm/secret.py +24 -0
- {python4cpm-1.1.2 → python4cpm-1.1.4/src/python4cpm.egg-info}/PKG-INFO +72 -131
- python4cpm-1.1.2/src/python4cpm/accounts.py +0 -63
- python4cpm-1.1.2/src/python4cpm/secret.py +0 -15
- {python4cpm-1.1.2 → python4cpm-1.1.4}/LICENSE +0 -0
- {python4cpm-1.1.2 → python4cpm-1.1.4}/setup.cfg +0 -0
- {python4cpm-1.1.2 → python4cpm-1.1.4}/src/python4cpm/__init__.py +0 -0
- {python4cpm-1.1.2 → python4cpm-1.1.4}/src/python4cpm/crypto.py +0 -0
- {python4cpm-1.1.2 → python4cpm-1.1.4}/src/python4cpm.egg-info/SOURCES.txt +0 -0
- {python4cpm-1.1.2 → python4cpm-1.1.4}/src/python4cpm.egg-info/dependency_links.txt +0 -0
- {python4cpm-1.1.2 → python4cpm-1.1.4}/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.4
|
|
4
4
|
Summary: Python for CPM
|
|
5
5
|
Author-email: Gonzalo Atienza Rela <gonatienza@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -19,7 +19,7 @@ This module leverages the [Credential Management .NET SDK](https://docs.cyberark
|
|
|
19
19
|
|
|
20
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
21
|
|
|
22
|
-
This platform allows you to duplicate it multiple times, simply changing its settings (
|
|
22
|
+
This platform allows you to duplicate it multiple times, simply changing its settings (from Privilege Cloud/PVWA) to point to different venvs and/or python scripts.
|
|
23
23
|
|
|
24
24
|
## Installation
|
|
25
25
|
|
|
@@ -67,136 +67,85 @@ This platform allows you to duplicate it multiple times, simply changing its set
|
|
|
67
67
|
from python4cpm import Python4CPMHandler
|
|
68
68
|
|
|
69
69
|
|
|
70
|
-
class
|
|
70
|
+
class CredManager(Python4CPMHandler):
|
|
71
71
|
"""
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
## Reconcile Account
|
|
96
|
-
self.reconcile_account.username
|
|
97
|
-
self.reconcile_account.password.get()
|
|
98
|
-
|
|
99
|
-
## Logging
|
|
100
|
-
|
|
101
|
-
self.logger.critical("this is critical message")
|
|
102
|
-
self.logger.error("this is an error message")
|
|
103
|
-
self.logger.warning("this is a warning message")
|
|
104
|
-
self.logger.info("this is an info message")
|
|
105
|
-
self.logger.debug("this is a debug message")
|
|
106
|
-
|
|
107
|
-
# The logging level comes from PythonLoggingLevel (platform parameters) (default is error)
|
|
108
|
-
|
|
109
|
-
=============================
|
|
110
|
-
REQUIRED TERMINATION SIGNALS
|
|
111
|
-
=============================
|
|
112
|
-
Terminate signals -> MUST use one of the following three signals to terminate the script:
|
|
113
|
-
|
|
114
|
-
self.close_success()
|
|
115
|
-
# terminate and provide CPM/SRS with a success state
|
|
116
|
-
|
|
117
|
-
self.close_fail()
|
|
118
|
-
# terminate and provide CPM/SRS with a failed recoverable state
|
|
119
|
-
|
|
120
|
-
self.close_fail(unrecoverable=True)
|
|
121
|
-
# terminate and provide CPM/SRS with a failed unrecoverable state
|
|
122
|
-
|
|
123
|
-
When calling a signal sys.exit is invoked and the script is terminated.
|
|
124
|
-
If no signal is called, and the script finishes without any exception,
|
|
125
|
-
it will behave like p4cpm.close_fail(unrecoverable=True) and log an error message.
|
|
126
|
-
=============================
|
|
127
|
-
=============================
|
|
72
|
+
Properties:
|
|
73
|
+
target_account (TargetAccount): Account being managed.
|
|
74
|
+
.policy_id (str): Platform name.
|
|
75
|
+
.object_name (str): Account name.
|
|
76
|
+
.username (str): Account username.
|
|
77
|
+
.address (str): Target address.
|
|
78
|
+
.port (str): Target port.
|
|
79
|
+
.password (Secret): Current password. Call .get() to retrieve value.
|
|
80
|
+
.new_password (Secret): Replacement password. Call .get() to retrieve value.
|
|
81
|
+
|
|
82
|
+
logon_account (LogonAccount): Linked Logon Account.
|
|
83
|
+
.username (str): Account username.
|
|
84
|
+
.password (Secret): Logon password. Call .get() to retrieve value.
|
|
85
|
+
|
|
86
|
+
reconcile_account (ReconcileAccount): Linked Reconcile Account.
|
|
87
|
+
.username (str): Account username.
|
|
88
|
+
.password (Secret): Reconcile password. Call .get() to retrieve value.
|
|
89
|
+
|
|
90
|
+
logger (logging.Logger): Logger instance.
|
|
91
|
+
|
|
92
|
+
Methods:
|
|
93
|
+
close_success(): Signal successful completion and terminate.
|
|
94
|
+
close_fail(): Signal failed completion and terminate.
|
|
128
95
|
"""
|
|
129
96
|
|
|
130
|
-
# =============================
|
|
131
|
-
# REQUIRED METHODS (MUST DEFINE)
|
|
132
|
-
# =============================
|
|
133
|
-
# verify(), logon(), change(), prereconcile(), reconcile()
|
|
134
97
|
|
|
135
98
|
def verify(self):
|
|
136
|
-
|
|
137
|
-
|
|
99
|
+
"""
|
|
100
|
+
REQUIRED METHOD
|
|
101
|
+
"""
|
|
102
|
+
# TODO: use account objects for your logic
|
|
138
103
|
self.close_success()
|
|
139
104
|
|
|
140
105
|
def logon(self):
|
|
106
|
+
"""
|
|
107
|
+
REQUIRED METHOD
|
|
108
|
+
"""
|
|
109
|
+
# TODO: use account objects for your logic
|
|
141
110
|
self.close_success()
|
|
142
111
|
|
|
143
112
|
def change(self):
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
113
|
+
"""
|
|
114
|
+
REQUIRED METHOD
|
|
115
|
+
"""
|
|
116
|
+
# TODO: use account objects for your logic
|
|
117
|
+
self.close_success()
|
|
147
118
|
|
|
148
119
|
def prereconcile(self):
|
|
149
|
-
|
|
120
|
+
"""
|
|
121
|
+
REQUIRED METHOD
|
|
122
|
+
"""
|
|
123
|
+
# TODO: use account objects for your logic
|
|
150
124
|
self.close_success()
|
|
151
125
|
|
|
152
126
|
def reconcile(self):
|
|
153
|
-
|
|
127
|
+
"""
|
|
128
|
+
REQUIRED METHOD
|
|
129
|
+
"""
|
|
130
|
+
# TODO: use account objects for your logic
|
|
154
131
|
self.close_success()
|
|
155
132
|
|
|
156
|
-
def _verify(self, from_reconcile=False):
|
|
157
|
-
if from_reconcile is False:
|
|
158
|
-
pass
|
|
159
|
-
# TODO: use account objects for your logic
|
|
160
|
-
else:
|
|
161
|
-
pass
|
|
162
|
-
# TODO: use account objects for your logic
|
|
163
|
-
result = True
|
|
164
|
-
if result is True:
|
|
165
|
-
self.log_info("verification successful")
|
|
166
|
-
else:
|
|
167
|
-
self.log_error("something went wrong")
|
|
168
|
-
self.close_fail()
|
|
169
|
-
|
|
170
|
-
def _change(self, from_reconcile=False):
|
|
171
|
-
if from_reconcile is False:
|
|
172
|
-
pass
|
|
173
|
-
# TODO: use account objects for your logic
|
|
174
|
-
else:
|
|
175
|
-
pass
|
|
176
|
-
# TODO: use account objects for your logic
|
|
177
|
-
result = True
|
|
178
|
-
if result is True:
|
|
179
|
-
self.log_info("rotation successful")
|
|
180
|
-
else:
|
|
181
|
-
self.log_error("something went wrong")
|
|
182
|
-
self.close_fail()
|
|
183
|
-
|
|
184
133
|
|
|
185
134
|
if __name__ == "__main__":
|
|
186
|
-
|
|
135
|
+
CredManager().run() # initializes the class and calls the action that was requested from CPM/SRS.
|
|
187
136
|
```
|
|
188
137
|
(*) More realistic examples can be found [here](https://github.com/gonatienza/python4cpm/blob/main/examples).
|
|
189
138
|
|
|
190
139
|
When doing `verify`, `change` or `reconcile` from Privilege Cloud/PVWA:
|
|
191
|
-
1. Verify -> the
|
|
192
|
-
2. Change -> the
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
140
|
+
1. Verify -> the script will be executed once running the `verify()` method.
|
|
141
|
+
2. Change -> the script will be executed twice: first `logon()`, then `change()`.
|
|
142
|
+
3. Reconcile -> the script will be executed twice: first `prereconcile()`, then `reconcile()`.
|
|
143
|
+
4. When calling `verify()`, `logon()` or `prereconcile()`: `target_account.new_password` will always return `None`.
|
|
144
|
+
5. If a logon account is not linked, `logon_account` will return `None`.
|
|
145
|
+
6. If a reconcile account is not linked, `reconcile_account` will return `None`.
|
|
146
|
+
7. Always use the `close_success` or `close_fail` methods to signal the proper termination for all actions.
|
|
147
|
+
- If any action is not terminated with a termination method, CPM/SRS will see this as a `close_fail(unrecoverable=True)`, even if no exceptions are raised.
|
|
148
|
+
8. The python `Logger` places its logs in the `Logs/ThirdParty` directory. The filename will be based on `target_account.policy_id` and `target_account.object_name`.
|
|
200
149
|
|
|
201
150
|
|
|
202
151
|
### Installing dependencies in python venv
|
|
@@ -210,7 +159,7 @@ As with any python venv, you can install dependencies in your venv.
|
|
|
210
159
|
|
|
211
160
|
## Dev Helper:
|
|
212
161
|
|
|
213
|
-
For dev purposes, `NETHelper` is a companion helper to test your scripts
|
|
162
|
+
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
163
|
|
|
215
164
|
**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
165
|
|
|
@@ -220,30 +169,24 @@ For dev purposes, `NETHelper` is a companion helper to test your scripts before
|
|
|
220
169
|
|
|
221
170
|
```python
|
|
222
171
|
from python4cpm import NETHelper, Python4CPM, Python4CPMHandler
|
|
223
|
-
from getpass import getpass
|
|
224
|
-
|
|
225
|
-
# Get secrets for your password, logon account password, reconcile account password and new password
|
|
226
|
-
# You can use an empty string if it does not apply
|
|
227
|
-
target_password = getpass("password: ") # password from account
|
|
228
|
-
logon_password = getpass("logon_password: ") # password from linked logon account
|
|
229
|
-
reconcile_password = getpass("reconcile_password: ") # password from linked reconcile account
|
|
230
|
-
target_new_password = getpass("new_password: ") # new password for the rotation
|
|
231
172
|
|
|
232
173
|
NETHelper.set(
|
|
233
174
|
action=Python4CPM.ACTION_CHANGE, # use actions from Python4CPM.ACTION_*
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
175
|
+
logging_level="debug",
|
|
176
|
+
target_policy_id="NETHelper",
|
|
177
|
+
target_object_name="Objectname",
|
|
178
|
+
target_username="jdoe",
|
|
179
|
+
target_address="myapp.corp.local",
|
|
180
|
+
target_port="8443",
|
|
181
|
+
logon_username="ldoe",
|
|
182
|
+
reconcile_username="rdoe",
|
|
183
|
+
target_password="", # str value of target password
|
|
184
|
+
logon_password="", # str value of logon password
|
|
185
|
+
reconcile_password="", # str value of reconcile password
|
|
186
|
+
target_new_password="" # str value of new password
|
|
244
187
|
)
|
|
245
188
|
|
|
246
|
-
class
|
|
189
|
+
class CredManager(Python4CPMHandler):
|
|
247
190
|
def verify(self):
|
|
248
191
|
# TODO: Add your logic here
|
|
249
192
|
self.close_success()
|
|
@@ -264,12 +207,10 @@ class MyRotator(Python4CPMHandler):
|
|
|
264
207
|
# TODO: Add your logic here
|
|
265
208
|
self.close_success()
|
|
266
209
|
|
|
267
|
-
|
|
210
|
+
CredManager().run()
|
|
268
211
|
```
|
|
269
212
|
|
|
270
213
|
#### Remember for your final script:
|
|
271
214
|
|
|
272
215
|
- Remove the import of `NETHelper`.
|
|
273
216
|
- Remove the `NETHelper.set()` call.
|
|
274
|
-
- If applicable, change the definition of `p4cpm` from `p4cpm = NETHelper.get()` to `p4cpm = Python4CPM("MyApp")`.
|
|
275
|
-
- Remove any secrets prompting or interactive interruptions.
|
|
@@ -8,7 +8,7 @@ This module leverages the [Credential Management .NET SDK](https://docs.cyberark
|
|
|
8
8
|
|
|
9
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
10
|
|
|
11
|
-
This platform allows you to duplicate it multiple times, simply changing its settings (
|
|
11
|
+
This platform allows you to duplicate it multiple times, simply changing its settings (from Privilege Cloud/PVWA) to point to different venvs and/or python scripts.
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
@@ -56,136 +56,85 @@ This platform allows you to duplicate it multiple times, simply changing its set
|
|
|
56
56
|
from python4cpm import Python4CPMHandler
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
class
|
|
59
|
+
class CredManager(Python4CPMHandler):
|
|
60
60
|
"""
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
## Reconcile Account
|
|
85
|
-
self.reconcile_account.username
|
|
86
|
-
self.reconcile_account.password.get()
|
|
87
|
-
|
|
88
|
-
## Logging
|
|
89
|
-
|
|
90
|
-
self.logger.critical("this is critical message")
|
|
91
|
-
self.logger.error("this is an error message")
|
|
92
|
-
self.logger.warning("this is a warning message")
|
|
93
|
-
self.logger.info("this is an info message")
|
|
94
|
-
self.logger.debug("this is a debug message")
|
|
95
|
-
|
|
96
|
-
# The logging level comes from PythonLoggingLevel (platform parameters) (default is error)
|
|
97
|
-
|
|
98
|
-
=============================
|
|
99
|
-
REQUIRED TERMINATION SIGNALS
|
|
100
|
-
=============================
|
|
101
|
-
Terminate signals -> MUST use one of the following three signals to terminate the script:
|
|
102
|
-
|
|
103
|
-
self.close_success()
|
|
104
|
-
# terminate and provide CPM/SRS with a success state
|
|
105
|
-
|
|
106
|
-
self.close_fail()
|
|
107
|
-
# terminate and provide CPM/SRS with a failed recoverable state
|
|
108
|
-
|
|
109
|
-
self.close_fail(unrecoverable=True)
|
|
110
|
-
# terminate and provide CPM/SRS with a failed unrecoverable state
|
|
111
|
-
|
|
112
|
-
When calling a signal sys.exit is invoked and the script is terminated.
|
|
113
|
-
If no signal is called, and the script finishes without any exception,
|
|
114
|
-
it will behave like p4cpm.close_fail(unrecoverable=True) and log an error message.
|
|
115
|
-
=============================
|
|
116
|
-
=============================
|
|
61
|
+
Properties:
|
|
62
|
+
target_account (TargetAccount): Account being managed.
|
|
63
|
+
.policy_id (str): Platform name.
|
|
64
|
+
.object_name (str): Account name.
|
|
65
|
+
.username (str): Account username.
|
|
66
|
+
.address (str): Target address.
|
|
67
|
+
.port (str): Target port.
|
|
68
|
+
.password (Secret): Current password. Call .get() to retrieve value.
|
|
69
|
+
.new_password (Secret): Replacement password. Call .get() to retrieve value.
|
|
70
|
+
|
|
71
|
+
logon_account (LogonAccount): Linked Logon Account.
|
|
72
|
+
.username (str): Account username.
|
|
73
|
+
.password (Secret): Logon password. Call .get() to retrieve value.
|
|
74
|
+
|
|
75
|
+
reconcile_account (ReconcileAccount): Linked Reconcile Account.
|
|
76
|
+
.username (str): Account username.
|
|
77
|
+
.password (Secret): Reconcile password. Call .get() to retrieve value.
|
|
78
|
+
|
|
79
|
+
logger (logging.Logger): Logger instance.
|
|
80
|
+
|
|
81
|
+
Methods:
|
|
82
|
+
close_success(): Signal successful completion and terminate.
|
|
83
|
+
close_fail(): Signal failed completion and terminate.
|
|
117
84
|
"""
|
|
118
85
|
|
|
119
|
-
# =============================
|
|
120
|
-
# REQUIRED METHODS (MUST DEFINE)
|
|
121
|
-
# =============================
|
|
122
|
-
# verify(), logon(), change(), prereconcile(), reconcile()
|
|
123
86
|
|
|
124
87
|
def verify(self):
|
|
125
|
-
|
|
126
|
-
|
|
88
|
+
"""
|
|
89
|
+
REQUIRED METHOD
|
|
90
|
+
"""
|
|
91
|
+
# TODO: use account objects for your logic
|
|
127
92
|
self.close_success()
|
|
128
93
|
|
|
129
94
|
def logon(self):
|
|
95
|
+
"""
|
|
96
|
+
REQUIRED METHOD
|
|
97
|
+
"""
|
|
98
|
+
# TODO: use account objects for your logic
|
|
130
99
|
self.close_success()
|
|
131
100
|
|
|
132
101
|
def change(self):
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
102
|
+
"""
|
|
103
|
+
REQUIRED METHOD
|
|
104
|
+
"""
|
|
105
|
+
# TODO: use account objects for your logic
|
|
106
|
+
self.close_success()
|
|
136
107
|
|
|
137
108
|
def prereconcile(self):
|
|
138
|
-
|
|
109
|
+
"""
|
|
110
|
+
REQUIRED METHOD
|
|
111
|
+
"""
|
|
112
|
+
# TODO: use account objects for your logic
|
|
139
113
|
self.close_success()
|
|
140
114
|
|
|
141
115
|
def reconcile(self):
|
|
142
|
-
|
|
116
|
+
"""
|
|
117
|
+
REQUIRED METHOD
|
|
118
|
+
"""
|
|
119
|
+
# TODO: use account objects for your logic
|
|
143
120
|
self.close_success()
|
|
144
121
|
|
|
145
|
-
def _verify(self, from_reconcile=False):
|
|
146
|
-
if from_reconcile is False:
|
|
147
|
-
pass
|
|
148
|
-
# TODO: use account objects for your logic
|
|
149
|
-
else:
|
|
150
|
-
pass
|
|
151
|
-
# TODO: use account objects for your logic
|
|
152
|
-
result = True
|
|
153
|
-
if result is True:
|
|
154
|
-
self.log_info("verification successful")
|
|
155
|
-
else:
|
|
156
|
-
self.log_error("something went wrong")
|
|
157
|
-
self.close_fail()
|
|
158
|
-
|
|
159
|
-
def _change(self, from_reconcile=False):
|
|
160
|
-
if from_reconcile is False:
|
|
161
|
-
pass
|
|
162
|
-
# TODO: use account objects for your logic
|
|
163
|
-
else:
|
|
164
|
-
pass
|
|
165
|
-
# TODO: use account objects for your logic
|
|
166
|
-
result = True
|
|
167
|
-
if result is True:
|
|
168
|
-
self.log_info("rotation successful")
|
|
169
|
-
else:
|
|
170
|
-
self.log_error("something went wrong")
|
|
171
|
-
self.close_fail()
|
|
172
|
-
|
|
173
122
|
|
|
174
123
|
if __name__ == "__main__":
|
|
175
|
-
|
|
124
|
+
CredManager().run() # initializes the class and calls the action that was requested from CPM/SRS.
|
|
176
125
|
```
|
|
177
126
|
(*) More realistic examples can be found [here](https://github.com/gonatienza/python4cpm/blob/main/examples).
|
|
178
127
|
|
|
179
128
|
When doing `verify`, `change` or `reconcile` from Privilege Cloud/PVWA:
|
|
180
|
-
1. Verify -> the
|
|
181
|
-
2. Change -> the
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
129
|
+
1. Verify -> the script will be executed once running the `verify()` method.
|
|
130
|
+
2. Change -> the script will be executed twice: first `logon()`, then `change()`.
|
|
131
|
+
3. Reconcile -> the script will be executed twice: first `prereconcile()`, then `reconcile()`.
|
|
132
|
+
4. When calling `verify()`, `logon()` or `prereconcile()`: `target_account.new_password` will always return `None`.
|
|
133
|
+
5. If a logon account is not linked, `logon_account` will return `None`.
|
|
134
|
+
6. If a reconcile account is not linked, `reconcile_account` will return `None`.
|
|
135
|
+
7. Always use the `close_success` or `close_fail` methods to signal the proper termination for all actions.
|
|
136
|
+
- If any action is not terminated with a termination method, CPM/SRS will see this as a `close_fail(unrecoverable=True)`, even if no exceptions are raised.
|
|
137
|
+
8. The python `Logger` places its logs in the `Logs/ThirdParty` directory. The filename will be based on `target_account.policy_id` and `target_account.object_name`.
|
|
189
138
|
|
|
190
139
|
|
|
191
140
|
### Installing dependencies in python venv
|
|
@@ -199,7 +148,7 @@ As with any python venv, you can install dependencies in your venv.
|
|
|
199
148
|
|
|
200
149
|
## Dev Helper:
|
|
201
150
|
|
|
202
|
-
For dev purposes, `NETHelper` is a companion helper to test your scripts
|
|
151
|
+
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
152
|
|
|
204
153
|
**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
154
|
|
|
@@ -209,30 +158,24 @@ For dev purposes, `NETHelper` is a companion helper to test your scripts before
|
|
|
209
158
|
|
|
210
159
|
```python
|
|
211
160
|
from python4cpm import NETHelper, Python4CPM, Python4CPMHandler
|
|
212
|
-
from getpass import getpass
|
|
213
|
-
|
|
214
|
-
# Get secrets for your password, logon account password, reconcile account password and new password
|
|
215
|
-
# You can use an empty string if it does not apply
|
|
216
|
-
target_password = getpass("password: ") # password from account
|
|
217
|
-
logon_password = getpass("logon_password: ") # password from linked logon account
|
|
218
|
-
reconcile_password = getpass("reconcile_password: ") # password from linked reconcile account
|
|
219
|
-
target_new_password = getpass("new_password: ") # new password for the rotation
|
|
220
161
|
|
|
221
162
|
NETHelper.set(
|
|
222
163
|
action=Python4CPM.ACTION_CHANGE, # use actions from Python4CPM.ACTION_*
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
164
|
+
logging_level="debug",
|
|
165
|
+
target_policy_id="NETHelper",
|
|
166
|
+
target_object_name="Objectname",
|
|
167
|
+
target_username="jdoe",
|
|
168
|
+
target_address="myapp.corp.local",
|
|
169
|
+
target_port="8443",
|
|
170
|
+
logon_username="ldoe",
|
|
171
|
+
reconcile_username="rdoe",
|
|
172
|
+
target_password="", # str value of target password
|
|
173
|
+
logon_password="", # str value of logon password
|
|
174
|
+
reconcile_password="", # str value of reconcile password
|
|
175
|
+
target_new_password="" # str value of new password
|
|
233
176
|
)
|
|
234
177
|
|
|
235
|
-
class
|
|
178
|
+
class CredManager(Python4CPMHandler):
|
|
236
179
|
def verify(self):
|
|
237
180
|
# TODO: Add your logic here
|
|
238
181
|
self.close_success()
|
|
@@ -253,12 +196,10 @@ class MyRotator(Python4CPMHandler):
|
|
|
253
196
|
# TODO: Add your logic here
|
|
254
197
|
self.close_success()
|
|
255
198
|
|
|
256
|
-
|
|
199
|
+
CredManager().run()
|
|
257
200
|
```
|
|
258
201
|
|
|
259
202
|
#### Remember for your final script:
|
|
260
203
|
|
|
261
204
|
- Remove the import of `NETHelper`.
|
|
262
205
|
- Remove the `NETHelper.set()` call.
|
|
263
|
-
- If applicable, change the definition of `p4cpm` from `p4cpm = NETHelper.get()` to `p4cpm = Python4CPM("MyApp")`.
|
|
264
|
-
- Remove any secrets prompting or interactive interruptions.
|
|
@@ -0,0 +1,90 @@
|
|
|
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(
|
|
35
|
+
"policy_id",
|
|
36
|
+
"object_name",
|
|
37
|
+
"username",
|
|
38
|
+
"password",
|
|
39
|
+
"address",
|
|
40
|
+
"port",
|
|
41
|
+
"new_password"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
policy_id: str | None,
|
|
47
|
+
object_name: str | None,
|
|
48
|
+
username: str | None,
|
|
49
|
+
password: str | None,
|
|
50
|
+
address: str | None,
|
|
51
|
+
port: str | None,
|
|
52
|
+
new_password: str | None
|
|
53
|
+
) -> None:
|
|
54
|
+
super().__init__(
|
|
55
|
+
username,
|
|
56
|
+
password
|
|
57
|
+
)
|
|
58
|
+
self._policy_id = policy_id
|
|
59
|
+
self._object_name = object_name
|
|
60
|
+
self._address = address
|
|
61
|
+
self._port = port
|
|
62
|
+
self._new_password = Secret.from_env_var(new_password)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def policy_id(self) -> str | None:
|
|
66
|
+
return self._policy_id
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def object_name(self) -> str | None:
|
|
70
|
+
return self._object_name
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def address(self) -> str | None:
|
|
74
|
+
return self._address
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def port(self) -> str | None:
|
|
78
|
+
return self._port
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def new_password(self) -> Secret:
|
|
82
|
+
return self._new_password
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class LogonAccount(BaseAccount):
|
|
86
|
+
OBJ_PREFIX = "logon_"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ReconcileAccount(BaseAccount):
|
|
90
|
+
OBJ_PREFIX = "reconcile_"
|
|
@@ -7,16 +7,16 @@ class Args(EnvHandler):
|
|
|
7
7
|
|
|
8
8
|
def __init__(
|
|
9
9
|
self,
|
|
10
|
-
action: str,
|
|
11
|
-
logging_level: str
|
|
10
|
+
action: str | None,
|
|
11
|
+
logging_level: str | None
|
|
12
12
|
) -> None:
|
|
13
13
|
self._action = action
|
|
14
14
|
self._logging_level = logging_level
|
|
15
15
|
|
|
16
16
|
@property
|
|
17
|
-
def action(self) -> str:
|
|
17
|
+
def action(self) -> str | None:
|
|
18
18
|
return self._action
|
|
19
19
|
|
|
20
20
|
@property
|
|
21
|
-
def logging_level(self) -> str:
|
|
21
|
+
def logging_level(self) -> str | None:
|
|
22
22
|
return self._logging_level
|
|
@@ -20,10 +20,13 @@ class EnvHandler:
|
|
|
20
20
|
env_key = f"{cls.PREFIX}{cls.OBJ_PREFIX}{key}"
|
|
21
21
|
return env_key.upper()
|
|
22
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
|
+
|
|
23
30
|
@classmethod
|
|
24
31
|
def get(cls) -> object:
|
|
25
|
-
|
|
26
|
-
for prop in cls.PROPS:
|
|
27
|
-
value = os.environ.get(cls.get_key(prop))
|
|
28
|
-
kwargs[prop] = value if value is not None else ""
|
|
29
|
-
return cls(**kwargs)
|
|
32
|
+
return cls(**cls.get_kwargs())
|
|
@@ -12,22 +12,23 @@ class Logger:
|
|
|
12
12
|
"info": logging.INFO,
|
|
13
13
|
"debug": logging.DEBUG
|
|
14
14
|
}
|
|
15
|
-
_DEFAULT_LEVEL =
|
|
15
|
+
_DEFAULT_LEVEL = "error"
|
|
16
16
|
|
|
17
17
|
@classmethod
|
|
18
18
|
def get_logger(
|
|
19
19
|
cls,
|
|
20
20
|
name: str,
|
|
21
|
-
logging_level: str
|
|
21
|
+
logging_level: str | None
|
|
22
22
|
) -> logging.Logger:
|
|
23
23
|
os.makedirs(cls._LOGS_DIR, exist_ok=True)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
uid = os.urandom(4).hex()
|
|
25
|
+
logger = logging.getLogger(uid)
|
|
26
|
+
_logging_level = logging_level.lower()
|
|
27
|
+
if _logging_level not in cls._LOGGING_LEVELS:
|
|
28
|
+
_logging_level = cls._DEFAULT_LEVEL
|
|
29
|
+
logger.setLevel(cls._LOGGING_LEVELS[_logging_level])
|
|
30
|
+
file_name = f"{__name__}_{_logging_level}_{name}.log"
|
|
31
|
+
logs_file = os.path.join(cls._LOGS_DIR, file_name)
|
|
31
32
|
handler = RotatingFileHandler(
|
|
32
33
|
filename=logs_file,
|
|
33
34
|
maxBytes=1024 ** 2,
|
|
@@ -9,17 +9,19 @@ class NETHelper:
|
|
|
9
9
|
@classmethod
|
|
10
10
|
def set(
|
|
11
11
|
cls,
|
|
12
|
-
action: str =
|
|
13
|
-
logging_level: str =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
action: str | None = None,
|
|
13
|
+
logging_level: str | None = None,
|
|
14
|
+
target_policy_id: str | None = None,
|
|
15
|
+
target_object_name: str | None = None,
|
|
16
|
+
target_username: str | None = None,
|
|
17
|
+
target_address: str | None = None,
|
|
18
|
+
target_port: str | None = None,
|
|
19
|
+
logon_username: str | None = None,
|
|
20
|
+
reconcile_username: str | None = None,
|
|
21
|
+
target_password: str | None = None,
|
|
22
|
+
logon_password: str | None = None,
|
|
23
|
+
reconcile_password: str | None = None,
|
|
24
|
+
target_new_password: str | None = None
|
|
23
25
|
) -> None:
|
|
24
26
|
if Crypto.ENABLED:
|
|
25
27
|
target_password = Crypto.encrypt(target_password)
|
|
@@ -29,6 +31,8 @@ class NETHelper:
|
|
|
29
31
|
keys = (
|
|
30
32
|
Args.get_key(Args.PROPS.action),
|
|
31
33
|
Args.get_key(Args.PROPS.logging_level),
|
|
34
|
+
TargetAccount.get_key(TargetAccount.PROPS.policy_id),
|
|
35
|
+
TargetAccount.get_key(TargetAccount.PROPS.object_name),
|
|
32
36
|
TargetAccount.get_key(TargetAccount.PROPS.username),
|
|
33
37
|
TargetAccount.get_key(TargetAccount.PROPS.address),
|
|
34
38
|
TargetAccount.get_key(TargetAccount.PROPS.port),
|
|
@@ -42,6 +46,8 @@ class NETHelper:
|
|
|
42
46
|
values = (
|
|
43
47
|
action,
|
|
44
48
|
logging_level,
|
|
49
|
+
target_policy_id,
|
|
50
|
+
target_object_name,
|
|
45
51
|
target_username,
|
|
46
52
|
target_address,
|
|
47
53
|
target_port,
|
|
@@ -53,8 +59,9 @@ class NETHelper:
|
|
|
53
59
|
target_new_password
|
|
54
60
|
)
|
|
55
61
|
for i, key in enumerate(keys):
|
|
56
|
-
|
|
62
|
+
if values[i] is not None:
|
|
63
|
+
os.environ.update({key: values[i]})
|
|
57
64
|
|
|
58
65
|
@classmethod
|
|
59
66
|
def get(cls) -> Python4CPM:
|
|
60
|
-
return Python4CPM(
|
|
67
|
+
return Python4CPM()
|
|
@@ -3,7 +3,6 @@ import atexit
|
|
|
3
3
|
import logging
|
|
4
4
|
from python4cpm.secret import Secret
|
|
5
5
|
from python4cpm.args import Args
|
|
6
|
-
from python4cpm.crypto import Crypto
|
|
7
6
|
from python4cpm.logger import Logger
|
|
8
7
|
from python4cpm.accounts import TargetAccount, LogonAccount, ReconcileAccount
|
|
9
8
|
|
|
@@ -25,13 +24,15 @@ class Python4CPM:
|
|
|
25
24
|
_FAILED_RECOVERABLE_CODE = 81
|
|
26
25
|
_FAILED_UNRECOVERABLE_CODE = 89
|
|
27
26
|
|
|
28
|
-
def __init__(self
|
|
29
|
-
self._name = name
|
|
27
|
+
def __init__(self) -> None:
|
|
30
28
|
self._args = Args.get()
|
|
31
29
|
self._target_account = TargetAccount.get()
|
|
32
30
|
self._logon_account = LogonAccount.get()
|
|
33
31
|
self._reconcile_account = ReconcileAccount.get()
|
|
34
|
-
self._logger = Logger.get_logger(
|
|
32
|
+
self._logger = Logger.get_logger(
|
|
33
|
+
f"{self._target_account.policy_id}-{self._target_account.object_name}",
|
|
34
|
+
self._args.logging_level
|
|
35
|
+
)
|
|
35
36
|
self._logger.debug("Initiating...")
|
|
36
37
|
self._log_obj(self._args)
|
|
37
38
|
self._verify_action()
|
|
@@ -65,20 +66,18 @@ class Python4CPM:
|
|
|
65
66
|
if self._args.action not in self._VALID_ACTIONS:
|
|
66
67
|
self._logger.warning(f"Unkonwn action -> '{self._args.action}'")
|
|
67
68
|
|
|
68
|
-
def _log_obj(self, obj: object) -> None:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if not
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if Crypto.ENABLED is True:
|
|
76
|
-
logging_value = "[ENCRYPTED]"
|
|
69
|
+
def _log_obj(self, obj: object | None) -> None:
|
|
70
|
+
if obj is not None:
|
|
71
|
+
for key, value in vars(obj).items():
|
|
72
|
+
_key = f"{obj.__class__.__name__}.{key.removeprefix('_')}"
|
|
73
|
+
if value is not None:
|
|
74
|
+
if not isinstance(value, Secret):
|
|
75
|
+
logging_value = f"'{value}'"
|
|
77
76
|
else:
|
|
78
|
-
logging_value =
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
logging_value = str(value)
|
|
78
|
+
else:
|
|
79
|
+
logging_value = "[NOT SET]"
|
|
80
|
+
self._logger.debug(f"{_key} -> {logging_value}")
|
|
82
81
|
|
|
83
82
|
def close_fail(self, unrecoverable: bool = False) -> None:
|
|
84
83
|
if unrecoverable is False:
|
|
@@ -3,9 +3,6 @@ from python4cpm.python4cpm import Python4CPM
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class Python4CPMHandler(ABC, Python4CPM):
|
|
6
|
-
def __init__(self) -> None:
|
|
7
|
-
super().__init__(self.__class__.__name__)
|
|
8
|
-
|
|
9
6
|
def run(self) -> None:
|
|
10
7
|
actions = {
|
|
11
8
|
self.ACTION_VERIFY: self.verify,
|
|
@@ -18,7 +15,7 @@ class Python4CPMHandler(ABC, Python4CPM):
|
|
|
18
15
|
if action is not None:
|
|
19
16
|
action()
|
|
20
17
|
else:
|
|
21
|
-
raise ValueError(f"Unknown action: {self._args.action}")
|
|
18
|
+
raise ValueError(f"Unknown action: '{self._args.action}'")
|
|
22
19
|
|
|
23
20
|
@abstractmethod
|
|
24
21
|
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.4
|
|
4
4
|
Summary: Python for CPM
|
|
5
5
|
Author-email: Gonzalo Atienza Rela <gonatienza@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -19,7 +19,7 @@ This module leverages the [Credential Management .NET SDK](https://docs.cyberark
|
|
|
19
19
|
|
|
20
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
21
|
|
|
22
|
-
This platform allows you to duplicate it multiple times, simply changing its settings (
|
|
22
|
+
This platform allows you to duplicate it multiple times, simply changing its settings (from Privilege Cloud/PVWA) to point to different venvs and/or python scripts.
|
|
23
23
|
|
|
24
24
|
## Installation
|
|
25
25
|
|
|
@@ -67,136 +67,85 @@ This platform allows you to duplicate it multiple times, simply changing its set
|
|
|
67
67
|
from python4cpm import Python4CPMHandler
|
|
68
68
|
|
|
69
69
|
|
|
70
|
-
class
|
|
70
|
+
class CredManager(Python4CPMHandler):
|
|
71
71
|
"""
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
## Reconcile Account
|
|
96
|
-
self.reconcile_account.username
|
|
97
|
-
self.reconcile_account.password.get()
|
|
98
|
-
|
|
99
|
-
## Logging
|
|
100
|
-
|
|
101
|
-
self.logger.critical("this is critical message")
|
|
102
|
-
self.logger.error("this is an error message")
|
|
103
|
-
self.logger.warning("this is a warning message")
|
|
104
|
-
self.logger.info("this is an info message")
|
|
105
|
-
self.logger.debug("this is a debug message")
|
|
106
|
-
|
|
107
|
-
# The logging level comes from PythonLoggingLevel (platform parameters) (default is error)
|
|
108
|
-
|
|
109
|
-
=============================
|
|
110
|
-
REQUIRED TERMINATION SIGNALS
|
|
111
|
-
=============================
|
|
112
|
-
Terminate signals -> MUST use one of the following three signals to terminate the script:
|
|
113
|
-
|
|
114
|
-
self.close_success()
|
|
115
|
-
# terminate and provide CPM/SRS with a success state
|
|
116
|
-
|
|
117
|
-
self.close_fail()
|
|
118
|
-
# terminate and provide CPM/SRS with a failed recoverable state
|
|
119
|
-
|
|
120
|
-
self.close_fail(unrecoverable=True)
|
|
121
|
-
# terminate and provide CPM/SRS with a failed unrecoverable state
|
|
122
|
-
|
|
123
|
-
When calling a signal sys.exit is invoked and the script is terminated.
|
|
124
|
-
If no signal is called, and the script finishes without any exception,
|
|
125
|
-
it will behave like p4cpm.close_fail(unrecoverable=True) and log an error message.
|
|
126
|
-
=============================
|
|
127
|
-
=============================
|
|
72
|
+
Properties:
|
|
73
|
+
target_account (TargetAccount): Account being managed.
|
|
74
|
+
.policy_id (str): Platform name.
|
|
75
|
+
.object_name (str): Account name.
|
|
76
|
+
.username (str): Account username.
|
|
77
|
+
.address (str): Target address.
|
|
78
|
+
.port (str): Target port.
|
|
79
|
+
.password (Secret): Current password. Call .get() to retrieve value.
|
|
80
|
+
.new_password (Secret): Replacement password. Call .get() to retrieve value.
|
|
81
|
+
|
|
82
|
+
logon_account (LogonAccount): Linked Logon Account.
|
|
83
|
+
.username (str): Account username.
|
|
84
|
+
.password (Secret): Logon password. Call .get() to retrieve value.
|
|
85
|
+
|
|
86
|
+
reconcile_account (ReconcileAccount): Linked Reconcile Account.
|
|
87
|
+
.username (str): Account username.
|
|
88
|
+
.password (Secret): Reconcile password. Call .get() to retrieve value.
|
|
89
|
+
|
|
90
|
+
logger (logging.Logger): Logger instance.
|
|
91
|
+
|
|
92
|
+
Methods:
|
|
93
|
+
close_success(): Signal successful completion and terminate.
|
|
94
|
+
close_fail(): Signal failed completion and terminate.
|
|
128
95
|
"""
|
|
129
96
|
|
|
130
|
-
# =============================
|
|
131
|
-
# REQUIRED METHODS (MUST DEFINE)
|
|
132
|
-
# =============================
|
|
133
|
-
# verify(), logon(), change(), prereconcile(), reconcile()
|
|
134
97
|
|
|
135
98
|
def verify(self):
|
|
136
|
-
|
|
137
|
-
|
|
99
|
+
"""
|
|
100
|
+
REQUIRED METHOD
|
|
101
|
+
"""
|
|
102
|
+
# TODO: use account objects for your logic
|
|
138
103
|
self.close_success()
|
|
139
104
|
|
|
140
105
|
def logon(self):
|
|
106
|
+
"""
|
|
107
|
+
REQUIRED METHOD
|
|
108
|
+
"""
|
|
109
|
+
# TODO: use account objects for your logic
|
|
141
110
|
self.close_success()
|
|
142
111
|
|
|
143
112
|
def change(self):
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
113
|
+
"""
|
|
114
|
+
REQUIRED METHOD
|
|
115
|
+
"""
|
|
116
|
+
# TODO: use account objects for your logic
|
|
117
|
+
self.close_success()
|
|
147
118
|
|
|
148
119
|
def prereconcile(self):
|
|
149
|
-
|
|
120
|
+
"""
|
|
121
|
+
REQUIRED METHOD
|
|
122
|
+
"""
|
|
123
|
+
# TODO: use account objects for your logic
|
|
150
124
|
self.close_success()
|
|
151
125
|
|
|
152
126
|
def reconcile(self):
|
|
153
|
-
|
|
127
|
+
"""
|
|
128
|
+
REQUIRED METHOD
|
|
129
|
+
"""
|
|
130
|
+
# TODO: use account objects for your logic
|
|
154
131
|
self.close_success()
|
|
155
132
|
|
|
156
|
-
def _verify(self, from_reconcile=False):
|
|
157
|
-
if from_reconcile is False:
|
|
158
|
-
pass
|
|
159
|
-
# TODO: use account objects for your logic
|
|
160
|
-
else:
|
|
161
|
-
pass
|
|
162
|
-
# TODO: use account objects for your logic
|
|
163
|
-
result = True
|
|
164
|
-
if result is True:
|
|
165
|
-
self.log_info("verification successful")
|
|
166
|
-
else:
|
|
167
|
-
self.log_error("something went wrong")
|
|
168
|
-
self.close_fail()
|
|
169
|
-
|
|
170
|
-
def _change(self, from_reconcile=False):
|
|
171
|
-
if from_reconcile is False:
|
|
172
|
-
pass
|
|
173
|
-
# TODO: use account objects for your logic
|
|
174
|
-
else:
|
|
175
|
-
pass
|
|
176
|
-
# TODO: use account objects for your logic
|
|
177
|
-
result = True
|
|
178
|
-
if result is True:
|
|
179
|
-
self.log_info("rotation successful")
|
|
180
|
-
else:
|
|
181
|
-
self.log_error("something went wrong")
|
|
182
|
-
self.close_fail()
|
|
183
|
-
|
|
184
133
|
|
|
185
134
|
if __name__ == "__main__":
|
|
186
|
-
|
|
135
|
+
CredManager().run() # initializes the class and calls the action that was requested from CPM/SRS.
|
|
187
136
|
```
|
|
188
137
|
(*) More realistic examples can be found [here](https://github.com/gonatienza/python4cpm/blob/main/examples).
|
|
189
138
|
|
|
190
139
|
When doing `verify`, `change` or `reconcile` from Privilege Cloud/PVWA:
|
|
191
|
-
1. Verify -> the
|
|
192
|
-
2. Change -> the
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
140
|
+
1. Verify -> the script will be executed once running the `verify()` method.
|
|
141
|
+
2. Change -> the script will be executed twice: first `logon()`, then `change()`.
|
|
142
|
+
3. Reconcile -> the script will be executed twice: first `prereconcile()`, then `reconcile()`.
|
|
143
|
+
4. When calling `verify()`, `logon()` or `prereconcile()`: `target_account.new_password` will always return `None`.
|
|
144
|
+
5. If a logon account is not linked, `logon_account` will return `None`.
|
|
145
|
+
6. If a reconcile account is not linked, `reconcile_account` will return `None`.
|
|
146
|
+
7. Always use the `close_success` or `close_fail` methods to signal the proper termination for all actions.
|
|
147
|
+
- If any action is not terminated with a termination method, CPM/SRS will see this as a `close_fail(unrecoverable=True)`, even if no exceptions are raised.
|
|
148
|
+
8. The python `Logger` places its logs in the `Logs/ThirdParty` directory. The filename will be based on `target_account.policy_id` and `target_account.object_name`.
|
|
200
149
|
|
|
201
150
|
|
|
202
151
|
### Installing dependencies in python venv
|
|
@@ -210,7 +159,7 @@ As with any python venv, you can install dependencies in your venv.
|
|
|
210
159
|
|
|
211
160
|
## Dev Helper:
|
|
212
161
|
|
|
213
|
-
For dev purposes, `NETHelper` is a companion helper to test your scripts
|
|
162
|
+
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
163
|
|
|
215
164
|
**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
165
|
|
|
@@ -220,30 +169,24 @@ For dev purposes, `NETHelper` is a companion helper to test your scripts before
|
|
|
220
169
|
|
|
221
170
|
```python
|
|
222
171
|
from python4cpm import NETHelper, Python4CPM, Python4CPMHandler
|
|
223
|
-
from getpass import getpass
|
|
224
|
-
|
|
225
|
-
# Get secrets for your password, logon account password, reconcile account password and new password
|
|
226
|
-
# You can use an empty string if it does not apply
|
|
227
|
-
target_password = getpass("password: ") # password from account
|
|
228
|
-
logon_password = getpass("logon_password: ") # password from linked logon account
|
|
229
|
-
reconcile_password = getpass("reconcile_password: ") # password from linked reconcile account
|
|
230
|
-
target_new_password = getpass("new_password: ") # new password for the rotation
|
|
231
172
|
|
|
232
173
|
NETHelper.set(
|
|
233
174
|
action=Python4CPM.ACTION_CHANGE, # use actions from Python4CPM.ACTION_*
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
175
|
+
logging_level="debug",
|
|
176
|
+
target_policy_id="NETHelper",
|
|
177
|
+
target_object_name="Objectname",
|
|
178
|
+
target_username="jdoe",
|
|
179
|
+
target_address="myapp.corp.local",
|
|
180
|
+
target_port="8443",
|
|
181
|
+
logon_username="ldoe",
|
|
182
|
+
reconcile_username="rdoe",
|
|
183
|
+
target_password="", # str value of target password
|
|
184
|
+
logon_password="", # str value of logon password
|
|
185
|
+
reconcile_password="", # str value of reconcile password
|
|
186
|
+
target_new_password="" # str value of new password
|
|
244
187
|
)
|
|
245
188
|
|
|
246
|
-
class
|
|
189
|
+
class CredManager(Python4CPMHandler):
|
|
247
190
|
def verify(self):
|
|
248
191
|
# TODO: Add your logic here
|
|
249
192
|
self.close_success()
|
|
@@ -264,12 +207,10 @@ class MyRotator(Python4CPMHandler):
|
|
|
264
207
|
# TODO: Add your logic here
|
|
265
208
|
self.close_success()
|
|
266
209
|
|
|
267
|
-
|
|
210
|
+
CredManager().run()
|
|
268
211
|
```
|
|
269
212
|
|
|
270
213
|
#### Remember for your final script:
|
|
271
214
|
|
|
272
215
|
- Remove the import of `NETHelper`.
|
|
273
216
|
- Remove the `NETHelper.set()` call.
|
|
274
|
-
- If applicable, change the definition of `p4cpm` from `p4cpm = NETHelper.get()` to `p4cpm = Python4CPM("MyApp")`.
|
|
275
|
-
- Remove any secrets prompting or interactive interruptions.
|
|
@@ -1,63 +0,0 @@
|
|
|
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,
|
|
11
|
-
password: str
|
|
12
|
-
) -> None:
|
|
13
|
-
self._username = username
|
|
14
|
-
self._password = Secret(password)
|
|
15
|
-
|
|
16
|
-
@property
|
|
17
|
-
def username(self) -> str:
|
|
18
|
-
return self._username
|
|
19
|
-
|
|
20
|
-
@property
|
|
21
|
-
def password(self) -> Secret:
|
|
22
|
-
return self._password
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class TargetAccount(BaseAccount):
|
|
26
|
-
OBJ_PREFIX = "target_"
|
|
27
|
-
PROPS = Props("username", "password", "address", "port", "new_password")
|
|
28
|
-
|
|
29
|
-
def __init__(
|
|
30
|
-
self,
|
|
31
|
-
username: str,
|
|
32
|
-
password: str,
|
|
33
|
-
address: str,
|
|
34
|
-
port: str,
|
|
35
|
-
new_password: str
|
|
36
|
-
) -> None:
|
|
37
|
-
super().__init__(
|
|
38
|
-
username,
|
|
39
|
-
password
|
|
40
|
-
)
|
|
41
|
-
self._address = address
|
|
42
|
-
self._port = port
|
|
43
|
-
self._new_password = Secret(new_password)
|
|
44
|
-
|
|
45
|
-
@property
|
|
46
|
-
def address(self) -> str:
|
|
47
|
-
return self._address
|
|
48
|
-
|
|
49
|
-
@property
|
|
50
|
-
def port(self) -> str:
|
|
51
|
-
return self._port
|
|
52
|
-
|
|
53
|
-
@property
|
|
54
|
-
def new_password(self) -> Secret:
|
|
55
|
-
return self._new_password
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class LogonAccount(BaseAccount):
|
|
59
|
-
OBJ_PREFIX = "logon_"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class ReconcileAccount(BaseAccount):
|
|
63
|
-
OBJ_PREFIX = "reconcile_"
|
|
@@ -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
|
|
File without changes
|