pyxecm 1.6__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyxecm might be problematic. Click here for more details.
- pyxecm/__init__.py +7 -4
- pyxecm/avts.py +727 -254
- pyxecm/coreshare.py +686 -467
- pyxecm/customizer/__init__.py +16 -4
- pyxecm/customizer/__main__.py +58 -0
- pyxecm/customizer/api/__init__.py +5 -0
- pyxecm/customizer/api/__main__.py +6 -0
- pyxecm/customizer/api/app.py +163 -0
- pyxecm/customizer/api/auth/__init__.py +1 -0
- pyxecm/customizer/api/auth/functions.py +92 -0
- pyxecm/customizer/api/auth/models.py +13 -0
- pyxecm/customizer/api/auth/router.py +78 -0
- pyxecm/customizer/api/common/__init__.py +1 -0
- pyxecm/customizer/api/common/functions.py +47 -0
- pyxecm/customizer/api/common/metrics.py +92 -0
- pyxecm/customizer/api/common/models.py +21 -0
- pyxecm/customizer/api/common/payload_list.py +870 -0
- pyxecm/customizer/api/common/router.py +72 -0
- pyxecm/customizer/api/settings.py +128 -0
- pyxecm/customizer/api/terminal/__init__.py +1 -0
- pyxecm/customizer/api/terminal/router.py +87 -0
- pyxecm/customizer/api/v1_csai/__init__.py +1 -0
- pyxecm/customizer/api/v1_csai/router.py +87 -0
- pyxecm/customizer/api/v1_maintenance/__init__.py +1 -0
- pyxecm/customizer/api/v1_maintenance/functions.py +100 -0
- pyxecm/customizer/api/v1_maintenance/models.py +12 -0
- pyxecm/customizer/api/v1_maintenance/router.py +76 -0
- pyxecm/customizer/api/v1_otcs/__init__.py +1 -0
- pyxecm/customizer/api/v1_otcs/functions.py +61 -0
- pyxecm/customizer/api/v1_otcs/router.py +179 -0
- pyxecm/customizer/api/v1_payload/__init__.py +1 -0
- pyxecm/customizer/api/v1_payload/functions.py +179 -0
- pyxecm/customizer/api/v1_payload/models.py +51 -0
- pyxecm/customizer/api/v1_payload/router.py +499 -0
- pyxecm/customizer/browser_automation.py +721 -286
- pyxecm/customizer/customizer.py +1076 -1425
- pyxecm/customizer/exceptions.py +35 -0
- pyxecm/customizer/guidewire.py +1186 -0
- pyxecm/customizer/k8s.py +901 -379
- pyxecm/customizer/log.py +107 -0
- pyxecm/customizer/m365.py +2967 -920
- pyxecm/customizer/nhc.py +1169 -0
- pyxecm/customizer/openapi.py +258 -0
- pyxecm/customizer/payload.py +18228 -7820
- pyxecm/customizer/pht.py +717 -286
- pyxecm/customizer/salesforce.py +516 -342
- pyxecm/customizer/sap.py +58 -41
- pyxecm/customizer/servicenow.py +611 -372
- pyxecm/customizer/settings.py +445 -0
- pyxecm/customizer/successfactors.py +408 -346
- pyxecm/customizer/translate.py +83 -48
- pyxecm/helper/__init__.py +5 -2
- pyxecm/helper/assoc.py +83 -43
- pyxecm/helper/data.py +2406 -870
- pyxecm/helper/logadapter.py +27 -0
- pyxecm/helper/web.py +229 -101
- pyxecm/helper/xml.py +596 -171
- pyxecm/maintenance_page/__init__.py +5 -0
- pyxecm/maintenance_page/__main__.py +6 -0
- pyxecm/maintenance_page/app.py +51 -0
- pyxecm/maintenance_page/settings.py +28 -0
- pyxecm/maintenance_page/static/favicon.avif +0 -0
- pyxecm/maintenance_page/templates/maintenance.html +165 -0
- pyxecm/otac.py +235 -141
- pyxecm/otawp.py +2668 -1220
- pyxecm/otca.py +569 -0
- pyxecm/otcs.py +7956 -3237
- pyxecm/otds.py +2178 -925
- pyxecm/otiv.py +36 -21
- pyxecm/otmm.py +1272 -325
- pyxecm/otpd.py +231 -127
- pyxecm-2.0.1.dist-info/METADATA +122 -0
- pyxecm-2.0.1.dist-info/RECORD +76 -0
- {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info}/WHEEL +1 -1
- pyxecm-1.6.dist-info/METADATA +0 -53
- pyxecm-1.6.dist-info/RECORD +0 -32
- {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info/licenses}/LICENSE +0 -0
- {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info}/top_level.txt +0 -0
pyxecm/otds.py
CHANGED
|
@@ -1,130 +1,57 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Class: OTDS
|
|
8
|
-
Methods:
|
|
9
|
-
|
|
10
|
-
__init__ : class initializer
|
|
11
|
-
config : returns config data set
|
|
12
|
-
cookie : returns cookie information
|
|
13
|
-
credentials: returns set of username and password
|
|
14
|
-
|
|
15
|
-
base_url : returns OTDS base URL
|
|
16
|
-
rest_url : returns OTDS REST base URL
|
|
17
|
-
credential_url : returns the OTDS Credentials REST URL
|
|
18
|
-
authHandler_url : returns the OTDS Authentication Handler REST URL
|
|
19
|
-
partition_url : returns OTDS Partition REST URL
|
|
20
|
-
access_role_url : returns OTDS Access Role REST URL
|
|
21
|
-
oauth_client_url : returns OTDS OAuth Client REST URL
|
|
22
|
-
resource_url : returns OTDS Resource REST URL
|
|
23
|
-
license_url : returns OTDS License REST URL
|
|
24
|
-
token_url : returns OTDS Token REST URL
|
|
25
|
-
users_url : returns OTDS Users REST URL
|
|
26
|
-
groups_url : returns OTDS Groups REST URL
|
|
27
|
-
system_config_url : returns OTDS System Config REST URL
|
|
28
|
-
consolidation_url: returns OTDS consolidation URL
|
|
29
|
-
|
|
30
|
-
do_request: call an OTDS REST API in a safe way.
|
|
31
|
-
parse_request_response: Converts the request response to a Python dict in a safe way
|
|
32
|
-
|
|
33
|
-
authenticate : authenticates at OTDS server
|
|
34
|
-
|
|
35
|
-
add_synchronized_partition: Add a Synchronized partition to OTDS
|
|
36
|
-
add_partition : Add an OTDS partition
|
|
37
|
-
get_partition : Get a partition with a specific name
|
|
38
|
-
|
|
39
|
-
add_user : Add a user to a partion
|
|
40
|
-
get_user : Get a user with a specific user ID (= login name @ partition)
|
|
41
|
-
get_users: get all users (with option to filter)
|
|
42
|
-
update_user : Update attributes of on OTDS user
|
|
43
|
-
delete_user : Delete a user with a specific ID in a specific partition
|
|
44
|
-
reset_user_password : Reset a password of a specific user ID
|
|
45
|
-
|
|
46
|
-
add_group: Add an OTDS group
|
|
47
|
-
get_group: Get a OTDS group by its name
|
|
48
|
-
add_user_to_group : Add an OTDS user to a OTDS group
|
|
49
|
-
add_group_to_parent_group : Add on OTDS group to a parent group
|
|
50
|
-
|
|
51
|
-
add_resource : Add a new resource to OTDS
|
|
52
|
-
get_resource : Get an OTDS resource with a specific name
|
|
53
|
-
update_resource: Update an existing OTDS resource
|
|
54
|
-
activate_resource : Activate an OTDS resource
|
|
55
|
-
|
|
56
|
-
get_access_roles : Get all OTDS Access Roles
|
|
57
|
-
get_access_role: Get an OTDS Access Role with a specific name
|
|
58
|
-
add_partition_to_access_role : Add an OTDS Partition to to an OTDS Access Role
|
|
59
|
-
add_user_to_access_role : Add an OTDS user to to an OTDS Access Role
|
|
60
|
-
add_group_to_access_role : Add an OTDS group to to an OTDS Access Role
|
|
61
|
-
update_access_role_attributes: Update attributes of an existing access role
|
|
62
|
-
|
|
63
|
-
add_license_to_resource : Add (or update) a product license to OTDS
|
|
64
|
-
get_license_for_resource : Get list of licenses for a resource
|
|
65
|
-
delete_license_from_resource : Delete a license from a resource
|
|
66
|
-
assign_user_to_license : Assign an OTDS user to a product license (feature) in OTDS.
|
|
67
|
-
assign_partition_to_license: Assign an OTDS user partition to a license (feature) in OTDS.
|
|
68
|
-
get_licensed_objects: Return the licensed objects (users, groups, partitions) an OTDS for a
|
|
69
|
-
license + license feature associated with an OTDS resource (like "cs").
|
|
70
|
-
is_user_licensed: Check if a user is licensed for a license and license feature associated
|
|
71
|
-
with a particular OTDS resource.
|
|
72
|
-
is_group_licensed: Check if a group is licensed for a license and license feature associated
|
|
73
|
-
with a particular OTDS resource.
|
|
74
|
-
is_partition_licensed: Check if a user partition is licensed for a license and license feature
|
|
75
|
-
associated with a particular OTDS resource.
|
|
76
|
-
|
|
77
|
-
add_system_attribute : Add an OTDS System Attribute
|
|
78
|
-
|
|
79
|
-
get_trusted_sites : Get OTDS Trusted Sites
|
|
80
|
-
add_trusted_site : Add a new trusted site to OTDS
|
|
81
|
-
|
|
82
|
-
enable_audit: enable OTDS audit
|
|
83
|
-
|
|
84
|
-
add_oauth_client : Add a new OAuth client to OTDS
|
|
85
|
-
get_oauth_client : Get an OAuth client with a specific client ID
|
|
86
|
-
update_oauth_client : Update an OAuth client
|
|
87
|
-
add_oauth_clients_to_access_role : Add an OTDS OAuth Client to an OTDS Access Role
|
|
88
|
-
get_access_token : Get an OTDS Access Token
|
|
89
|
-
|
|
90
|
-
get_auth_handler: Gen an auth handler with a given name
|
|
91
|
-
add_auth_handler_saml: Add an authentication handler for SAML (e.g. for SuccessFactors)
|
|
92
|
-
add_auth_handler_sap: Add an authentication handler for SAP
|
|
93
|
-
add_auth_handler_oauth: Add an authentication handler for OAuth (used for Salesforce)
|
|
94
|
-
|
|
95
|
-
consolidate: Consolidate an OTDS resource
|
|
96
|
-
impersonate_resource: Configure impersonation for an OTDS resource
|
|
97
|
-
impersonate_oauth_client: Configure impersonation for an OTDS OAuth Client
|
|
98
|
-
|
|
99
|
-
get_password_policy: get the global password policy
|
|
100
|
-
update_password_policy: updates the global password policy
|
|
1
|
+
"""OTDS Module to implement functions to read / write OTDS objects.
|
|
2
|
+
|
|
3
|
+
This includes Ressources, Users, Groups, Licenses, Trusted Sites, OAuth Clients, ...
|
|
4
|
+
|
|
5
|
+
The documentation for the used REST APIs can be found here:
|
|
6
|
+
- [https://developer.opentext.com](https://developer.opentext.com/ce/products/opentext-directory-services)
|
|
101
7
|
|
|
8
|
+
|
|
9
|
+
!!! tip
|
|
10
|
+
Important: userIDs consists of login name + "@" + partition name
|
|
102
11
|
"""
|
|
103
12
|
|
|
104
13
|
__author__ = "Dr. Marc Diefenbruch"
|
|
105
|
-
__copyright__ = "Copyright 2024, OpenText"
|
|
106
|
-
__credits__ = ["Kai-Philip Gatzweiler"
|
|
14
|
+
__copyright__ = "Copyright (C) 2024-2025, OpenText"
|
|
15
|
+
__credits__ = ["Kai-Philip Gatzweiler"]
|
|
107
16
|
__maintainer__ = "Dr. Marc Diefenbruch"
|
|
108
17
|
__email__ = "mdiefenb@opentext.com"
|
|
109
18
|
|
|
110
|
-
import os
|
|
111
|
-
import logging
|
|
112
|
-
import json
|
|
113
|
-
import urllib.parse
|
|
114
19
|
import base64
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
import platform
|
|
24
|
+
import sys
|
|
25
|
+
import tempfile
|
|
115
26
|
import time
|
|
116
|
-
|
|
27
|
+
import urllib.parse
|
|
117
28
|
from http import HTTPStatus
|
|
29
|
+
from importlib.metadata import version
|
|
30
|
+
|
|
118
31
|
import requests
|
|
119
32
|
|
|
120
|
-
|
|
33
|
+
APP_NAME = "pyxecm"
|
|
34
|
+
APP_VERSION = version("pyxecm")
|
|
35
|
+
MODULE_NAME = APP_NAME + ".otds"
|
|
36
|
+
|
|
37
|
+
PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
38
|
+
OS_INFO = f"{platform.system()} {platform.release()}"
|
|
39
|
+
ARCH_INFO = platform.machine()
|
|
40
|
+
REQUESTS_VERSION = requests.__version__
|
|
41
|
+
|
|
42
|
+
USER_AGENT = (
|
|
43
|
+
f"{APP_NAME}/{APP_VERSION} ({MODULE_NAME}/{APP_VERSION}; "
|
|
44
|
+
f"Python/{PYTHON_VERSION}; {OS_INFO}; {ARCH_INFO}; Requests/{REQUESTS_VERSION})"
|
|
45
|
+
)
|
|
121
46
|
|
|
122
47
|
REQUEST_HEADERS = {
|
|
48
|
+
"User-Agent": USER_AGENT,
|
|
123
49
|
"accept": "application/json;charset=utf-8",
|
|
124
50
|
"Content-Type": "application/json",
|
|
125
51
|
}
|
|
126
52
|
|
|
127
53
|
REQUEST_FORM_HEADERS = {
|
|
54
|
+
"User-Agent": USER_AGENT,
|
|
128
55
|
"accept": "application/json;charset=utf-8",
|
|
129
56
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
130
57
|
}
|
|
@@ -133,9 +60,13 @@ REQUEST_TIMEOUT = 60
|
|
|
133
60
|
REQUEST_RETRY_DELAY = 20
|
|
134
61
|
REQUEST_MAX_RETRIES = 2
|
|
135
62
|
|
|
63
|
+
default_logger = logging.getLogger(MODULE_NAME)
|
|
64
|
+
|
|
136
65
|
|
|
137
66
|
class OTDS:
|
|
138
|
-
"""
|
|
67
|
+
"""Class OTDS is used to automate stettings in OpenText Directory Services (OTDS)."""
|
|
68
|
+
|
|
69
|
+
logger: logging.Logger = default_logger
|
|
139
70
|
|
|
140
71
|
_config = None
|
|
141
72
|
_cookie = None
|
|
@@ -149,19 +80,38 @@ class OTDS:
|
|
|
149
80
|
username: str | None = None,
|
|
150
81
|
password: str | None = None,
|
|
151
82
|
otds_ticket: str | None = None,
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
83
|
+
bind_password: str | None = None,
|
|
84
|
+
admin_partition: str = "otds.admin",
|
|
85
|
+
logger: logging.Logger = default_logger,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Initialize the OTDS object.
|
|
155
88
|
|
|
156
89
|
Args:
|
|
157
|
-
protocol (str):
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
90
|
+
protocol (str):
|
|
91
|
+
This is either http or https.
|
|
92
|
+
hostname (str):
|
|
93
|
+
The hostname of OTDS.
|
|
94
|
+
port (int):
|
|
95
|
+
The port number - typically 80 or 443.
|
|
96
|
+
username (str, optional):
|
|
97
|
+
The OTDS user name. Optional if otds_ticket is provided.
|
|
98
|
+
password (str, optional):
|
|
99
|
+
The OTDS password. Optional if otds_ticket is provided.
|
|
100
|
+
otds_ticket (str | None, optional):
|
|
101
|
+
Authentication ticket of OTDS.
|
|
102
|
+
bind_password (str | None, optional): TODO
|
|
103
|
+
admin_partition (str, optional):
|
|
104
|
+
Name of the admin partition. Default is "otds.admin".
|
|
105
|
+
logger (logging.Logger, optional):
|
|
106
|
+
The logging object to use for all log messages. Defaults to default_logger.
|
|
107
|
+
|
|
163
108
|
"""
|
|
164
109
|
|
|
110
|
+
if logger != default_logger:
|
|
111
|
+
self.logger = logger.getChild("otds")
|
|
112
|
+
for logfilter in logger.filters:
|
|
113
|
+
self.logger.addFilter(logfilter)
|
|
114
|
+
|
|
165
115
|
# Initialize otdsConfig as an empty dictionary
|
|
166
116
|
otds_config = {}
|
|
167
117
|
|
|
@@ -189,67 +139,101 @@ class OTDS:
|
|
|
189
139
|
otds_config["password"] = password
|
|
190
140
|
else:
|
|
191
141
|
otds_config["password"] = ""
|
|
192
|
-
|
|
193
|
-
if
|
|
194
|
-
otds_config["bindPassword"] =
|
|
142
|
+
|
|
143
|
+
if bind_password:
|
|
144
|
+
otds_config["bindPassword"] = bind_password
|
|
195
145
|
else:
|
|
196
146
|
otds_config["bindPassword"] = ""
|
|
197
147
|
|
|
148
|
+
otds_config["adminPartition"] = admin_partition
|
|
149
|
+
|
|
198
150
|
if otds_ticket:
|
|
199
151
|
self._cookie = {"OTDSTicket": otds_ticket}
|
|
200
152
|
|
|
201
|
-
|
|
153
|
+
otds_base_url = protocol + "://" + otds_config["hostname"]
|
|
202
154
|
if str(port) not in ["80", "443"]:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
otds_config["baseUrl"] =
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
otds_config["restUrl"] =
|
|
209
|
-
|
|
210
|
-
otds_config["partitionUrl"] =
|
|
211
|
-
otds_config["identityproviderprofiles"] =
|
|
212
|
-
otds_config["accessRoleUrl"] =
|
|
213
|
-
otds_config["credentialUrl"] =
|
|
214
|
-
otds_config["
|
|
215
|
-
otds_config["
|
|
216
|
-
otds_config["
|
|
217
|
-
otds_config["
|
|
218
|
-
otds_config["
|
|
219
|
-
otds_config["
|
|
220
|
-
otds_config["
|
|
221
|
-
otds_config["
|
|
222
|
-
otds_config["
|
|
155
|
+
otds_base_url += ":{}".format(port)
|
|
156
|
+
otds_base_url += "/otdsws"
|
|
157
|
+
otds_config["baseUrl"] = otds_base_url
|
|
158
|
+
|
|
159
|
+
otds_rest_url = otds_base_url + "/rest"
|
|
160
|
+
otds_config["restUrl"] = otds_rest_url
|
|
161
|
+
|
|
162
|
+
otds_config["partitionUrl"] = otds_rest_url + "/partitions"
|
|
163
|
+
otds_config["identityproviderprofiles"] = otds_rest_url + "/identityproviderprofiles"
|
|
164
|
+
otds_config["accessRoleUrl"] = otds_rest_url + "/accessroles"
|
|
165
|
+
otds_config["credentialUrl"] = otds_rest_url + "/authentication/credentials"
|
|
166
|
+
otds_config["ticketforuserUrl"] = otds_rest_url + "/authentication/ticketforuser"
|
|
167
|
+
otds_config["oauthClientUrl"] = otds_rest_url + "/oauthclients"
|
|
168
|
+
otds_config["tokenUrl"] = otds_base_url + "/oauth2/token"
|
|
169
|
+
otds_config["resourceUrl"] = otds_rest_url + "/resources"
|
|
170
|
+
otds_config["licenseUrl"] = otds_rest_url + "/licensemanagement/licenses"
|
|
171
|
+
otds_config["usersUrl"] = otds_rest_url + "/users"
|
|
172
|
+
otds_config["currentUserUrl"] = otds_rest_url + "/currentuser"
|
|
173
|
+
otds_config["groupsUrl"] = otds_rest_url + "/groups"
|
|
174
|
+
otds_config["systemConfigUrl"] = otds_rest_url + "/systemconfig"
|
|
175
|
+
otds_config["authHandlerUrl"] = otds_rest_url + "/authhandlers"
|
|
176
|
+
otds_config["consolidationUrl"] = otds_rest_url + "/consolidation"
|
|
177
|
+
otds_config["rolesUrl"] = otds_rest_url + "/roles"
|
|
223
178
|
|
|
224
179
|
self._config = otds_config
|
|
225
180
|
|
|
226
181
|
def config(self) -> dict:
|
|
227
|
-
"""
|
|
182
|
+
"""Return the configuration dictionary.
|
|
228
183
|
|
|
229
184
|
Returns:
|
|
230
|
-
dict:
|
|
185
|
+
dict:
|
|
186
|
+
The configuration dictionary.
|
|
187
|
+
|
|
231
188
|
"""
|
|
232
189
|
return self._config
|
|
233
190
|
|
|
234
191
|
# end method definition
|
|
235
192
|
|
|
236
193
|
def cookie(self) -> dict:
|
|
237
|
-
"""
|
|
238
|
-
|
|
194
|
+
"""Return the login cookie of OTDS.
|
|
195
|
+
|
|
196
|
+
This is set by the authenticate() method
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
dict:
|
|
200
|
+
The OTDS cookie.
|
|
201
|
+
|
|
202
|
+
"""
|
|
203
|
+
return self._cookie
|
|
204
|
+
|
|
205
|
+
# end method definition
|
|
206
|
+
|
|
207
|
+
def set_cookie(self, ticket: str) -> dict:
|
|
208
|
+
"""Return the login cookie of OTDS.
|
|
209
|
+
|
|
210
|
+
This is set by the authenticate() method
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
ticket (str):
|
|
214
|
+
The new ticket value for the cookie.
|
|
239
215
|
|
|
240
216
|
Returns:
|
|
241
|
-
dict:
|
|
217
|
+
dict:
|
|
218
|
+
The updated OTDS cookie.
|
|
219
|
+
|
|
242
220
|
"""
|
|
221
|
+
|
|
222
|
+
self._cookie["OTDSTicket"] = ticket
|
|
223
|
+
|
|
243
224
|
return self._cookie
|
|
244
225
|
|
|
245
226
|
# end method definition
|
|
246
227
|
|
|
247
228
|
def credentials(self) -> dict:
|
|
248
|
-
"""
|
|
229
|
+
"""Return the credentials (username + password).
|
|
249
230
|
|
|
250
231
|
Returns:
|
|
251
|
-
dict:
|
|
232
|
+
dict:
|
|
233
|
+
The dictionary with username and password.
|
|
234
|
+
|
|
252
235
|
"""
|
|
236
|
+
|
|
253
237
|
return {
|
|
254
238
|
"userName": self.config()["username"],
|
|
255
239
|
"password": self.config()["password"],
|
|
@@ -258,153 +242,225 @@ class OTDS:
|
|
|
258
242
|
# end method definition
|
|
259
243
|
|
|
260
244
|
def base_url(self) -> str:
|
|
261
|
-
"""
|
|
245
|
+
"""Return the base URL of OTDS.
|
|
262
246
|
|
|
263
247
|
Returns:
|
|
264
|
-
str:
|
|
248
|
+
str:
|
|
249
|
+
The base URL.
|
|
250
|
+
|
|
265
251
|
"""
|
|
252
|
+
|
|
266
253
|
return self.config()["baseUrl"]
|
|
267
254
|
|
|
268
255
|
# end method definition
|
|
269
256
|
|
|
270
257
|
def rest_url(self) -> str:
|
|
271
|
-
"""
|
|
258
|
+
"""Return the REST URL of OTDS.
|
|
272
259
|
|
|
273
260
|
Returns:
|
|
274
|
-
str:
|
|
261
|
+
str:
|
|
262
|
+
The REST URL.
|
|
263
|
+
|
|
275
264
|
"""
|
|
265
|
+
|
|
276
266
|
return self.config()["restUrl"]
|
|
277
267
|
|
|
278
268
|
# end method definition
|
|
279
269
|
|
|
280
270
|
def credential_url(self) -> str:
|
|
281
|
-
"""
|
|
271
|
+
"""Return the Credentials URL of OTDS.
|
|
282
272
|
|
|
283
273
|
Returns:
|
|
284
|
-
str:
|
|
274
|
+
str:
|
|
275
|
+
The credentials URL.
|
|
276
|
+
|
|
285
277
|
"""
|
|
278
|
+
|
|
286
279
|
return self.config()["credentialUrl"]
|
|
287
280
|
|
|
288
281
|
# end method definition
|
|
289
282
|
|
|
290
283
|
def auth_handler_url(self) -> str:
|
|
291
|
-
"""
|
|
284
|
+
"""Return the Auth Handler URL of OTDS.
|
|
292
285
|
|
|
293
286
|
Returns:
|
|
294
|
-
str:
|
|
287
|
+
str:
|
|
288
|
+
The auth handler URL.
|
|
289
|
+
|
|
295
290
|
"""
|
|
291
|
+
|
|
296
292
|
return self.config()["authHandlerUrl"]
|
|
297
293
|
|
|
298
294
|
# end method definition
|
|
299
295
|
def synchronized_partition_url(self) -> str:
|
|
300
|
-
"""
|
|
296
|
+
"""Return the Partition URL of OTDS.
|
|
301
297
|
|
|
302
298
|
Returns:
|
|
303
|
-
str:
|
|
299
|
+
str:
|
|
300
|
+
The synchronized partition URL.
|
|
301
|
+
|
|
304
302
|
"""
|
|
303
|
+
|
|
305
304
|
return self.config()["identityproviderprofiles"]
|
|
306
|
-
|
|
305
|
+
|
|
306
|
+
# end of method definition
|
|
307
307
|
|
|
308
308
|
def partition_url(self) -> str:
|
|
309
|
-
"""
|
|
309
|
+
"""Return the partition URL of OTDS.
|
|
310
310
|
|
|
311
311
|
Returns:
|
|
312
|
-
str:
|
|
312
|
+
str:
|
|
313
|
+
The partition URL.
|
|
314
|
+
|
|
313
315
|
"""
|
|
316
|
+
|
|
314
317
|
return self.config()["partitionUrl"]
|
|
315
318
|
|
|
316
319
|
# end method definition
|
|
317
320
|
|
|
318
321
|
def access_role_url(self) -> str:
|
|
319
|
-
"""
|
|
322
|
+
"""Return the access role URL of OTDS.
|
|
320
323
|
|
|
321
324
|
Returns:
|
|
322
|
-
str:
|
|
325
|
+
str:
|
|
326
|
+
The access role URL.
|
|
327
|
+
|
|
323
328
|
"""
|
|
329
|
+
|
|
324
330
|
return self.config()["accessRoleUrl"]
|
|
325
331
|
|
|
326
332
|
# end method definition
|
|
327
333
|
|
|
328
334
|
def oauth_client_url(self) -> str:
|
|
329
|
-
"""
|
|
335
|
+
"""Return the OAuth client URL of OTDS.
|
|
330
336
|
|
|
331
337
|
Returns:
|
|
332
|
-
str:
|
|
338
|
+
str:
|
|
339
|
+
The OAuth client URL.
|
|
340
|
+
|
|
333
341
|
"""
|
|
342
|
+
|
|
334
343
|
return self.config()["oauthClientUrl"]
|
|
335
344
|
|
|
336
345
|
# end method definition
|
|
337
346
|
|
|
338
347
|
def resource_url(self) -> str:
|
|
339
|
-
"""
|
|
348
|
+
"""Return the resource URL of OTDS.
|
|
340
349
|
|
|
341
350
|
Returns:
|
|
342
|
-
str:
|
|
351
|
+
str:
|
|
352
|
+
The resource URL.
|
|
353
|
+
|
|
343
354
|
"""
|
|
355
|
+
|
|
344
356
|
return self.config()["resourceUrl"]
|
|
345
357
|
|
|
346
358
|
# end method definition
|
|
347
359
|
|
|
348
360
|
def license_url(self) -> str:
|
|
349
|
-
"""
|
|
361
|
+
"""Return the License URL of OTDS.
|
|
350
362
|
|
|
351
363
|
Returns:
|
|
352
|
-
str:
|
|
364
|
+
str:
|
|
365
|
+
The license URL.
|
|
366
|
+
|
|
353
367
|
"""
|
|
368
|
+
|
|
354
369
|
return self.config()["licenseUrl"]
|
|
355
370
|
|
|
356
371
|
# end method definition
|
|
357
372
|
|
|
358
373
|
def token_url(self) -> str:
|
|
359
|
-
"""
|
|
374
|
+
"""Return the token URL of OTDS.
|
|
360
375
|
|
|
361
376
|
Returns:
|
|
362
|
-
str:
|
|
377
|
+
str:
|
|
378
|
+
The token URL.
|
|
379
|
+
|
|
363
380
|
"""
|
|
381
|
+
|
|
364
382
|
return self.config()["tokenUrl"]
|
|
365
383
|
|
|
366
384
|
# end method definition
|
|
367
385
|
|
|
368
386
|
def users_url(self) -> str:
|
|
369
|
-
"""
|
|
387
|
+
"""Return the users URL of OTDS.
|
|
370
388
|
|
|
371
389
|
Returns:
|
|
372
|
-
str:
|
|
390
|
+
str:
|
|
391
|
+
The users URL.
|
|
392
|
+
|
|
373
393
|
"""
|
|
394
|
+
|
|
374
395
|
return self.config()["usersUrl"]
|
|
375
396
|
|
|
376
397
|
# end method definition
|
|
377
398
|
|
|
399
|
+
def current_user_url(self) -> str:
|
|
400
|
+
"""Return the current user URL of OTDS.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
str:
|
|
404
|
+
The current user URL.
|
|
405
|
+
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
return self.config()["currentUserUrl"]
|
|
409
|
+
|
|
410
|
+
# end method definition
|
|
411
|
+
|
|
378
412
|
def groups_url(self) -> str:
|
|
379
|
-
"""
|
|
413
|
+
"""Return the groups URL of OTDS.
|
|
380
414
|
|
|
381
415
|
Returns:
|
|
382
|
-
str:
|
|
416
|
+
str:
|
|
417
|
+
The groups URL.
|
|
418
|
+
|
|
383
419
|
"""
|
|
420
|
+
|
|
384
421
|
return self.config()["groupsUrl"]
|
|
385
422
|
|
|
386
423
|
# end method definition
|
|
387
424
|
|
|
388
425
|
def system_config_url(self) -> str:
|
|
389
|
-
"""
|
|
426
|
+
"""Return the system config URL of OTDS.
|
|
390
427
|
|
|
391
428
|
Returns:
|
|
392
|
-
str:
|
|
429
|
+
str:
|
|
430
|
+
The system config URL.
|
|
431
|
+
|
|
393
432
|
"""
|
|
433
|
+
|
|
394
434
|
return self.config()["systemConfigUrl"]
|
|
395
435
|
|
|
396
436
|
# end method definition
|
|
397
437
|
|
|
398
438
|
def consolidation_url(self) -> str:
|
|
399
|
-
"""
|
|
439
|
+
"""Return the consolidation URL of OTDS.
|
|
400
440
|
|
|
401
441
|
Returns:
|
|
402
|
-
str:
|
|
442
|
+
str:
|
|
443
|
+
The consolidation URL.
|
|
444
|
+
|
|
403
445
|
"""
|
|
446
|
+
|
|
404
447
|
return self.config()["consolidationUrl"]
|
|
405
448
|
|
|
406
449
|
# end method definition
|
|
407
450
|
|
|
451
|
+
def admin_partition_name(self) -> str:
|
|
452
|
+
"""Return OTDS admin partition name.
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
str:
|
|
456
|
+
The OTDS admin partition name.
|
|
457
|
+
|
|
458
|
+
"""
|
|
459
|
+
|
|
460
|
+
return self.config()["adminPartition"]
|
|
461
|
+
|
|
462
|
+
# end method definition
|
|
463
|
+
|
|
408
464
|
def do_request(
|
|
409
465
|
self,
|
|
410
466
|
url: str,
|
|
@@ -423,27 +479,49 @@ class OTDS:
|
|
|
423
479
|
retry_forever: bool = False,
|
|
424
480
|
parse_request_response: bool = True,
|
|
425
481
|
) -> dict | None:
|
|
426
|
-
"""Call an OTDS REST API in a safe way
|
|
482
|
+
"""Call an OTDS REST API in a safe way.
|
|
427
483
|
|
|
428
484
|
Args:
|
|
429
|
-
url (str):
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
485
|
+
url (str):
|
|
486
|
+
The URL to send the request to.
|
|
487
|
+
method (str, optional):
|
|
488
|
+
The HTTP method (GET, POST, etc.). Defaults to "GET".
|
|
489
|
+
headers (dict | None, optional):
|
|
490
|
+
The request headers. Defaults to None.
|
|
491
|
+
data (dict | None, optional):
|
|
492
|
+
Request payload. Defaults to None
|
|
493
|
+
json_data (dict | None, optional):
|
|
494
|
+
Request payload for the JSON parameter. Defaults to None.
|
|
495
|
+
files (dict | None, optional):
|
|
496
|
+
Dictionary of {"name": file-tuple} for multipart encoding upload.
|
|
497
|
+
File-tuple can be a 2-tuple ("filename", fileobj) or a 3-tuple ("filename", fileobj, "content_type")
|
|
498
|
+
timeout (int | None, optional):
|
|
499
|
+
The timeout for the request in seconds. Defaults to REQUEST_TIMEOUT.
|
|
500
|
+
show_error (bool, optional):
|
|
501
|
+
Whether or not an error should be logged in case of a failed REST call.
|
|
502
|
+
If False, then only a warning is logged. Defaults to True.
|
|
503
|
+
show_warning (bool, optional):
|
|
504
|
+
Whether or not an warning should be logged in case of a
|
|
505
|
+
failed REST call.
|
|
506
|
+
If False, then only a warning is logged. Defaults to True.
|
|
507
|
+
warning_message (str, optional):
|
|
508
|
+
Specific warning message. Defaults to "". If not given the error_message will be used.
|
|
509
|
+
failure_message (str, optional):
|
|
510
|
+
Specific error message. Defaults to "".
|
|
511
|
+
success_message (str, optional):
|
|
512
|
+
Specific success message. Defaults to "".
|
|
513
|
+
max_retries (int, optional):
|
|
514
|
+
How many retries on Connection errors? Default is REQUEST_MAX_RETRIES.
|
|
515
|
+
retry_forever (bool, optional):
|
|
516
|
+
Eventually wait forever - without timeout. Defaults to False.
|
|
517
|
+
parse_request_response (bool, optional):
|
|
518
|
+
Defines if the response.text should be interpreted as json and loaded into a dictionary.
|
|
519
|
+
True is the default.
|
|
444
520
|
|
|
445
521
|
Returns:
|
|
446
|
-
dict | None:
|
|
522
|
+
dict | None:
|
|
523
|
+
Response of OTDS REST API or None in case of an error.
|
|
524
|
+
|
|
447
525
|
"""
|
|
448
526
|
|
|
449
527
|
if headers is None:
|
|
@@ -469,26 +547,33 @@ class OTDS:
|
|
|
469
547
|
|
|
470
548
|
if response.ok:
|
|
471
549
|
if success_message:
|
|
472
|
-
logger.info(success_message)
|
|
550
|
+
self.logger.info(success_message)
|
|
473
551
|
if parse_request_response:
|
|
474
552
|
return self.parse_request_response(response)
|
|
475
553
|
else:
|
|
476
554
|
return response
|
|
477
555
|
# Check if Session has expired - then re-authenticate and try once more
|
|
478
|
-
elif
|
|
479
|
-
|
|
556
|
+
elif (
|
|
557
|
+
(response.status_code == 401 and retries == 0)
|
|
558
|
+
or (
|
|
559
|
+
response.status_code == 400
|
|
560
|
+
and retries == 0
|
|
561
|
+
and "Expired OTDS SSO ticket"
|
|
562
|
+
in response.text # OTDS seems to return 400 and not 401 for token expiry (in some cases like impersonation)
|
|
563
|
+
)
|
|
564
|
+
):
|
|
565
|
+
self.logger.info("Session has expired - try to re-authenticate...")
|
|
480
566
|
self.authenticate(revalidate=True)
|
|
481
567
|
retries += 1
|
|
482
568
|
else:
|
|
483
569
|
# Handle plain HTML responses to not pollute the logs
|
|
484
570
|
content_type = response.headers.get("content-type", None)
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
response_text = response.text
|
|
571
|
+
response_text = (
|
|
572
|
+
"HTML content (only printed in debug log)" if content_type == "text/html" else response.text
|
|
573
|
+
)
|
|
489
574
|
|
|
490
575
|
if show_error:
|
|
491
|
-
logger.error(
|
|
576
|
+
self.logger.error(
|
|
492
577
|
"%s; status -> %s/%s; error -> %s",
|
|
493
578
|
failure_message,
|
|
494
579
|
response.status_code,
|
|
@@ -496,7 +581,7 @@ class OTDS:
|
|
|
496
581
|
response_text,
|
|
497
582
|
)
|
|
498
583
|
elif show_warning:
|
|
499
|
-
logger.warning(
|
|
584
|
+
self.logger.warning(
|
|
500
585
|
"%s; status -> %s/%s; warning -> %s",
|
|
501
586
|
warning_message if warning_message else failure_message,
|
|
502
587
|
response.status_code,
|
|
@@ -504,7 +589,7 @@ class OTDS:
|
|
|
504
589
|
response_text,
|
|
505
590
|
)
|
|
506
591
|
if content_type == "text/html":
|
|
507
|
-
logger.debug(
|
|
592
|
+
self.logger.debug(
|
|
508
593
|
"%s; status -> %s/%s; warning -> %s",
|
|
509
594
|
failure_message,
|
|
510
595
|
response.status_code,
|
|
@@ -514,45 +599,45 @@ class OTDS:
|
|
|
514
599
|
return None
|
|
515
600
|
except requests.exceptions.Timeout:
|
|
516
601
|
if retries <= max_retries:
|
|
517
|
-
logger.warning(
|
|
602
|
+
self.logger.warning(
|
|
518
603
|
"Request timed out. Retrying in %s seconds...",
|
|
519
604
|
str(REQUEST_RETRY_DELAY),
|
|
520
605
|
)
|
|
521
606
|
retries += 1
|
|
522
607
|
time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
|
|
523
608
|
else:
|
|
524
|
-
logger.error(
|
|
525
|
-
"%s; timeout error",
|
|
609
|
+
self.logger.error(
|
|
610
|
+
"%s; timeout error.",
|
|
526
611
|
failure_message,
|
|
527
612
|
)
|
|
528
613
|
if retry_forever:
|
|
529
614
|
# If it fails after REQUEST_MAX_RETRIES retries we let it wait forever
|
|
530
|
-
logger.warning("Turn timeouts off and wait forever...")
|
|
615
|
+
self.logger.warning("Turn timeouts off and wait forever...")
|
|
531
616
|
timeout = None
|
|
532
617
|
else:
|
|
533
618
|
return None
|
|
534
619
|
except requests.exceptions.ConnectionError:
|
|
535
620
|
if retries <= max_retries:
|
|
536
|
-
logger.warning(
|
|
621
|
+
self.logger.warning(
|
|
537
622
|
"Connection error. Retrying in %s seconds...",
|
|
538
623
|
str(REQUEST_RETRY_DELAY),
|
|
539
624
|
)
|
|
540
625
|
retries += 1
|
|
541
626
|
time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
|
|
542
627
|
else:
|
|
543
|
-
logger.error(
|
|
544
|
-
"%s; connection error",
|
|
628
|
+
self.logger.error(
|
|
629
|
+
"%s; connection error.",
|
|
545
630
|
failure_message,
|
|
546
631
|
)
|
|
547
632
|
if retry_forever:
|
|
548
633
|
# If it fails after REQUEST_MAX_RETRIES retries we let it wait forever
|
|
549
|
-
logger.warning("Turn timeouts off and wait forever...")
|
|
634
|
+
self.logger.warning("Turn timeouts off and wait forever...")
|
|
550
635
|
timeout = None
|
|
551
636
|
time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
|
|
552
637
|
else:
|
|
553
638
|
return None
|
|
554
639
|
# end try
|
|
555
|
-
logger.
|
|
640
|
+
self.logger.info(
|
|
556
641
|
"Retrying REST API %s call -> %s... (retry = %s, cookie -> %s)",
|
|
557
642
|
method,
|
|
558
643
|
url,
|
|
@@ -569,22 +654,27 @@ class OTDS:
|
|
|
569
654
|
additional_error_message: str = "",
|
|
570
655
|
show_error: bool = True,
|
|
571
656
|
) -> dict | None:
|
|
572
|
-
"""
|
|
573
|
-
that also handles exceptions.
|
|
657
|
+
"""Convert the request response to a dict in a safe way that also handles exceptions.
|
|
574
658
|
|
|
575
659
|
Args:
|
|
576
|
-
response_object (object):
|
|
577
|
-
|
|
578
|
-
|
|
660
|
+
response_object (object):
|
|
661
|
+
This is reponse object delivered by the request call.
|
|
662
|
+
additional_error_message (str, optional):
|
|
663
|
+
Print a custom error message.
|
|
664
|
+
show_error (bool, optional):
|
|
665
|
+
If True, log an error, if False log a warning.
|
|
666
|
+
|
|
579
667
|
Returns:
|
|
580
|
-
dict
|
|
668
|
+
dict | None:
|
|
669
|
+
Response dictionary or None in case of an error.
|
|
670
|
+
|
|
581
671
|
"""
|
|
582
672
|
|
|
583
673
|
if not response_object:
|
|
584
674
|
return None
|
|
585
675
|
|
|
586
676
|
if not response_object.text:
|
|
587
|
-
logger.warning("Response text is empty. Cannot decode response.")
|
|
677
|
+
self.logger.warning("Response text is empty. Cannot decode response.")
|
|
588
678
|
return None
|
|
589
679
|
|
|
590
680
|
try:
|
|
@@ -592,14 +682,15 @@ class OTDS:
|
|
|
592
682
|
except json.JSONDecodeError as e:
|
|
593
683
|
if additional_error_message:
|
|
594
684
|
message = "Cannot decode response as JSon. {}; error -> {}".format(
|
|
595
|
-
additional_error_message,
|
|
685
|
+
additional_error_message,
|
|
686
|
+
e,
|
|
596
687
|
)
|
|
597
688
|
else:
|
|
598
689
|
message = "Cannot decode response as JSon; error -> {}".format(e)
|
|
599
690
|
if show_error:
|
|
600
|
-
logger.error(message)
|
|
691
|
+
self.logger.error(message)
|
|
601
692
|
else:
|
|
602
|
-
logger.warning(message)
|
|
693
|
+
self.logger.warning(message)
|
|
603
694
|
return None
|
|
604
695
|
else:
|
|
605
696
|
return dict_object
|
|
@@ -607,18 +698,22 @@ class OTDS:
|
|
|
607
698
|
# end method definition
|
|
608
699
|
|
|
609
700
|
def authenticate(self, revalidate: bool = False) -> dict | None:
|
|
610
|
-
"""Authenticate at Directory Services and retrieve
|
|
701
|
+
"""Authenticate at Directory Services and retrieve OTDS ticket.
|
|
611
702
|
|
|
612
703
|
Args:
|
|
613
|
-
revalidate (bool, optional):
|
|
614
|
-
|
|
704
|
+
revalidate (bool, optional):
|
|
705
|
+
Determine if a re-athentication is enforced.
|
|
706
|
+
(e.g. if session has timed out with 401 error)
|
|
707
|
+
|
|
615
708
|
Returns:
|
|
616
|
-
dict
|
|
709
|
+
dict | None:
|
|
710
|
+
Cookie information. Also stores cookie information in self._cookie
|
|
711
|
+
|
|
617
712
|
"""
|
|
618
713
|
|
|
619
714
|
# Already authenticated and session still valid?
|
|
620
715
|
if self._cookie and not revalidate:
|
|
621
|
-
logger.debug(
|
|
716
|
+
self.logger.debug(
|
|
622
717
|
"Session still valid - return existing cookie -> %s",
|
|
623
718
|
str(self._cookie),
|
|
624
719
|
)
|
|
@@ -626,7 +721,7 @@ class OTDS:
|
|
|
626
721
|
|
|
627
722
|
otds_ticket = "NotSet"
|
|
628
723
|
|
|
629
|
-
logger.debug("Requesting OTDS ticket from -> %s", self.credential_url())
|
|
724
|
+
self.logger.debug("Requesting OTDS ticket from -> %s", self.credential_url())
|
|
630
725
|
|
|
631
726
|
response = None
|
|
632
727
|
try:
|
|
@@ -634,15 +729,14 @@ class OTDS:
|
|
|
634
729
|
url=self.credential_url(),
|
|
635
730
|
json=self.credentials(),
|
|
636
731
|
headers=REQUEST_HEADERS,
|
|
637
|
-
timeout=
|
|
732
|
+
timeout=REQUEST_TIMEOUT,
|
|
638
733
|
)
|
|
639
734
|
except requests.exceptions.RequestException as exception:
|
|
640
|
-
logger.warning(
|
|
641
|
-
"Unable to connect to -> %s
|
|
735
|
+
self.logger.warning(
|
|
736
|
+
"Unable to connect to OTDS authentication endpoint -> %s%s. OTDS service may not be ready yet.",
|
|
642
737
|
self.credential_url(),
|
|
643
|
-
exception
|
|
738
|
+
"; error -> {}".format(str(exception)) if str(exception) else "",
|
|
644
739
|
)
|
|
645
|
-
logger.warning("OTDS service may not be ready yet.")
|
|
646
740
|
return None
|
|
647
741
|
|
|
648
742
|
if response.ok:
|
|
@@ -651,9 +745,12 @@ class OTDS:
|
|
|
651
745
|
return None
|
|
652
746
|
else:
|
|
653
747
|
otds_ticket = authenticate_dict["ticket"]
|
|
654
|
-
logger.debug("Ticket -> %s", otds_ticket)
|
|
748
|
+
self.logger.debug("Ticket -> %s", otds_ticket)
|
|
655
749
|
else:
|
|
656
|
-
logger.error(
|
|
750
|
+
self.logger.error(
|
|
751
|
+
"Failed to request an OTDS ticket; error -> %s",
|
|
752
|
+
response.text,
|
|
753
|
+
)
|
|
657
754
|
return None
|
|
658
755
|
|
|
659
756
|
# Store authentication ticket:
|
|
@@ -664,21 +761,349 @@ class OTDS:
|
|
|
664
761
|
|
|
665
762
|
# end method definition
|
|
666
763
|
|
|
764
|
+
def impersonate_user(
|
|
765
|
+
self,
|
|
766
|
+
user_id: str,
|
|
767
|
+
partition: str = "Content Server Members",
|
|
768
|
+
ticket: str = "",
|
|
769
|
+
) -> dict | None:
|
|
770
|
+
"""Impersonate as a user.
|
|
771
|
+
|
|
772
|
+
Args:
|
|
773
|
+
partition (str):
|
|
774
|
+
The partition of the user.
|
|
775
|
+
user_id (str):
|
|
776
|
+
The ID (= login) of the user.
|
|
777
|
+
ticket (str, optional):
|
|
778
|
+
Optional, if the ticket to impersonate with is already known.
|
|
779
|
+
Defaults to "".
|
|
780
|
+
|
|
781
|
+
Returns:
|
|
782
|
+
dict | None:
|
|
783
|
+
Information about the impersonated user.
|
|
784
|
+
|
|
785
|
+
Example:
|
|
786
|
+
{
|
|
787
|
+
'token': None,
|
|
788
|
+
'userId': 'nwheeler@Content Server Members',
|
|
789
|
+
'ticket': '*OTDSSSO*Adh...*',
|
|
790
|
+
'resourceID': None,
|
|
791
|
+
'failureReason': None,
|
|
792
|
+
'passwordExpirationTime': 0,
|
|
793
|
+
'continuation': False,
|
|
794
|
+
'continuationContext': None,
|
|
795
|
+
'continuationData': None
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
"""
|
|
799
|
+
|
|
800
|
+
if not ticket:
|
|
801
|
+
ticket = self._otds_ticket
|
|
802
|
+
|
|
803
|
+
request_url = self.config()["ticketforuserUrl"]
|
|
804
|
+
|
|
805
|
+
impersonate_post_body = {
|
|
806
|
+
"userName": user_id + "@" + partition,
|
|
807
|
+
"ticket": ticket,
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
self.logger.debug(
|
|
811
|
+
"Impersonate user -> '%s' ; calling -> %s",
|
|
812
|
+
user_id,
|
|
813
|
+
request_url,
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
return self.do_request(
|
|
817
|
+
url=request_url,
|
|
818
|
+
method="POST",
|
|
819
|
+
json_data=impersonate_post_body,
|
|
820
|
+
timeout=None,
|
|
821
|
+
failure_message="Failed to impersonate as user -> '{}'".format(user_id),
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
# end method definition
|
|
825
|
+
|
|
826
|
+
def add_application_role(
|
|
827
|
+
self,
|
|
828
|
+
name: str,
|
|
829
|
+
partition_id: str = "OAuthClients",
|
|
830
|
+
description: str = "",
|
|
831
|
+
values: list | None = None,
|
|
832
|
+
custom_attributes: list | None = None,
|
|
833
|
+
) -> dict | None:
|
|
834
|
+
"""Add a new application role to partition.
|
|
835
|
+
|
|
836
|
+
Args:
|
|
837
|
+
name (str):
|
|
838
|
+
The name of the new partition.
|
|
839
|
+
partition_id (str, optional):
|
|
840
|
+
ID of the partition to add the role to, defaults to "OAuthClients".
|
|
841
|
+
description (str):
|
|
842
|
+
The description of the new partition.
|
|
843
|
+
values (list, optional):
|
|
844
|
+
List of optional values to pass with the create request.
|
|
845
|
+
custom_attributes (list, optional):
|
|
846
|
+
List of optional custom attributes to pass with the create request.
|
|
847
|
+
|
|
848
|
+
Returns:
|
|
849
|
+
dict | None:
|
|
850
|
+
Request response or None if the creation fails.
|
|
851
|
+
|
|
852
|
+
"""
|
|
853
|
+
|
|
854
|
+
if values is None:
|
|
855
|
+
values = []
|
|
856
|
+
role_post_body_json = {
|
|
857
|
+
"name": name,
|
|
858
|
+
"description": description,
|
|
859
|
+
"userPartitionID": partition_id,
|
|
860
|
+
"values": values if values else [],
|
|
861
|
+
"customAttributes": custom_attributes if custom_attributes else [],
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
request_url = self.config()["rolesUrl"]
|
|
865
|
+
|
|
866
|
+
self.logger.debug(
|
|
867
|
+
"Adding application role -> '%s' (%s) to partition -> '%s' ; calling -> %s",
|
|
868
|
+
name,
|
|
869
|
+
description,
|
|
870
|
+
partition_id,
|
|
871
|
+
request_url,
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
return self.do_request(
|
|
875
|
+
url=request_url,
|
|
876
|
+
method="POST",
|
|
877
|
+
json_data=role_post_body_json,
|
|
878
|
+
timeout=None,
|
|
879
|
+
failure_message="Failed to add application role -> '{}'".format(name),
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
# end method definition
|
|
883
|
+
|
|
884
|
+
def get_application_role(self, name: str, partition: str = "OAuthClients", show_error: bool = True) -> dict | None:
|
|
885
|
+
"""Get an existing application role from OTDS.
|
|
886
|
+
|
|
887
|
+
Args:
|
|
888
|
+
name (str):
|
|
889
|
+
The name of the application role to retrieve.
|
|
890
|
+
partition (str):
|
|
891
|
+
Partition of the application role.
|
|
892
|
+
show_error (bool, optional):
|
|
893
|
+
Defines whether or not we want to log an error
|
|
894
|
+
if the partition is not found.
|
|
895
|
+
|
|
896
|
+
Returns:
|
|
897
|
+
dict | None:
|
|
898
|
+
Request response or None if the REST call fails.
|
|
899
|
+
|
|
900
|
+
"""
|
|
901
|
+
|
|
902
|
+
request_url = "{}?where_filter={}".format(self.config()["rolesUrl"], name)
|
|
903
|
+
|
|
904
|
+
self.logger.debug(
|
|
905
|
+
"Get application Roles -> '%s'; calling -> %s",
|
|
906
|
+
name,
|
|
907
|
+
request_url,
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
response = self.do_request(
|
|
911
|
+
url=request_url,
|
|
912
|
+
method="GET",
|
|
913
|
+
timeout=None,
|
|
914
|
+
failure_message="Failed to get user partition -> '{}'".format(name),
|
|
915
|
+
show_error=show_error,
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
role = next(
|
|
919
|
+
(role for role in response.get("roles") if role["name"] == name and role["userPartitionID"] == partition),
|
|
920
|
+
None,
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
return role
|
|
924
|
+
|
|
925
|
+
# end method definition
|
|
926
|
+
|
|
927
|
+
def assign_user_to_application_role(
|
|
928
|
+
self,
|
|
929
|
+
user_id: str,
|
|
930
|
+
user_partition: str,
|
|
931
|
+
role_name: str,
|
|
932
|
+
role_partition: str = "OAuthClients",
|
|
933
|
+
) -> bool:
|
|
934
|
+
"""Assign an OTDS user to an application role in OTDS.
|
|
935
|
+
|
|
936
|
+
Args:
|
|
937
|
+
user_id (str):
|
|
938
|
+
The ID of the user (= login name) to assign to the license.
|
|
939
|
+
user_partition (str):
|
|
940
|
+
The user partition in OTDS, e.g. "Content Server Members".
|
|
941
|
+
role_name (str):
|
|
942
|
+
Name of the application role to be assigned.
|
|
943
|
+
role_partition (str):
|
|
944
|
+
The name of the partition of the Role, defaults to "OAuthClients".
|
|
945
|
+
|
|
946
|
+
Returns:
|
|
947
|
+
bool:
|
|
948
|
+
True if successful or False if the REST call fails or the license is not found.
|
|
949
|
+
|
|
950
|
+
"""
|
|
951
|
+
|
|
952
|
+
user = self.get_user(user_partition, user_id)
|
|
953
|
+
if user:
|
|
954
|
+
user_location = user["location"]
|
|
955
|
+
else:
|
|
956
|
+
self.logger.error("Cannot find location for user -> '%s'", user_id)
|
|
957
|
+
return False
|
|
958
|
+
|
|
959
|
+
role = self.get_application_role(role_name, role_partition)
|
|
960
|
+
if role:
|
|
961
|
+
rolelocation = role.get("location")
|
|
962
|
+
else:
|
|
963
|
+
self.logger.warning("Cannot find application role -> '%s' (%s)", role_name, role_partition)
|
|
964
|
+
return False
|
|
965
|
+
|
|
966
|
+
role_post_body_json = {
|
|
967
|
+
"stringList": [
|
|
968
|
+
rolelocation,
|
|
969
|
+
],
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
request_url = self.users_url() + "/" + user_location + "/roles"
|
|
973
|
+
|
|
974
|
+
self.logger.debug(
|
|
975
|
+
"Assign application role -> '%s' (%s) to user -> '%s' (%s); calling -> %s",
|
|
976
|
+
role_name,
|
|
977
|
+
role_partition,
|
|
978
|
+
user_id,
|
|
979
|
+
user_partition,
|
|
980
|
+
request_url,
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
response = self.do_request(
|
|
984
|
+
url=request_url,
|
|
985
|
+
method="POST",
|
|
986
|
+
json_data=role_post_body_json,
|
|
987
|
+
timeout=None,
|
|
988
|
+
failure_message="Failed to assign application role -> '{}' to user -> '{}'".format(
|
|
989
|
+
role_name,
|
|
990
|
+
user_id,
|
|
991
|
+
),
|
|
992
|
+
parse_request_response=False,
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
if response and response.ok:
|
|
996
|
+
self.logger.debug(
|
|
997
|
+
"Added application role -> '%s' to user -> '%s'",
|
|
998
|
+
role_name,
|
|
999
|
+
user_id,
|
|
1000
|
+
)
|
|
1001
|
+
return True
|
|
1002
|
+
|
|
1003
|
+
return False
|
|
1004
|
+
|
|
1005
|
+
# end method definition
|
|
1006
|
+
|
|
1007
|
+
def assign_group_to_application_role(
|
|
1008
|
+
self,
|
|
1009
|
+
group_id: str,
|
|
1010
|
+
group_partition: str,
|
|
1011
|
+
role_name: str,
|
|
1012
|
+
role_partition: str = "OAuthClients",
|
|
1013
|
+
) -> bool:
|
|
1014
|
+
"""Assign an OTDS group to an application role in OTDS.
|
|
1015
|
+
|
|
1016
|
+
Args:
|
|
1017
|
+
group_id (str):
|
|
1018
|
+
The ID of the group to assign to the application role.
|
|
1019
|
+
group_partition (str):
|
|
1020
|
+
The group partition in OTDS, e.g. "Content Server Members".
|
|
1021
|
+
role_name (str):
|
|
1022
|
+
Name of the application role to be assigned.
|
|
1023
|
+
role_partition (str):
|
|
1024
|
+
The name of the partition of the Role, defaults to "OAuthClients".
|
|
1025
|
+
|
|
1026
|
+
Returns:
|
|
1027
|
+
bool:
|
|
1028
|
+
True if successful or False if the REST call fails or the license is not found.
|
|
1029
|
+
|
|
1030
|
+
"""
|
|
1031
|
+
|
|
1032
|
+
group = self.get_group(group_id)
|
|
1033
|
+
if group:
|
|
1034
|
+
group_location = group["location"]
|
|
1035
|
+
else:
|
|
1036
|
+
self.logger.error("Cannot find location for group -> '%s'", group_id)
|
|
1037
|
+
return False
|
|
1038
|
+
|
|
1039
|
+
role = self.get_application_role(role_name, role_partition)
|
|
1040
|
+
if role:
|
|
1041
|
+
rolelocation = role.get("location")
|
|
1042
|
+
else:
|
|
1043
|
+
self.logger.warning("Cannot find application role -> '%s' (%s)", role_name, role_partition)
|
|
1044
|
+
return False
|
|
1045
|
+
|
|
1046
|
+
role_post_body_json = {
|
|
1047
|
+
"stringList": [
|
|
1048
|
+
rolelocation,
|
|
1049
|
+
],
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
request_url = self.groups_url() + "/" + group_location + "/roles"
|
|
1053
|
+
|
|
1054
|
+
self.logger.debug(
|
|
1055
|
+
"Assign application role -> '%s' (%s) to group -> '%s' (%s); calling -> %s",
|
|
1056
|
+
role_name,
|
|
1057
|
+
role_partition,
|
|
1058
|
+
group_id,
|
|
1059
|
+
group_partition,
|
|
1060
|
+
request_url,
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
response = self.do_request(
|
|
1064
|
+
url=request_url,
|
|
1065
|
+
method="POST",
|
|
1066
|
+
json_data=role_post_body_json,
|
|
1067
|
+
timeout=None,
|
|
1068
|
+
failure_message="Failed to assign application role -> '{}' to group -> '{}'".format(
|
|
1069
|
+
role_name,
|
|
1070
|
+
group_id,
|
|
1071
|
+
),
|
|
1072
|
+
parse_request_response=False,
|
|
1073
|
+
)
|
|
1074
|
+
|
|
1075
|
+
if response and response.ok:
|
|
1076
|
+
self.logger.debug(
|
|
1077
|
+
"Added application role -> '%s' to group -> '%s'",
|
|
1078
|
+
role_name,
|
|
1079
|
+
group_id,
|
|
1080
|
+
)
|
|
1081
|
+
return True
|
|
1082
|
+
|
|
1083
|
+
return False
|
|
1084
|
+
|
|
1085
|
+
# end method definition
|
|
1086
|
+
|
|
667
1087
|
def add_partition(self, name: str, description: str) -> dict | None:
|
|
668
|
-
"""Add a new user partition to OTDS
|
|
1088
|
+
"""Add a new user partition to OTDS.
|
|
669
1089
|
|
|
670
1090
|
Args:
|
|
671
|
-
name (str):
|
|
672
|
-
|
|
1091
|
+
name (str):
|
|
1092
|
+
The name of the new partition.
|
|
1093
|
+
description (str):
|
|
1094
|
+
The description of the new partition.
|
|
1095
|
+
|
|
673
1096
|
Returns:
|
|
674
|
-
dict
|
|
1097
|
+
dict | None:
|
|
1098
|
+
Request response or None if the creation fails.
|
|
1099
|
+
|
|
675
1100
|
"""
|
|
676
1101
|
|
|
677
1102
|
partition_post_body_json = {"name": name, "description": description}
|
|
678
1103
|
|
|
679
1104
|
request_url = self.partition_url()
|
|
680
1105
|
|
|
681
|
-
logger.debug(
|
|
1106
|
+
self.logger.debug(
|
|
682
1107
|
"Adding user partition -> '%s' (%s); calling -> %s",
|
|
683
1108
|
name,
|
|
684
1109
|
description,
|
|
@@ -696,19 +1121,28 @@ class OTDS:
|
|
|
696
1121
|
# end method definition
|
|
697
1122
|
|
|
698
1123
|
def get_partition(self, name: str, show_error: bool = True) -> dict | None:
|
|
699
|
-
"""Get an existing user partition from OTDS
|
|
1124
|
+
"""Get an existing user partition from OTDS.
|
|
700
1125
|
|
|
701
1126
|
Args:
|
|
702
|
-
name (str):
|
|
703
|
-
|
|
704
|
-
|
|
1127
|
+
name (str):
|
|
1128
|
+
The name of the partition to retrieve.
|
|
1129
|
+
show_error (bool, optional):
|
|
1130
|
+
Defines whether or not we want to log an error
|
|
1131
|
+
if the partition is not found.
|
|
1132
|
+
|
|
705
1133
|
Returns:
|
|
706
|
-
dict
|
|
1134
|
+
dict | None:
|
|
1135
|
+
Request response or None if the REST call fails.
|
|
1136
|
+
|
|
707
1137
|
"""
|
|
708
1138
|
|
|
709
1139
|
request_url = "{}/{}".format(self.config()["partitionUrl"], name)
|
|
710
1140
|
|
|
711
|
-
logger.debug(
|
|
1141
|
+
self.logger.debug(
|
|
1142
|
+
"Get user partition -> '%s'; calling -> %s",
|
|
1143
|
+
name,
|
|
1144
|
+
request_url,
|
|
1145
|
+
)
|
|
712
1146
|
|
|
713
1147
|
return self.do_request(
|
|
714
1148
|
url=request_url,
|
|
@@ -729,17 +1163,26 @@ class OTDS:
|
|
|
729
1163
|
last_name: str = "",
|
|
730
1164
|
email: str = "",
|
|
731
1165
|
) -> dict | None:
|
|
732
|
-
"""Add a new user to a user partition in OTDS
|
|
1166
|
+
"""Add a new user to a user partition in OTDS.
|
|
733
1167
|
|
|
734
1168
|
Args:
|
|
735
|
-
partition (str):
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
1169
|
+
partition (str):
|
|
1170
|
+
The name of the OTDS user partition (needs to exist).
|
|
1171
|
+
name (str):
|
|
1172
|
+
The login name of the new user.
|
|
1173
|
+
description (str, optional):
|
|
1174
|
+
The description of the new user. Default is empty string.
|
|
1175
|
+
first_name (str, optional):
|
|
1176
|
+
The optional first name of the new user.
|
|
1177
|
+
last_name (str, optional):
|
|
1178
|
+
The optional last name of the new user.
|
|
1179
|
+
email (str, optional):
|
|
1180
|
+
The email address of the new user.
|
|
1181
|
+
|
|
741
1182
|
Returns:
|
|
742
|
-
dict
|
|
1183
|
+
dict | None:
|
|
1184
|
+
Request response or None if the creation fails.
|
|
1185
|
+
|
|
743
1186
|
"""
|
|
744
1187
|
|
|
745
1188
|
user_post_body_json = {
|
|
@@ -755,13 +1198,13 @@ class OTDS:
|
|
|
755
1198
|
|
|
756
1199
|
request_url = self.users_url()
|
|
757
1200
|
|
|
758
|
-
logger.debug(
|
|
1201
|
+
self.logger.debug(
|
|
759
1202
|
"Adding user -> '%s' to partition -> '%s'; calling -> %s",
|
|
760
1203
|
name,
|
|
761
1204
|
partition,
|
|
762
1205
|
request_url,
|
|
763
1206
|
)
|
|
764
|
-
logger.debug("User Attributes -> %s", str(user_post_body_json))
|
|
1207
|
+
self.logger.debug("User Attributes -> %s", str(user_post_body_json))
|
|
765
1208
|
|
|
766
1209
|
return self.do_request(
|
|
767
1210
|
url=request_url,
|
|
@@ -774,18 +1217,23 @@ class OTDS:
|
|
|
774
1217
|
# end method definition
|
|
775
1218
|
|
|
776
1219
|
def get_user(self, partition: str, user_id: str) -> dict | None:
|
|
777
|
-
"""Get
|
|
1220
|
+
"""Get an existing user by its partition and user ID.
|
|
778
1221
|
|
|
779
1222
|
Args:
|
|
780
|
-
partition (str):
|
|
781
|
-
|
|
1223
|
+
partition (str):
|
|
1224
|
+
The name of the partition the user is in.
|
|
1225
|
+
user_id (str):
|
|
1226
|
+
The ID of the user (= login name).
|
|
1227
|
+
|
|
782
1228
|
Returns:
|
|
783
|
-
dict
|
|
1229
|
+
dict | None:
|
|
1230
|
+
Request response or None if the user was not found.
|
|
1231
|
+
|
|
784
1232
|
"""
|
|
785
1233
|
|
|
786
1234
|
request_url = self.users_url() + "/" + user_id + "@" + partition
|
|
787
1235
|
|
|
788
|
-
logger.debug(
|
|
1236
|
+
self.logger.debug(
|
|
789
1237
|
"Get user -> '%s' in partition -> '%s'; calling -> %s",
|
|
790
1238
|
user_id,
|
|
791
1239
|
partition,
|
|
@@ -801,22 +1249,141 @@ class OTDS:
|
|
|
801
1249
|
|
|
802
1250
|
# end method definition
|
|
803
1251
|
|
|
804
|
-
def get_users(
|
|
805
|
-
|
|
1252
|
+
def get_users(
|
|
1253
|
+
self,
|
|
1254
|
+
partition: str = "",
|
|
1255
|
+
where_filter: str | None = None,
|
|
1256
|
+
where_location: str | None = None,
|
|
1257
|
+
where_state: str | None = None,
|
|
1258
|
+
limit: int | None = None,
|
|
1259
|
+
page_size: int | None = None,
|
|
1260
|
+
attributes_as_keys: bool = True,
|
|
1261
|
+
next_page_cookie: str | None = None,
|
|
1262
|
+
) -> dict | None:
|
|
1263
|
+
"""Get all users in a partition. Additional filters can be applied.
|
|
806
1264
|
|
|
807
1265
|
Args:
|
|
808
|
-
partition (str, optional):
|
|
809
|
-
|
|
1266
|
+
partition (str, optional):
|
|
1267
|
+
The name of the partition.
|
|
1268
|
+
where_filter (str | None, optional):
|
|
1269
|
+
Filter returned users. This is a string filter.
|
|
1270
|
+
If None, no filtering applies.
|
|
1271
|
+
where_location (str | None, optional):
|
|
1272
|
+
Filter based on the DN of the Organizational Unit.
|
|
1273
|
+
where_state (str | None, optional):
|
|
1274
|
+
Filter returned users by their state. Possible values are 'enabled' and 'disabled'.
|
|
1275
|
+
If None, no filtering based on state applies.
|
|
1276
|
+
limit (int, optional):
|
|
1277
|
+
The maximum number of users to return. None = unlimited.
|
|
1278
|
+
page_size (int, optional):
|
|
1279
|
+
The chunk size for the number of users returned by one
|
|
1280
|
+
REST API call. If None, then a default of 250 is used.
|
|
1281
|
+
attributes_as_keys (bool, optional):
|
|
1282
|
+
If True, it creates a much simpler to parse result structure
|
|
1283
|
+
per user that includes the user attributes in a "attributes"
|
|
1284
|
+
dictionary where the keys are the attribute names and the
|
|
1285
|
+
values the attribute values.
|
|
1286
|
+
'attributes': {
|
|
1287
|
+
'schemaType' = ['3']
|
|
1288
|
+
'cn' = ['psopentext.com']
|
|
1289
|
+
...
|
|
1290
|
+
}
|
|
1291
|
+
If False a "values" list with "name" and "values" elements is created:
|
|
1292
|
+
'values': [
|
|
1293
|
+
{
|
|
1294
|
+
'name': 'schemaType',
|
|
1295
|
+
'values': ['3']
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
'name': 'cn',
|
|
1299
|
+
'values': ['xyz@opentext.com']
|
|
1300
|
+
},
|
|
1301
|
+
...
|
|
1302
|
+
]
|
|
1303
|
+
Default is True (= attributes as keys).
|
|
1304
|
+
next_page_cookie (str, optional):
|
|
1305
|
+
A key returned by a former call to this method in with
|
|
1306
|
+
a return key 'nextPageCookie' (see example below). This
|
|
1307
|
+
can be used to get the next page of result items.
|
|
1308
|
+
|
|
810
1309
|
Returns:
|
|
811
|
-
dict
|
|
1310
|
+
dict | None:
|
|
1311
|
+
Request response or None if the user was not found.
|
|
1312
|
+
|
|
1313
|
+
Example:
|
|
1314
|
+
{
|
|
1315
|
+
'actualPageSize': 21,
|
|
1316
|
+
'users': [
|
|
1317
|
+
{
|
|
1318
|
+
'userPartitionID': 'Content Server Members',
|
|
1319
|
+
'name': 'ps@opentext.com',
|
|
1320
|
+
'location': 'oTPerson=04f2d12b-b7aa-4797-b4eb-1b6e6bd5ce2e,orgunit=users,partition=Content Server Members,dc=identity,dc=opentext,dc=net',
|
|
1321
|
+
'id': 'ps@opentext.com',
|
|
1322
|
+
'attributes': {
|
|
1323
|
+
'oTExternalID3': ['ps@opentext.com'],
|
|
1324
|
+
'entryUUID': ['04f2d12b-b7aa-4797-b4eb-1b6e6bd5ce2e'],
|
|
1325
|
+
'oTExternalID4': ['Content Server Membersps@opentext.com'],
|
|
1326
|
+
'mail': ['ps@opentext.com'],
|
|
1327
|
+
'displayName': ['Paul Smith'],
|
|
1328
|
+
'oTMemberOf': ['oTGroup=6381fcfe-7b30-4bbb-b849-2cbd8f3a0a48,dc=identity,dc=opentext,dc=net'],
|
|
1329
|
+
'description': ['test description'],
|
|
1330
|
+
'title': ['Lead Systems Analyst'],
|
|
1331
|
+
'oTExternalID1': ['ps@opentext.com'],
|
|
1332
|
+
'modifyTimestamp': ['2025-01-24T10:19:22Z'],
|
|
1333
|
+
'oTExternalID2': ['ps@opentext.com@Content Server Members'],
|
|
1334
|
+
'createTimestamp': ['2025-01-24T10:18:53Z'],
|
|
1335
|
+
'passwordChangedTime': ['2025-01-24T10:18:53Z'],
|
|
1336
|
+
'UserMustChangePasswordAtNextSignIn': ['false'],
|
|
1337
|
+
'sn': ['Smith'],
|
|
1338
|
+
'entryDN': ['oTPerson=04f2d12b-b7aa-4797-b4eb-1b6e6bd5ce2e,orgunit=users,partition=Content Server Members,dc=identity,dc=opentext,dc=net'],
|
|
1339
|
+
'oTObjectGUID': ['BPLRK7eqR5e06xtua9XOLg=='],
|
|
1340
|
+
'UserCannotChangePassword': ['true'],
|
|
1341
|
+
'oTLastLoginTimestamp': ['2025-01-24T10:19:21Z'],
|
|
1342
|
+
'oTSource': ['cs'],
|
|
1343
|
+
'PasswordNeverExpires': ['true'],
|
|
1344
|
+
'givenName': ['Paul'],
|
|
1345
|
+
'cn': ['ps@opentext.com'],
|
|
1346
|
+
'pwdReset': ['true'],
|
|
1347
|
+
'oTObjectIDInResource': ['3b461d9f-ed1d-4be3-859a-316d8eb35aa5:6650'],
|
|
1348
|
+
'accountLockedOut': ['false'],
|
|
1349
|
+
'schemaType': ['3'],
|
|
1350
|
+
'accountDisabled': ['false']
|
|
1351
|
+
}
|
|
1352
|
+
'values': [], # empty because this example is with attrAsKeys = True
|
|
1353
|
+
'customAttributes': None,
|
|
1354
|
+
'objectClass': 'oTPerson',
|
|
1355
|
+
'uuid': '04f2d12b-b7aa-4797-b4eb-1b6e6bd5ce2e',
|
|
1356
|
+
'description': 'test description',
|
|
1357
|
+
'originUUID': None,
|
|
1358
|
+
'urlId': 'xyz@opentext.com',
|
|
1359
|
+
'urlLocation': 'oTPerson=04f2d12b-b7aa-4797-b4eb-1b6e6bd5ce2e,orgunit=users,partition=Content Server Members,dc=identity,dc=opentext,dc=net'
|
|
1360
|
+
},
|
|
1361
|
+
...
|
|
1362
|
+
],
|
|
1363
|
+
'nextPageCookie': 'JIHw2CLHSoeTOmo7Ng/bPw==',
|
|
1364
|
+
'requestedPageSize': 250
|
|
1365
|
+
}
|
|
1366
|
+
|
|
812
1367
|
"""
|
|
813
1368
|
|
|
814
1369
|
# Add query parameters (these are NOT passed via JSon body!)
|
|
815
1370
|
query = {}
|
|
816
|
-
if limit:
|
|
817
|
-
query["limit"] = limit
|
|
818
1371
|
if partition:
|
|
819
1372
|
query["where_partition_name"] = partition
|
|
1373
|
+
if where_filter:
|
|
1374
|
+
query["where_filter"] = where_filter
|
|
1375
|
+
if where_location:
|
|
1376
|
+
query["where_location"] = where_location
|
|
1377
|
+
if where_state:
|
|
1378
|
+
query["where_state"] = where_state
|
|
1379
|
+
if limit:
|
|
1380
|
+
query["limit"] = limit
|
|
1381
|
+
if page_size:
|
|
1382
|
+
query["page_size"] = page_size
|
|
1383
|
+
if attributes_as_keys:
|
|
1384
|
+
query["attrsAsKeys"] = attributes_as_keys
|
|
1385
|
+
if next_page_cookie:
|
|
1386
|
+
query["next_page_cookie"] = next_page_cookie
|
|
820
1387
|
|
|
821
1388
|
encoded_query = urllib.parse.urlencode(query, doseq=True)
|
|
822
1389
|
|
|
@@ -825,17 +1392,17 @@ class OTDS:
|
|
|
825
1392
|
request_url += "?{}".format(encoded_query)
|
|
826
1393
|
|
|
827
1394
|
if partition:
|
|
828
|
-
logger.debug(
|
|
1395
|
+
self.logger.debug(
|
|
829
1396
|
"Get all users in partition -> '%s' (limit -> %s); calling -> %s",
|
|
830
1397
|
partition,
|
|
831
1398
|
limit,
|
|
832
1399
|
request_url,
|
|
833
1400
|
)
|
|
834
1401
|
failure_message = "Failed to get all users in partition -> '{}'".format(
|
|
835
|
-
partition
|
|
1402
|
+
partition,
|
|
836
1403
|
)
|
|
837
1404
|
else:
|
|
838
|
-
logger.debug(
|
|
1405
|
+
self.logger.debug(
|
|
839
1406
|
"Get all users (limit -> %s); calling -> %s",
|
|
840
1407
|
limit,
|
|
841
1408
|
request_url,
|
|
@@ -843,23 +1410,140 @@ class OTDS:
|
|
|
843
1410
|
failure_message = "Failed to get all users"
|
|
844
1411
|
|
|
845
1412
|
return self.do_request(
|
|
846
|
-
url=request_url,
|
|
1413
|
+
url=request_url,
|
|
1414
|
+
method="GET",
|
|
1415
|
+
timeout=None,
|
|
1416
|
+
failure_message=failure_message,
|
|
1417
|
+
)
|
|
1418
|
+
|
|
1419
|
+
# end method definition
|
|
1420
|
+
|
|
1421
|
+
def get_users_iterator(
|
|
1422
|
+
self,
|
|
1423
|
+
partition: str = "",
|
|
1424
|
+
where_state: str | None = None,
|
|
1425
|
+
where_filter: str | None = None,
|
|
1426
|
+
where_location: str | None = None,
|
|
1427
|
+
page_size: int | None = None,
|
|
1428
|
+
) -> iter:
|
|
1429
|
+
"""Get an iterator object that can be used to traverse all members for a given users partition.
|
|
1430
|
+
|
|
1431
|
+
Filters such as user state, location, etc. can be applied.
|
|
1432
|
+
|
|
1433
|
+
Returning a generator avoids loading a large number of nodes into memory at once. Instead you
|
|
1434
|
+
can iterate over the potential large list of related workspaces.
|
|
1435
|
+
|
|
1436
|
+
Example usage:
|
|
1437
|
+
users = otds_object.get_users_iterator(partition="Content Server Members", page_size=10)
|
|
1438
|
+
for user in users:
|
|
1439
|
+
logger.info("Traversing user -> %s", user["name"])
|
|
1440
|
+
|
|
1441
|
+
Args:
|
|
1442
|
+
partition (str, optional):
|
|
1443
|
+
The name of the partition.
|
|
1444
|
+
where_filter (str | None, optional):
|
|
1445
|
+
Filter returned users. This is a string filter.
|
|
1446
|
+
If None, no filtering applies.
|
|
1447
|
+
where_location (str | None, optional):
|
|
1448
|
+
Filter based on the DN of the Organizational Unit.
|
|
1449
|
+
where_state (str | None, optional):
|
|
1450
|
+
Filter returned users by their state. Possible values are 'enabled' and 'disabled'.
|
|
1451
|
+
If None, no filtering based on state applies.
|
|
1452
|
+
page_size (int, optional):
|
|
1453
|
+
The chunk size for the number of users returned by one
|
|
1454
|
+
REST API call. If None, then a default of 250 is used.
|
|
1455
|
+
next_page_cookie (str, optional):
|
|
1456
|
+
A key returned by a former call to this method in with
|
|
1457
|
+
a return key 'nextPageCookie' (see example below). This
|
|
1458
|
+
can be used to get the next page of result items.
|
|
1459
|
+
|
|
1460
|
+
Returns:
|
|
1461
|
+
iter:
|
|
1462
|
+
A generator yielding one OTDS user per iteration.
|
|
1463
|
+
If the REST API fails, returns no value.
|
|
1464
|
+
|
|
1465
|
+
"""
|
|
1466
|
+
|
|
1467
|
+
next_page_cookie = None
|
|
1468
|
+
|
|
1469
|
+
while True:
|
|
1470
|
+
response = self.get_users(
|
|
1471
|
+
partition=partition,
|
|
1472
|
+
where_filter=where_filter,
|
|
1473
|
+
where_location=where_location,
|
|
1474
|
+
where_state=where_state,
|
|
1475
|
+
page_size=page_size,
|
|
1476
|
+
next_page_cookie=next_page_cookie,
|
|
1477
|
+
)
|
|
1478
|
+
if not response or "users" not in response:
|
|
1479
|
+
# Don't return None! Plain return is what we need for iterators.
|
|
1480
|
+
# Natural Termination: If the generator does not yield, it behaves
|
|
1481
|
+
# like an empty iterable when used in a loop or converted to a list:
|
|
1482
|
+
return
|
|
1483
|
+
|
|
1484
|
+
# Yield users one at a time:
|
|
1485
|
+
yield from response["users"]
|
|
1486
|
+
|
|
1487
|
+
# See if we have an additional result page.
|
|
1488
|
+
# If not terminate the iterator and return
|
|
1489
|
+
# no value.
|
|
1490
|
+
next_page_cookie = response["nextPageCookie"]
|
|
1491
|
+
if not next_page_cookie:
|
|
1492
|
+
# Don't return None! Plain return is what we need for iterators.
|
|
1493
|
+
# Natural Termination: If the generator does not yield, it behaves
|
|
1494
|
+
# like an empty iterable when used in a loop or converted to a list:
|
|
1495
|
+
return
|
|
1496
|
+
|
|
1497
|
+
# end method definition
|
|
1498
|
+
|
|
1499
|
+
def get_current_user(self) -> dict | None:
|
|
1500
|
+
"""Get the currently logged in user.
|
|
1501
|
+
|
|
1502
|
+
Returns:
|
|
1503
|
+
dict | None:
|
|
1504
|
+
Request response or None if the user was not found.
|
|
1505
|
+
|
|
1506
|
+
"""
|
|
1507
|
+
|
|
1508
|
+
request_url = self.current_user_url()
|
|
1509
|
+
|
|
1510
|
+
self.logger.debug(
|
|
1511
|
+
"Get current user; calling -> %s",
|
|
1512
|
+
request_url,
|
|
1513
|
+
)
|
|
1514
|
+
|
|
1515
|
+
return self.do_request(
|
|
1516
|
+
url=request_url,
|
|
1517
|
+
method="GET",
|
|
1518
|
+
timeout=None,
|
|
1519
|
+
failure_message="Failed to get current user",
|
|
847
1520
|
)
|
|
848
1521
|
|
|
849
1522
|
# end method definition
|
|
850
1523
|
|
|
851
1524
|
def update_user(
|
|
852
|
-
self,
|
|
1525
|
+
self,
|
|
1526
|
+
partition: str,
|
|
1527
|
+
user_id: str,
|
|
1528
|
+
attribute_name: str,
|
|
1529
|
+
attribute_value: str,
|
|
853
1530
|
) -> dict | None:
|
|
854
|
-
"""Update a user attribute with a new value
|
|
1531
|
+
"""Update a user attribute with a new value.
|
|
855
1532
|
|
|
856
1533
|
Args:
|
|
857
|
-
partition (str):
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1534
|
+
partition (str):
|
|
1535
|
+
The name of the partition the user is in.
|
|
1536
|
+
user_id (str):
|
|
1537
|
+
The ID of the user (= login name).
|
|
1538
|
+
attribute_name (str):
|
|
1539
|
+
The name of the attribute.
|
|
1540
|
+
attribute_value (str):
|
|
1541
|
+
The new (updated) value of the attribute.
|
|
1542
|
+
|
|
1543
|
+
Returns:
|
|
1544
|
+
dict | None:
|
|
1545
|
+
Request response or None if the update fails.
|
|
1546
|
+
|
|
863
1547
|
"""
|
|
864
1548
|
|
|
865
1549
|
if attribute_name in ["description"]:
|
|
@@ -875,7 +1559,7 @@ class OTDS:
|
|
|
875
1559
|
|
|
876
1560
|
request_url = self.users_url() + "/" + user_id
|
|
877
1561
|
|
|
878
|
-
logger.debug(
|
|
1562
|
+
self.logger.debug(
|
|
879
1563
|
"Update user -> '%s' attribute -> '%s' to value -> '%s'; calling -> %s",
|
|
880
1564
|
user_id,
|
|
881
1565
|
attribute_name,
|
|
@@ -894,18 +1578,23 @@ class OTDS:
|
|
|
894
1578
|
# end method definition
|
|
895
1579
|
|
|
896
1580
|
def delete_user(self, partition: str, user_id: str) -> bool:
|
|
897
|
-
"""Delete an existing user
|
|
1581
|
+
"""Delete an existing user.
|
|
898
1582
|
|
|
899
1583
|
Args:
|
|
900
|
-
partition (str):
|
|
901
|
-
|
|
1584
|
+
partition (str):
|
|
1585
|
+
The name of the partition the user is in.
|
|
1586
|
+
user_id (str):
|
|
1587
|
+
The ID (= login name) of the user to delete.
|
|
1588
|
+
|
|
902
1589
|
Returns:
|
|
903
|
-
bool:
|
|
1590
|
+
bool:
|
|
1591
|
+
True = success, False = error
|
|
1592
|
+
|
|
904
1593
|
"""
|
|
905
1594
|
|
|
906
1595
|
request_url = self.users_url() + "/" + user_id + "@" + partition
|
|
907
1596
|
|
|
908
|
-
logger.debug(
|
|
1597
|
+
self.logger.debug(
|
|
909
1598
|
"Delete user -> '%s' in partition -> '%s'; calling -> %s",
|
|
910
1599
|
user_id,
|
|
911
1600
|
partition,
|
|
@@ -920,29 +1609,33 @@ class OTDS:
|
|
|
920
1609
|
parse_request_response=False,
|
|
921
1610
|
)
|
|
922
1611
|
|
|
923
|
-
|
|
924
|
-
return True
|
|
925
|
-
|
|
926
|
-
return False
|
|
1612
|
+
bool(response and response.ok)
|
|
927
1613
|
|
|
928
1614
|
# end method definition
|
|
929
1615
|
|
|
930
1616
|
def reset_user_password(self, user_id: str, password: str) -> bool:
|
|
931
|
-
"""Reset a password of an existing user
|
|
1617
|
+
"""Reset a password of an existing user.
|
|
932
1618
|
|
|
933
1619
|
Args:
|
|
934
|
-
user_id (str):
|
|
935
|
-
|
|
1620
|
+
user_id (str):
|
|
1621
|
+
The Id (= login name) of the user.
|
|
1622
|
+
password (str):
|
|
1623
|
+
The new password of the user.
|
|
1624
|
+
|
|
936
1625
|
Returns:
|
|
937
|
-
bool:
|
|
1626
|
+
bool:
|
|
1627
|
+
True = success, False = error.
|
|
1628
|
+
|
|
938
1629
|
"""
|
|
939
1630
|
|
|
940
1631
|
user_post_body_json = {"newPassword": password}
|
|
941
1632
|
|
|
942
1633
|
request_url = "{}/{}/password".format(self.users_url(), user_id)
|
|
943
1634
|
|
|
944
|
-
logger.debug(
|
|
945
|
-
"Resetting password for user -> '%s'; calling -> %s",
|
|
1635
|
+
self.logger.debug(
|
|
1636
|
+
"Resetting password for user -> '%s'; calling -> %s",
|
|
1637
|
+
user_id,
|
|
1638
|
+
request_url,
|
|
946
1639
|
)
|
|
947
1640
|
|
|
948
1641
|
response = self.do_request(
|
|
@@ -954,22 +1647,25 @@ class OTDS:
|
|
|
954
1647
|
parse_request_response=False,
|
|
955
1648
|
)
|
|
956
1649
|
|
|
957
|
-
|
|
958
|
-
return True
|
|
959
|
-
|
|
960
|
-
return False
|
|
1650
|
+
bool(response and response.ok)
|
|
961
1651
|
|
|
962
1652
|
# end method definition
|
|
963
1653
|
|
|
964
1654
|
def add_group(self, partition: str, name: str, description: str) -> dict | None:
|
|
965
|
-
"""Add a new user group to a user partition in OTDS
|
|
1655
|
+
"""Add a new user group to a user partition in OTDS.
|
|
966
1656
|
|
|
967
1657
|
Args:
|
|
968
|
-
partition (str):
|
|
969
|
-
|
|
970
|
-
|
|
1658
|
+
partition (str):
|
|
1659
|
+
The name of the OTDS user partition (needs to exist).
|
|
1660
|
+
name (str):
|
|
1661
|
+
The name of the new group.
|
|
1662
|
+
description (str):
|
|
1663
|
+
The description of the new group.
|
|
1664
|
+
|
|
971
1665
|
Returns:
|
|
972
|
-
dict
|
|
1666
|
+
dict | None:
|
|
1667
|
+
Request response (json) or None if the creation fails.
|
|
1668
|
+
|
|
973
1669
|
"""
|
|
974
1670
|
|
|
975
1671
|
group_post_body_json = {
|
|
@@ -980,13 +1676,13 @@ class OTDS:
|
|
|
980
1676
|
|
|
981
1677
|
request_url = self.groups_url()
|
|
982
1678
|
|
|
983
|
-
logger.debug(
|
|
1679
|
+
self.logger.debug(
|
|
984
1680
|
"Adding group -> '%s' to partition -> '%s'; calling -> %s",
|
|
985
1681
|
name,
|
|
986
1682
|
partition,
|
|
987
1683
|
request_url,
|
|
988
1684
|
)
|
|
989
|
-
logger.debug("Group Attributes -> %s", str(group_post_body_json))
|
|
1685
|
+
self.logger.debug("Group Attributes -> %s", str(group_post_body_json))
|
|
990
1686
|
|
|
991
1687
|
return self.do_request(
|
|
992
1688
|
url=request_url,
|
|
@@ -999,14 +1695,19 @@ class OTDS:
|
|
|
999
1695
|
# end method definition
|
|
1000
1696
|
|
|
1001
1697
|
def get_group(self, group: str, show_error: bool = True) -> dict | None:
|
|
1002
|
-
"""Get a OTDS group by its group name
|
|
1698
|
+
"""Get a OTDS group by its group name.
|
|
1003
1699
|
|
|
1004
1700
|
Args:
|
|
1005
|
-
group (str):
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1701
|
+
group (str):
|
|
1702
|
+
The ID of the group (= group name).
|
|
1703
|
+
show_error (bool, optional):
|
|
1704
|
+
If True, log an error if resource is not found. Otherwise log a warning.
|
|
1705
|
+
|
|
1706
|
+
Returns:
|
|
1707
|
+
dict | None:
|
|
1708
|
+
Request response or None if the group was not found.
|
|
1709
|
+
|
|
1710
|
+
Example:
|
|
1010
1711
|
{
|
|
1011
1712
|
'numMembers': 7,
|
|
1012
1713
|
'userPartitionID': 'Content Server Members',
|
|
@@ -1022,11 +1723,12 @@ class OTDS:
|
|
|
1022
1723
|
'urlId': 'Sales@Content Server Members',
|
|
1023
1724
|
'urlLocation': 'oTGroup=3f921294-b92a-4c9e-bf7c-b50df16bb937,orgunit=groups,partition=Content Server Members,dc=identity,dc=opentext,dc=net'
|
|
1024
1725
|
}
|
|
1726
|
+
|
|
1025
1727
|
"""
|
|
1026
1728
|
|
|
1027
1729
|
request_url = self.groups_url() + "/" + group
|
|
1028
1730
|
|
|
1029
|
-
logger.debug("Get group -> '%s'; calling -> %s", group, request_url)
|
|
1731
|
+
self.logger.debug("Get group -> '%s'; calling -> %s", group, request_url)
|
|
1030
1732
|
|
|
1031
1733
|
return self.do_request(
|
|
1032
1734
|
url=request_url,
|
|
@@ -1038,21 +1740,275 @@ class OTDS:
|
|
|
1038
1740
|
|
|
1039
1741
|
# end method definition
|
|
1040
1742
|
|
|
1743
|
+
def get_groups(
|
|
1744
|
+
self,
|
|
1745
|
+
partition: str = "",
|
|
1746
|
+
where_filter: str | None = None,
|
|
1747
|
+
where_location: str | None = None,
|
|
1748
|
+
limit: int | None = None,
|
|
1749
|
+
page_size: int | None = None,
|
|
1750
|
+
attributes_as_keys: bool = True,
|
|
1751
|
+
next_page_cookie: str | None = None,
|
|
1752
|
+
) -> dict | None:
|
|
1753
|
+
"""Get all groups in a partition. Additional filters can be applied.
|
|
1754
|
+
|
|
1755
|
+
Args:
|
|
1756
|
+
partition (str, optional):
|
|
1757
|
+
The name of the partition.
|
|
1758
|
+
where_filter (str | None, optional):
|
|
1759
|
+
Filter returned groups. This is a string filter.
|
|
1760
|
+
If None, no filtering applies.
|
|
1761
|
+
where_location (str | None, optional):
|
|
1762
|
+
Filter based on the DN of the Organizational Unit.
|
|
1763
|
+
limit (int, optional):
|
|
1764
|
+
The maximum number of groups to return. None = unlimited.
|
|
1765
|
+
page_size (int, optional):
|
|
1766
|
+
The chunk size for the number of groups returned by one
|
|
1767
|
+
REST API call. If None, then a default of 250 is used.
|
|
1768
|
+
attributes_as_keys (bool, optional):
|
|
1769
|
+
If True, it creates a much simpler to parse result structure
|
|
1770
|
+
per group that includes the group attributes in a "attributes"
|
|
1771
|
+
dictionary where the keys are the attribute names and the
|
|
1772
|
+
values the attribute values.
|
|
1773
|
+
'attributes': {
|
|
1774
|
+
'schemaType' = ['3']
|
|
1775
|
+
'cn' = [...]
|
|
1776
|
+
...
|
|
1777
|
+
}
|
|
1778
|
+
If False a "values" list with "name" and "values" elements is created:
|
|
1779
|
+
'values': [
|
|
1780
|
+
{
|
|
1781
|
+
'name': 'schemaType',
|
|
1782
|
+
'values': ['3']
|
|
1783
|
+
},
|
|
1784
|
+
{
|
|
1785
|
+
'name': 'cn',
|
|
1786
|
+
'values': ['...']
|
|
1787
|
+
},
|
|
1788
|
+
...
|
|
1789
|
+
]
|
|
1790
|
+
Default is True (= attributes as keys).
|
|
1791
|
+
next_page_cookie (str, optional):
|
|
1792
|
+
A key returned by a former call to this method in with
|
|
1793
|
+
a return key 'nextPageCookie' (see example below). This
|
|
1794
|
+
can be used to get the next page of result items.
|
|
1795
|
+
|
|
1796
|
+
Returns:
|
|
1797
|
+
dict | None:
|
|
1798
|
+
Request response or None if the user was not found.
|
|
1799
|
+
|
|
1800
|
+
Example:
|
|
1801
|
+
{
|
|
1802
|
+
'groups': [
|
|
1803
|
+
{
|
|
1804
|
+
'numMembers': 0,
|
|
1805
|
+
'userPartitionID': 'Content Server Members',
|
|
1806
|
+
'name': 'Unified_ArchiveLink',
|
|
1807
|
+
'location': 'oTGroup=050a3c27-7636-4406-a94e-dcc4947fa21f,orgunit=groups,partition=Content Server Members,dc=identity,dc=opentext,dc=net',
|
|
1808
|
+
'id': 'Unified_ArchiveLink@Content Server Members',
|
|
1809
|
+
'attributes': {
|
|
1810
|
+
'oTExternalID3': [...],
|
|
1811
|
+
'entryUUID': [...],
|
|
1812
|
+
'oTExternalID4': [...],
|
|
1813
|
+
'oTObjectIDInResource': [...],
|
|
1814
|
+
'oTSource': [...],
|
|
1815
|
+
'schemaType': [...],
|
|
1816
|
+
'cn': [...],
|
|
1817
|
+
'oTObjectGUID': [...],
|
|
1818
|
+
'oTExternalID1': [...],
|
|
1819
|
+
'entryDN': [...],
|
|
1820
|
+
'oTExternalID2': [...],
|
|
1821
|
+
'createTimestamp': [...]
|
|
1822
|
+
},
|
|
1823
|
+
'values': None,
|
|
1824
|
+
'customAttributes': None,
|
|
1825
|
+
'objectClass': 'oTGroup',
|
|
1826
|
+
'uuid': '050a3c27-7636-4406-a94e-dcc4947fa21f',
|
|
1827
|
+
'description': None,
|
|
1828
|
+
'originUUID': None,
|
|
1829
|
+
'urlId': 'Unified_ArchiveLink@Content Server Members',
|
|
1830
|
+
'urlLocation': 'oTGroup=050a3c27-7636-4406-a94e-dcc4947fa21f,orgunit=groups,partition=Content Server Members,dc=identity,dc=opentext,dc=net'
|
|
1831
|
+
},
|
|
1832
|
+
{
|
|
1833
|
+
'numMembers': 0,
|
|
1834
|
+
'userPartitionID': 'Content Server Members',
|
|
1835
|
+
'name': 'R&D',
|
|
1836
|
+
'location': 'oTGroup=24356f83-5636-47f0-9ac3-9646d3b34804,orgunit=groups,partition=Content Server Members,dc=identity,dc=opentext,dc=net',
|
|
1837
|
+
'id': 'R&D@Content Server Members',
|
|
1838
|
+
'attributes': {
|
|
1839
|
+
'oTExternalID3': [...],
|
|
1840
|
+
'entryUUID': [...],
|
|
1841
|
+
'oTExternalID4': [...],
|
|
1842
|
+
'oTObjectIDInResource': [...],
|
|
1843
|
+
'oTSource': [...],
|
|
1844
|
+
'schemaType': [...],
|
|
1845
|
+
'cn': [...],
|
|
1846
|
+
'oTObjectGUID': [...],
|
|
1847
|
+
'oTExternalID1': [...],
|
|
1848
|
+
'entryDN': [...],
|
|
1849
|
+
'oTExternalID2': [...],
|
|
1850
|
+
'createTimestamp': [...]
|
|
1851
|
+
},
|
|
1852
|
+
'values': None,
|
|
1853
|
+
'customAttributes': None,
|
|
1854
|
+
'objectClass': 'oTGroup',
|
|
1855
|
+
'uuid': '24356f83-5636-47f0-9ac3-9646d3b34804',
|
|
1856
|
+
'description': None,
|
|
1857
|
+
'originUUID': None,
|
|
1858
|
+
'urlId': 'R&D@Content Server Members',
|
|
1859
|
+
'urlLocation': 'oTGroup=24356f83-5636-47f0-9ac3-9646d3b34804,orgunit=groups,partition=Content Server Members,dc=identity,dc=opentext,dc=net'
|
|
1860
|
+
},
|
|
1861
|
+
...
|
|
1862
|
+
],
|
|
1863
|
+
'actualPageSize': 5,
|
|
1864
|
+
'nextPageCookie': 'JIHw2CLHSoeTOmo7Ng/bPw==',
|
|
1865
|
+
'requestedPageSize': 5
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
"""
|
|
1869
|
+
|
|
1870
|
+
# Add query parameters (these are NOT passed via request body!)
|
|
1871
|
+
query = {}
|
|
1872
|
+
if partition:
|
|
1873
|
+
query["where_partition_name"] = partition
|
|
1874
|
+
if where_filter:
|
|
1875
|
+
query["where_filter"] = where_filter
|
|
1876
|
+
if where_location:
|
|
1877
|
+
query["where_location"] = where_location
|
|
1878
|
+
if limit:
|
|
1879
|
+
query["limit"] = limit
|
|
1880
|
+
if page_size:
|
|
1881
|
+
query["page_size"] = page_size
|
|
1882
|
+
if attributes_as_keys:
|
|
1883
|
+
query["attrsAsKeys"] = attributes_as_keys
|
|
1884
|
+
if next_page_cookie:
|
|
1885
|
+
query["next_page_cookie"] = next_page_cookie
|
|
1886
|
+
|
|
1887
|
+
encoded_query = urllib.parse.urlencode(query, doseq=True)
|
|
1888
|
+
|
|
1889
|
+
request_url = self.groups_url()
|
|
1890
|
+
if query:
|
|
1891
|
+
request_url += "?{}".format(encoded_query)
|
|
1892
|
+
|
|
1893
|
+
if partition:
|
|
1894
|
+
self.logger.debug(
|
|
1895
|
+
"Get all groups in partition -> '%s' (limit -> %s, page size -> %s); calling -> %s",
|
|
1896
|
+
partition,
|
|
1897
|
+
str(limit),
|
|
1898
|
+
str(page_size),
|
|
1899
|
+
request_url,
|
|
1900
|
+
)
|
|
1901
|
+
failure_message = "Failed to get all groups in partition -> '{}'".format(
|
|
1902
|
+
partition,
|
|
1903
|
+
)
|
|
1904
|
+
else:
|
|
1905
|
+
self.logger.debug(
|
|
1906
|
+
"Get all groups (limit -> %s); calling -> %s",
|
|
1907
|
+
limit,
|
|
1908
|
+
request_url,
|
|
1909
|
+
)
|
|
1910
|
+
failure_message = "Failed to get all groups"
|
|
1911
|
+
|
|
1912
|
+
return self.do_request(
|
|
1913
|
+
url=request_url,
|
|
1914
|
+
method="GET",
|
|
1915
|
+
timeout=None,
|
|
1916
|
+
failure_message=failure_message,
|
|
1917
|
+
)
|
|
1918
|
+
|
|
1919
|
+
# end method definition
|
|
1920
|
+
|
|
1921
|
+
def get_groups_iterator(
|
|
1922
|
+
self,
|
|
1923
|
+
partition: str = "",
|
|
1924
|
+
where_filter: str | None = None,
|
|
1925
|
+
where_location: str | None = None,
|
|
1926
|
+
page_size: int | None = None,
|
|
1927
|
+
) -> iter:
|
|
1928
|
+
"""Get an iterator object that can be used to traverse all groups for a given users partition.
|
|
1929
|
+
|
|
1930
|
+
Returning a generator avoids loading a large number of nodes into memory at once. Instead you
|
|
1931
|
+
can iterate over the potential large list of related workspaces.
|
|
1932
|
+
|
|
1933
|
+
Example usage:
|
|
1934
|
+
groups = otds_object.get_groups_iterator(partition="Content Server Members", page_size=10)
|
|
1935
|
+
for group in groups:
|
|
1936
|
+
logger.info("Traversing group -> %s", group["name"])
|
|
1937
|
+
|
|
1938
|
+
Args:
|
|
1939
|
+
partition (str, optional):
|
|
1940
|
+
The name of the partition.
|
|
1941
|
+
where_filter (str | None, optional):
|
|
1942
|
+
Filter returned groups. This is a string filter.
|
|
1943
|
+
If None, no filtering applies.
|
|
1944
|
+
where_location (str | None, optional):
|
|
1945
|
+
Filter based on the DN of the Organizational Unit.
|
|
1946
|
+
page_size (int, optional):
|
|
1947
|
+
The chunk size for the number of groups returned by one
|
|
1948
|
+
REST API call. If None, then a default of 250 is used.
|
|
1949
|
+
next_page_cookie (str, optional):
|
|
1950
|
+
A key returned by a former call to this method in with
|
|
1951
|
+
a return key 'nextPageCookie' (see example below). This
|
|
1952
|
+
can be used to get the next page of result items.
|
|
1953
|
+
|
|
1954
|
+
Returns:
|
|
1955
|
+
iter:
|
|
1956
|
+
A generator yielding one OTDS group per iteration.
|
|
1957
|
+
If the REST API fails, returns no value.
|
|
1958
|
+
|
|
1959
|
+
"""
|
|
1960
|
+
|
|
1961
|
+
next_page_cookie = None
|
|
1962
|
+
|
|
1963
|
+
while True:
|
|
1964
|
+
response = self.get_groups(
|
|
1965
|
+
partition=partition,
|
|
1966
|
+
where_filter=where_filter,
|
|
1967
|
+
where_location=where_location,
|
|
1968
|
+
page_size=page_size,
|
|
1969
|
+
next_page_cookie=next_page_cookie,
|
|
1970
|
+
)
|
|
1971
|
+
if not response or "groups" not in response:
|
|
1972
|
+
# Don't return None! Plain return is what we need for iterators.
|
|
1973
|
+
# Natural Termination: If the generator does not yield, it behaves
|
|
1974
|
+
# like an empty iterable when used in a loop or converted to a list:
|
|
1975
|
+
return
|
|
1976
|
+
|
|
1977
|
+
# Yield users one at a time:
|
|
1978
|
+
yield from response["groups"]
|
|
1979
|
+
|
|
1980
|
+
# See if we have an additional result page.
|
|
1981
|
+
# If not terminate the iterator and return
|
|
1982
|
+
# no value.
|
|
1983
|
+
next_page_cookie = response["nextPageCookie"]
|
|
1984
|
+
if not next_page_cookie:
|
|
1985
|
+
# Don't return None! Plain return is what we need for iterators.
|
|
1986
|
+
# Natural Termination: If the generator does not yield, it behaves
|
|
1987
|
+
# like an empty iterable when used in a loop or converted to a list:
|
|
1988
|
+
return
|
|
1989
|
+
|
|
1990
|
+
# end method definition
|
|
1991
|
+
|
|
1041
1992
|
def add_user_to_group(self, user: str, group: str) -> bool:
|
|
1042
|
-
"""Add an existing user to an existing group in OTDS
|
|
1993
|
+
"""Add an existing user to an existing group in OTDS.
|
|
1043
1994
|
|
|
1044
1995
|
Args:
|
|
1045
|
-
user (str):
|
|
1046
|
-
|
|
1996
|
+
user (str):
|
|
1997
|
+
The name of the OTDS user (needs to exist).
|
|
1998
|
+
group (str):
|
|
1999
|
+
The name of the OTDS group (needs to exist).
|
|
2000
|
+
|
|
1047
2001
|
Returns:
|
|
1048
|
-
bool:
|
|
2002
|
+
bool:
|
|
2003
|
+
True, if the request is successful, False otherwise.
|
|
2004
|
+
|
|
1049
2005
|
"""
|
|
1050
2006
|
|
|
1051
2007
|
user_to_group_post_body_json = {"stringList": [group]}
|
|
1052
2008
|
|
|
1053
2009
|
request_url = self.users_url() + "/" + user + "/memberof"
|
|
1054
2010
|
|
|
1055
|
-
logger.debug(
|
|
2011
|
+
self.logger.debug(
|
|
1056
2012
|
"Adding user -> '%s' to group -> '%s'; calling -> %s",
|
|
1057
2013
|
user,
|
|
1058
2014
|
group,
|
|
@@ -1066,33 +2022,36 @@ class OTDS:
|
|
|
1066
2022
|
json_data=user_to_group_post_body_json,
|
|
1067
2023
|
timeout=None,
|
|
1068
2024
|
failure_message="Failed to add user -> '{}' to group -> '{}'".format(
|
|
1069
|
-
user,
|
|
2025
|
+
user,
|
|
2026
|
+
group,
|
|
1070
2027
|
),
|
|
1071
2028
|
parse_request_response=False,
|
|
1072
2029
|
)
|
|
1073
2030
|
|
|
1074
|
-
|
|
1075
|
-
return True
|
|
1076
|
-
|
|
1077
|
-
return False
|
|
2031
|
+
return bool(response and response.ok)
|
|
1078
2032
|
|
|
1079
2033
|
# end method definition
|
|
1080
2034
|
|
|
1081
2035
|
def add_group_to_parent_group(self, group: str, parent_group: str) -> bool:
|
|
1082
|
-
"""Add an existing group to an existing parent group in OTDS
|
|
2036
|
+
"""Add an existing group to an existing parent group in OTDS.
|
|
1083
2037
|
|
|
1084
2038
|
Args:
|
|
1085
|
-
group (str):
|
|
1086
|
-
|
|
2039
|
+
group (str):
|
|
2040
|
+
The name of the OTDS group (needs to exist).
|
|
2041
|
+
parent_group (str):
|
|
2042
|
+
The name of the OTDS parent group (needs to exist).
|
|
2043
|
+
|
|
1087
2044
|
Returns:
|
|
1088
|
-
bool:
|
|
2045
|
+
bool:
|
|
2046
|
+
True, if the request is successful, False otherwise.
|
|
2047
|
+
|
|
1089
2048
|
"""
|
|
1090
2049
|
|
|
1091
2050
|
group_to_parent_group_post_body_json = {"stringList": [parent_group]}
|
|
1092
2051
|
|
|
1093
2052
|
request_url = self.groups_url() + "/" + group + "/memberof"
|
|
1094
2053
|
|
|
1095
|
-
logger.debug(
|
|
2054
|
+
self.logger.debug(
|
|
1096
2055
|
"Adding group -> '%s' to parent group -> '%s'; calling -> %s",
|
|
1097
2056
|
group,
|
|
1098
2057
|
parent_group,
|
|
@@ -1106,15 +2065,13 @@ class OTDS:
|
|
|
1106
2065
|
json_data=group_to_parent_group_post_body_json,
|
|
1107
2066
|
timeout=None,
|
|
1108
2067
|
failure_message="Failed to add group -> '{}' to parent group -> '{}'".format(
|
|
1109
|
-
group,
|
|
2068
|
+
group,
|
|
2069
|
+
parent_group,
|
|
1110
2070
|
),
|
|
1111
2071
|
parse_request_response=False,
|
|
1112
2072
|
)
|
|
1113
2073
|
|
|
1114
|
-
|
|
1115
|
-
return True
|
|
1116
|
-
|
|
1117
|
-
return False
|
|
2074
|
+
return bool(response and response.ok)
|
|
1118
2075
|
|
|
1119
2076
|
# end method definition
|
|
1120
2077
|
|
|
@@ -1128,15 +2085,29 @@ class OTDS:
|
|
|
1128
2085
|
secret: str | None = None, # needs to be 16 bytes!
|
|
1129
2086
|
additional_payload: dict | None = None,
|
|
1130
2087
|
) -> dict | None:
|
|
1131
|
-
"""Add an OTDS resource
|
|
2088
|
+
"""Add an OTDS resource.
|
|
1132
2089
|
|
|
1133
2090
|
Args:
|
|
1134
|
-
name (str):
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
2091
|
+
name (str):
|
|
2092
|
+
The name of the new OTDS resource.
|
|
2093
|
+
description (str):
|
|
2094
|
+
The optional description of the new OTDS resource.
|
|
2095
|
+
display_name (str, optional):
|
|
2096
|
+
The optional display name of the OTDS resource.
|
|
2097
|
+
allow_impersonation (bool):
|
|
2098
|
+
Defines whether or not the resource allows impersonation.
|
|
2099
|
+
resource_id (str | None, optional):
|
|
2100
|
+
Allows to set a predefined resource ID. This requires the
|
|
2101
|
+
secret parameter in additon.
|
|
2102
|
+
secret (str):
|
|
2103
|
+
A 24 charcters secret key. Required to set a predefined resource ID.
|
|
2104
|
+
additional_payload (dict, optional):
|
|
2105
|
+
Additional values for the JSON payload.
|
|
2106
|
+
|
|
1138
2107
|
Returns:
|
|
1139
|
-
dict
|
|
2108
|
+
dict | None:
|
|
2109
|
+
Request response (dictionary) or None if the REST call fails.
|
|
2110
|
+
|
|
1140
2111
|
"""
|
|
1141
2112
|
|
|
1142
2113
|
resource_post_body = {
|
|
@@ -1147,8 +2118,8 @@ class OTDS:
|
|
|
1147
2118
|
}
|
|
1148
2119
|
|
|
1149
2120
|
if resource_id and not secret:
|
|
1150
|
-
logger.error(
|
|
1151
|
-
"A resource ID can only be specified if a secret value is also provided!"
|
|
2121
|
+
self.logger.error(
|
|
2122
|
+
"A resource ID can only be specified if a secret value is also provided!",
|
|
1152
2123
|
)
|
|
1153
2124
|
return None
|
|
1154
2125
|
|
|
@@ -1156,8 +2127,8 @@ class OTDS:
|
|
|
1156
2127
|
resource_post_body["resourceID"] = resource_id
|
|
1157
2128
|
if secret:
|
|
1158
2129
|
if len(secret) != 24 or not secret.endswith("=="):
|
|
1159
|
-
logger.warning(
|
|
1160
|
-
"The secret should by 24 characters long and should end with '=='"
|
|
2130
|
+
self.logger.warning(
|
|
2131
|
+
"The secret should by 24 characters long and should end with '=='",
|
|
1161
2132
|
)
|
|
1162
2133
|
resource_post_body["secretKey"] = secret
|
|
1163
2134
|
|
|
@@ -1168,7 +2139,7 @@ class OTDS:
|
|
|
1168
2139
|
|
|
1169
2140
|
request_url = self.config()["resourceUrl"]
|
|
1170
2141
|
|
|
1171
|
-
logger.debug(
|
|
2142
|
+
self.logger.debug(
|
|
1172
2143
|
"Adding resource -> '%s' ('%s'); calling -> %s",
|
|
1173
2144
|
name,
|
|
1174
2145
|
description,
|
|
@@ -1186,15 +2157,19 @@ class OTDS:
|
|
|
1186
2157
|
# end method definition
|
|
1187
2158
|
|
|
1188
2159
|
def get_resource(self, name: str, show_error: bool = False) -> dict | None:
|
|
1189
|
-
"""Get an existing OTDS resource
|
|
2160
|
+
"""Get an existing OTDS resource.
|
|
1190
2161
|
|
|
1191
2162
|
Args:
|
|
1192
|
-
name (str):
|
|
1193
|
-
|
|
2163
|
+
name (str):
|
|
2164
|
+
The name of the new OTDS resource.
|
|
2165
|
+
show_error (bool, optional):
|
|
2166
|
+
If True, log an error if resource is not found. Else log just a warning.
|
|
2167
|
+
|
|
1194
2168
|
Returns:
|
|
1195
|
-
dict
|
|
2169
|
+
dict | None:
|
|
2170
|
+
Request response or None if the REST call fails.
|
|
1196
2171
|
|
|
1197
|
-
|
|
2172
|
+
Example:
|
|
1198
2173
|
{
|
|
1199
2174
|
'resourceName': 'cs',
|
|
1200
2175
|
'id': 'cs',
|
|
@@ -1223,11 +2198,12 @@ class OTDS:
|
|
|
1223
2198
|
'logonStyle': None,
|
|
1224
2199
|
'logonUXVersion': 0
|
|
1225
2200
|
}
|
|
2201
|
+
|
|
1226
2202
|
"""
|
|
1227
2203
|
|
|
1228
2204
|
request_url = "{}/{}".format(self.config()["resourceUrl"], name)
|
|
1229
2205
|
|
|
1230
|
-
logger.debug("Get resource -> '%s'; calling -> %s", name, request_url)
|
|
2206
|
+
self.logger.debug("Get resource -> '%s'; calling -> %s", name, request_url)
|
|
1231
2207
|
|
|
1232
2208
|
return self.do_request(
|
|
1233
2209
|
url=request_url,
|
|
@@ -1240,21 +2216,30 @@ class OTDS:
|
|
|
1240
2216
|
# end method definition
|
|
1241
2217
|
|
|
1242
2218
|
def update_resource(
|
|
1243
|
-
self,
|
|
2219
|
+
self,
|
|
2220
|
+
name: str,
|
|
2221
|
+
resource: object,
|
|
2222
|
+
show_error: bool = True,
|
|
1244
2223
|
) -> dict | None:
|
|
1245
|
-
"""Update an existing OTDS resource
|
|
2224
|
+
"""Update an existing OTDS resource.
|
|
1246
2225
|
|
|
1247
2226
|
Args:
|
|
1248
|
-
name (str):
|
|
1249
|
-
|
|
1250
|
-
|
|
2227
|
+
name (str):
|
|
2228
|
+
The name of the OTDS resource to update.
|
|
2229
|
+
resource (object):
|
|
2230
|
+
updated resource object of get_resource called before
|
|
2231
|
+
show_error (bool, optional):
|
|
2232
|
+
If True, log an error if resource is not found. Else just log a warning.
|
|
2233
|
+
|
|
1251
2234
|
Returns:
|
|
1252
|
-
dict
|
|
2235
|
+
dict | None:
|
|
2236
|
+
Request response (json) or None if the REST call fails.
|
|
2237
|
+
|
|
1253
2238
|
"""
|
|
1254
2239
|
|
|
1255
2240
|
request_url = "{}/{}".format(self.config()["resourceUrl"], name)
|
|
1256
2241
|
|
|
1257
|
-
logger.debug("Updating resource -> '%s'; calling -> %s", name, request_url)
|
|
2242
|
+
self.logger.debug("Updating resource -> '%s'; calling -> %s", name, request_url)
|
|
1258
2243
|
|
|
1259
2244
|
return self.do_request(
|
|
1260
2245
|
url=request_url,
|
|
@@ -1268,20 +2253,26 @@ class OTDS:
|
|
|
1268
2253
|
# end method definition
|
|
1269
2254
|
|
|
1270
2255
|
def activate_resource(self, resource_id: str) -> dict | None:
|
|
1271
|
-
"""Activate an OTDS resource
|
|
2256
|
+
"""Activate an OTDS resource.
|
|
1272
2257
|
|
|
1273
2258
|
Args:
|
|
1274
|
-
resource_id (str):
|
|
2259
|
+
resource_id (str):
|
|
2260
|
+
The ID of the OTDS resource to update.
|
|
2261
|
+
|
|
1275
2262
|
Returns:
|
|
1276
|
-
dict
|
|
2263
|
+
dict | None:
|
|
2264
|
+
Request response or None if the REST call fails.
|
|
2265
|
+
|
|
1277
2266
|
"""
|
|
1278
2267
|
|
|
1279
2268
|
resource_post_body_json = {}
|
|
1280
2269
|
|
|
1281
2270
|
request_url = "{}/{}/activate".format(self.config()["resourceUrl"], resource_id)
|
|
1282
2271
|
|
|
1283
|
-
logger.debug(
|
|
1284
|
-
"Activating resource -> '%s'; calling -> %s",
|
|
2272
|
+
self.logger.debug(
|
|
2273
|
+
"Activating resource -> '%s'; calling -> %s",
|
|
2274
|
+
resource_id,
|
|
2275
|
+
request_url,
|
|
1285
2276
|
)
|
|
1286
2277
|
|
|
1287
2278
|
return self.do_request(
|
|
@@ -1295,17 +2286,20 @@ class OTDS:
|
|
|
1295
2286
|
# end method definition
|
|
1296
2287
|
|
|
1297
2288
|
def get_access_roles(self) -> dict | None:
|
|
1298
|
-
"""Get a list of all OTDS access roles
|
|
2289
|
+
"""Get a list of all OTDS access roles.
|
|
1299
2290
|
|
|
1300
2291
|
Args:
|
|
1301
2292
|
None
|
|
2293
|
+
|
|
1302
2294
|
Returns:
|
|
1303
|
-
dict
|
|
2295
|
+
dict | None:
|
|
2296
|
+
Request response or None if the REST call fails.
|
|
2297
|
+
|
|
1304
2298
|
"""
|
|
1305
2299
|
|
|
1306
2300
|
request_url = self.config()["accessRoleUrl"]
|
|
1307
2301
|
|
|
1308
|
-
logger.debug("Retrieving access roles; calling -> %s", request_url)
|
|
2302
|
+
self.logger.debug("Retrieving access roles; calling -> %s", request_url)
|
|
1309
2303
|
|
|
1310
2304
|
return self.do_request(
|
|
1311
2305
|
url=request_url,
|
|
@@ -1317,17 +2311,25 @@ class OTDS:
|
|
|
1317
2311
|
# end method definition
|
|
1318
2312
|
|
|
1319
2313
|
def get_access_role(self, access_role: str) -> dict | None:
|
|
1320
|
-
"""Get an OTDS access role
|
|
2314
|
+
"""Get an OTDS access role.
|
|
1321
2315
|
|
|
1322
2316
|
Args:
|
|
1323
|
-
|
|
2317
|
+
access_role (str):
|
|
2318
|
+
The name of the access role.
|
|
2319
|
+
|
|
1324
2320
|
Returns:
|
|
1325
|
-
dict
|
|
2321
|
+
dict | None:
|
|
2322
|
+
Request response or None if the REST call fails.
|
|
2323
|
+
|
|
1326
2324
|
"""
|
|
1327
2325
|
|
|
1328
2326
|
request_url = self.config()["accessRoleUrl"] + "/" + access_role
|
|
1329
2327
|
|
|
1330
|
-
logger.debug(
|
|
2328
|
+
self.logger.debug(
|
|
2329
|
+
"Get access role -> '%s'; calling -> %s",
|
|
2330
|
+
access_role,
|
|
2331
|
+
request_url,
|
|
2332
|
+
)
|
|
1331
2333
|
|
|
1332
2334
|
return self.do_request(
|
|
1333
2335
|
url=request_url,
|
|
@@ -1339,29 +2341,39 @@ class OTDS:
|
|
|
1339
2341
|
# end method definition
|
|
1340
2342
|
|
|
1341
2343
|
def add_partition_to_access_role(
|
|
1342
|
-
self,
|
|
2344
|
+
self,
|
|
2345
|
+
access_role: str,
|
|
2346
|
+
partition: str,
|
|
2347
|
+
location: str = "",
|
|
1343
2348
|
) -> bool:
|
|
1344
|
-
"""Add an OTDS partition to an OTDS access role
|
|
2349
|
+
"""Add an OTDS partition to an OTDS access role.
|
|
1345
2350
|
|
|
1346
2351
|
Args:
|
|
1347
|
-
access_role (str):
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
2352
|
+
access_role (str):
|
|
2353
|
+
The name of the OTDS access role.
|
|
2354
|
+
partition (str):
|
|
2355
|
+
The name of the partition.
|
|
2356
|
+
location (str, optional):
|
|
2357
|
+
This is kind of a unique identifier DN (Distinguished Name)
|
|
2358
|
+
most of the times you will want to keep it to empty string ("")
|
|
2359
|
+
|
|
1351
2360
|
Returns:
|
|
1352
|
-
bool:
|
|
1353
|
-
|
|
2361
|
+
bool:
|
|
2362
|
+
True if partition is in access role or has been successfully added.
|
|
2363
|
+
False if partition has been not been added (error)
|
|
2364
|
+
|
|
1354
2365
|
"""
|
|
1355
2366
|
|
|
1356
2367
|
access_role_post_body_json = {
|
|
1357
|
-
"userPartitions": [{"name": partition, "location": location}]
|
|
2368
|
+
"userPartitions": [{"name": partition, "location": location}],
|
|
1358
2369
|
}
|
|
1359
2370
|
|
|
1360
2371
|
request_url = "{}/{}/members".format(
|
|
1361
|
-
self.config()["accessRoleUrl"],
|
|
2372
|
+
self.config()["accessRoleUrl"],
|
|
2373
|
+
access_role,
|
|
1362
2374
|
)
|
|
1363
2375
|
|
|
1364
|
-
logger.debug(
|
|
2376
|
+
self.logger.debug(
|
|
1365
2377
|
"Add user partition -> '%s' to access role -> '%s'; calling -> %s",
|
|
1366
2378
|
partition,
|
|
1367
2379
|
access_role,
|
|
@@ -1374,31 +2386,38 @@ class OTDS:
|
|
|
1374
2386
|
json_data=access_role_post_body_json,
|
|
1375
2387
|
timeout=None,
|
|
1376
2388
|
failure_message="Failed to add partition -> '{}' to access role -> '{}'".format(
|
|
1377
|
-
partition,
|
|
2389
|
+
partition,
|
|
2390
|
+
access_role,
|
|
1378
2391
|
),
|
|
1379
2392
|
parse_request_response=False,
|
|
1380
2393
|
)
|
|
1381
2394
|
|
|
1382
|
-
|
|
1383
|
-
return True
|
|
1384
|
-
|
|
1385
|
-
return False
|
|
2395
|
+
return bool(response and response.ok)
|
|
1386
2396
|
|
|
1387
2397
|
# end method definition
|
|
1388
2398
|
|
|
1389
2399
|
def add_user_to_access_role(
|
|
1390
|
-
self,
|
|
2400
|
+
self,
|
|
2401
|
+
access_role: str,
|
|
2402
|
+
user_id: str,
|
|
2403
|
+
location: str = "",
|
|
1391
2404
|
) -> bool:
|
|
1392
|
-
"""Add an OTDS user to an OTDS access role
|
|
2405
|
+
"""Add an OTDS user to an OTDS access role.
|
|
1393
2406
|
|
|
1394
2407
|
Args:
|
|
1395
|
-
access_role (str):
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
2408
|
+
access_role (str):
|
|
2409
|
+
The name of the OTDS access role.
|
|
2410
|
+
user_id (str):
|
|
2411
|
+
The ID of the user (= login name) to add to the access role.
|
|
2412
|
+
location (str, optional):
|
|
2413
|
+
This is kind of a unique identifier DN (Distinguished Name)
|
|
2414
|
+
most of the times you will want to keep it to empty string ("").
|
|
2415
|
+
|
|
1399
2416
|
Returns:
|
|
1400
|
-
bool:
|
|
1401
|
-
|
|
2417
|
+
bool:
|
|
2418
|
+
True if user is in access role or has been successfully added.
|
|
2419
|
+
False if user has not been added (error).
|
|
2420
|
+
|
|
1402
2421
|
"""
|
|
1403
2422
|
|
|
1404
2423
|
# get existing members to check if user is already a member:
|
|
@@ -1407,17 +2426,17 @@ class OTDS:
|
|
|
1407
2426
|
return False
|
|
1408
2427
|
|
|
1409
2428
|
# Checking if user already added to access role
|
|
1410
|
-
|
|
1411
|
-
for user in
|
|
2429
|
+
access_role_users = access_roles_get_response["accessRoleMembers"]["users"]
|
|
2430
|
+
for user in access_role_users:
|
|
1412
2431
|
if user["displayName"] == user_id:
|
|
1413
|
-
logger.debug(
|
|
2432
|
+
self.logger.debug(
|
|
1414
2433
|
"User -> '%s' already added to access role -> '%s'",
|
|
1415
2434
|
user_id,
|
|
1416
2435
|
access_role,
|
|
1417
2436
|
)
|
|
1418
2437
|
return True
|
|
1419
2438
|
|
|
1420
|
-
logger.debug(
|
|
2439
|
+
self.logger.debug(
|
|
1421
2440
|
"User -> '%s' is not yet in access role -> '%s' - adding...",
|
|
1422
2441
|
user_id,
|
|
1423
2442
|
access_role,
|
|
@@ -1425,14 +2444,15 @@ class OTDS:
|
|
|
1425
2444
|
|
|
1426
2445
|
# create payload for REST call:
|
|
1427
2446
|
access_role_post_body_json = {
|
|
1428
|
-
"users": [{"name": user_id, "location": location}]
|
|
2447
|
+
"users": [{"name": user_id, "location": location}],
|
|
1429
2448
|
}
|
|
1430
2449
|
|
|
1431
2450
|
request_url = "{}/{}/members".format(
|
|
1432
|
-
self.config()["accessRoleUrl"],
|
|
2451
|
+
self.config()["accessRoleUrl"],
|
|
2452
|
+
access_role,
|
|
1433
2453
|
)
|
|
1434
2454
|
|
|
1435
|
-
logger.debug(
|
|
2455
|
+
self.logger.debug(
|
|
1436
2456
|
"Add user -> %s to access role -> %s; calling -> %s",
|
|
1437
2457
|
user_id,
|
|
1438
2458
|
access_role,
|
|
@@ -1445,31 +2465,38 @@ class OTDS:
|
|
|
1445
2465
|
json_data=access_role_post_body_json,
|
|
1446
2466
|
timeout=None,
|
|
1447
2467
|
failure_message="Failed to add user -> '{}' to access role -> '{}'".format(
|
|
1448
|
-
user_id,
|
|
2468
|
+
user_id,
|
|
2469
|
+
access_role,
|
|
1449
2470
|
),
|
|
1450
2471
|
parse_request_response=False,
|
|
1451
2472
|
)
|
|
1452
2473
|
|
|
1453
|
-
|
|
1454
|
-
return True
|
|
1455
|
-
|
|
1456
|
-
return False
|
|
2474
|
+
return bool(response and response.ok)
|
|
1457
2475
|
|
|
1458
2476
|
# end method definition
|
|
1459
2477
|
|
|
1460
2478
|
def add_group_to_access_role(
|
|
1461
|
-
self,
|
|
2479
|
+
self,
|
|
2480
|
+
access_role: str,
|
|
2481
|
+
group: str,
|
|
2482
|
+
location: str = "",
|
|
1462
2483
|
) -> bool:
|
|
1463
|
-
"""Add an OTDS group to an OTDS access role
|
|
2484
|
+
"""Add an OTDS group to an OTDS access role.
|
|
1464
2485
|
|
|
1465
2486
|
Args:
|
|
1466
|
-
access_role (str):
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
2487
|
+
access_role (str):
|
|
2488
|
+
The name of the OTDS access role.
|
|
2489
|
+
group (str):
|
|
2490
|
+
The name of the group to add to the access role.
|
|
2491
|
+
location (str, optional):
|
|
2492
|
+
This is kind of a unique identifier DN (Distinguished Name)
|
|
2493
|
+
most of the times you will want to keep it to empty string ("").
|
|
2494
|
+
|
|
1470
2495
|
Returns:
|
|
1471
|
-
bool:
|
|
1472
|
-
|
|
2496
|
+
bool:
|
|
2497
|
+
True if group is in access role or has been successfully added.
|
|
2498
|
+
False if group has been not been added (error)
|
|
2499
|
+
|
|
1473
2500
|
"""
|
|
1474
2501
|
|
|
1475
2502
|
# get existing members to check if user is already a member:
|
|
@@ -1481,14 +2508,14 @@ class OTDS:
|
|
|
1481
2508
|
access_role_groups = access_roles_get_response["accessRoleMembers"]["groups"]
|
|
1482
2509
|
for access_role_group in access_role_groups:
|
|
1483
2510
|
if access_role_group["name"] == group:
|
|
1484
|
-
logger.debug(
|
|
2511
|
+
self.logger.debug(
|
|
1485
2512
|
"Group -> '%s' already added to access role -> '%s'",
|
|
1486
2513
|
group,
|
|
1487
2514
|
access_role,
|
|
1488
2515
|
)
|
|
1489
2516
|
return True
|
|
1490
2517
|
|
|
1491
|
-
logger.debug(
|
|
2518
|
+
self.logger.debug(
|
|
1492
2519
|
"Group -> '%s' is not yet in access role -> '%s' - adding...",
|
|
1493
2520
|
group,
|
|
1494
2521
|
access_role,
|
|
@@ -1498,10 +2525,11 @@ class OTDS:
|
|
|
1498
2525
|
access_role_post_body_json = {"groups": [{"name": group, "location": location}]}
|
|
1499
2526
|
|
|
1500
2527
|
request_url = "{}/{}/members".format(
|
|
1501
|
-
self.config()["accessRoleUrl"],
|
|
2528
|
+
self.config()["accessRoleUrl"],
|
|
2529
|
+
access_role,
|
|
1502
2530
|
)
|
|
1503
2531
|
|
|
1504
|
-
logger.debug(
|
|
2532
|
+
self.logger.debug(
|
|
1505
2533
|
"Add group -> '%s' to access role -> '%s'; calling -> %s",
|
|
1506
2534
|
group,
|
|
1507
2535
|
access_role,
|
|
@@ -1514,30 +2542,41 @@ class OTDS:
|
|
|
1514
2542
|
json_data=access_role_post_body_json,
|
|
1515
2543
|
timeout=None,
|
|
1516
2544
|
failure_message="Failed to add group -> '{}' to access role -> '{}'".format(
|
|
1517
|
-
group,
|
|
2545
|
+
group,
|
|
2546
|
+
access_role,
|
|
1518
2547
|
),
|
|
1519
2548
|
parse_request_response=False,
|
|
1520
2549
|
)
|
|
1521
2550
|
|
|
1522
|
-
|
|
1523
|
-
return True
|
|
1524
|
-
|
|
1525
|
-
return False
|
|
2551
|
+
return bool(response and response.ok)
|
|
1526
2552
|
|
|
1527
2553
|
# end method definition
|
|
1528
2554
|
|
|
1529
2555
|
def update_access_role_attributes(
|
|
1530
|
-
self,
|
|
2556
|
+
self,
|
|
2557
|
+
name: str,
|
|
2558
|
+
attribute_list: list,
|
|
1531
2559
|
) -> dict | None:
|
|
1532
|
-
"""Update some attributes of an existing OTDS
|
|
2560
|
+
"""Update some attributes of an existing OTDS access role.
|
|
1533
2561
|
|
|
1534
2562
|
Args:
|
|
1535
|
-
name (str):
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
2563
|
+
name (str):
|
|
2564
|
+
The name of the existing access role.
|
|
2565
|
+
attribute_list (list):
|
|
2566
|
+
A list of attribute name and attribute value pairs.
|
|
2567
|
+
The values need to be a list as well.
|
|
2568
|
+
Example values:
|
|
2569
|
+
[
|
|
2570
|
+
{
|
|
2571
|
+
name: "pushAllGroups",
|
|
2572
|
+
values: ["True"]
|
|
2573
|
+
}
|
|
2574
|
+
]
|
|
2575
|
+
|
|
1539
2576
|
Returns:
|
|
1540
|
-
dict
|
|
2577
|
+
dict | None:
|
|
2578
|
+
Request response or None if the REST call fails.
|
|
2579
|
+
|
|
1541
2580
|
"""
|
|
1542
2581
|
|
|
1543
2582
|
# Return if list is empty:
|
|
@@ -1547,14 +2586,14 @@ class OTDS:
|
|
|
1547
2586
|
# create payload for REST call:
|
|
1548
2587
|
access_role = self.get_access_role(name)
|
|
1549
2588
|
if not access_role:
|
|
1550
|
-
logger.error("Failed to get access role -> '%s'", name)
|
|
2589
|
+
self.logger.error("Failed to get access role -> '%s'", name)
|
|
1551
2590
|
return None
|
|
1552
2591
|
|
|
1553
2592
|
access_role_put_body_json = {"attributes": attribute_list}
|
|
1554
2593
|
|
|
1555
2594
|
request_url = "{}/{}/attributes".format(self.config()["accessRoleUrl"], name)
|
|
1556
2595
|
|
|
1557
|
-
logger.debug(
|
|
2596
|
+
self.logger.debug(
|
|
1558
2597
|
"Update access role -> '%s' with attributes -> %s; calling -> %s",
|
|
1559
2598
|
name,
|
|
1560
2599
|
str(access_role_put_body_json),
|
|
@@ -1582,24 +2621,31 @@ class OTDS:
|
|
|
1582
2621
|
"""Add a product license to an OTDS resource.
|
|
1583
2622
|
|
|
1584
2623
|
Args:
|
|
1585
|
-
path_to_license_file (str):
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
2624
|
+
path_to_license_file (str):
|
|
2625
|
+
A fully qualified filename of the license file.
|
|
2626
|
+
product_name (str):
|
|
2627
|
+
The product name.
|
|
2628
|
+
product_description (str):
|
|
2629
|
+
The product description.
|
|
2630
|
+
resource_id (str):
|
|
2631
|
+
OTDS resource ID (this is ID not the resource name!).
|
|
2632
|
+
update (bool, optional):
|
|
2633
|
+
Whether or not an existing license should be updated (default = True).
|
|
2634
|
+
|
|
1590
2635
|
Returns:
|
|
1591
|
-
dict
|
|
2636
|
+
dict | None:
|
|
2637
|
+
Request response (dictionary) or None if the REST call fails.
|
|
2638
|
+
|
|
1592
2639
|
"""
|
|
1593
2640
|
|
|
1594
|
-
logger.debug("Reading license file -> '%s'...", path_to_license_file)
|
|
2641
|
+
self.logger.debug("Reading license file -> '%s'...", path_to_license_file)
|
|
1595
2642
|
try:
|
|
1596
|
-
with open(path_to_license_file,
|
|
2643
|
+
with open(path_to_license_file, encoding="UTF-8") as license_file:
|
|
1597
2644
|
license_content = license_file.read()
|
|
1598
|
-
except
|
|
1599
|
-
logger.error(
|
|
1600
|
-
"Error opening license file -> '%s'
|
|
2645
|
+
except OSError:
|
|
2646
|
+
self.logger.error(
|
|
2647
|
+
"Error opening license file -> '%s'!",
|
|
1601
2648
|
path_to_license_file,
|
|
1602
|
-
exception.strerror,
|
|
1603
2649
|
)
|
|
1604
2650
|
return None
|
|
1605
2651
|
|
|
@@ -1620,17 +2666,19 @@ class OTDS:
|
|
|
1620
2666
|
if existing_license:
|
|
1621
2667
|
request_url += "/" + existing_license[0]["id"]
|
|
1622
2668
|
else:
|
|
1623
|
-
logger.
|
|
1624
|
-
"No existing license found for resource -> '%s' - adding a new license...",
|
|
2669
|
+
self.logger.info(
|
|
2670
|
+
"No existing license found for product -> '%s' and resource -> '%s' - adding a new license...",
|
|
2671
|
+
product_name,
|
|
1625
2672
|
resource_id,
|
|
1626
2673
|
)
|
|
1627
2674
|
# change strategy to create a new license:
|
|
1628
2675
|
update = False
|
|
1629
2676
|
|
|
1630
|
-
logger.debug(
|
|
1631
|
-
"
|
|
2677
|
+
self.logger.debug(
|
|
2678
|
+
"%s product license -> '%s' for product -> '%s' to resource ->'%s'; calling -> %s",
|
|
2679
|
+
"Adding" if not update else "Updating",
|
|
1632
2680
|
path_to_license_file,
|
|
1633
|
-
product_description,
|
|
2681
|
+
"{} ({})".format(product_name, product_description) if product_description else product_name,
|
|
1634
2682
|
resource_id,
|
|
1635
2683
|
request_url,
|
|
1636
2684
|
)
|
|
@@ -1642,8 +2690,10 @@ class OTDS:
|
|
|
1642
2690
|
method="PUT",
|
|
1643
2691
|
json_data=license_post_body_json,
|
|
1644
2692
|
timeout=None,
|
|
1645
|
-
failure_message="Failed to update product license -> '{}' for product -> '{}'".format(
|
|
1646
|
-
path_to_license_file,
|
|
2693
|
+
failure_message="Failed to update product license -> '{}' for product -> '{}' to resource -> '{}'".format(
|
|
2694
|
+
path_to_license_file,
|
|
2695
|
+
"{} ({})".format(product_name, product_description) if product_description else product_name,
|
|
2696
|
+
resource_id,
|
|
1647
2697
|
),
|
|
1648
2698
|
)
|
|
1649
2699
|
else:
|
|
@@ -1653,44 +2703,47 @@ class OTDS:
|
|
|
1653
2703
|
method="POST",
|
|
1654
2704
|
json_data=license_post_body_json,
|
|
1655
2705
|
timeout=None,
|
|
1656
|
-
failure_message="Failed to add product license -> '{}' for product -> '{}'".format(
|
|
1657
|
-
path_to_license_file,
|
|
2706
|
+
failure_message="Failed to add product license -> '{}' for product -> '{}' to resource -> '{}'".format(
|
|
2707
|
+
path_to_license_file,
|
|
2708
|
+
"{} ({})".format(product_name, product_description) if product_description else product_name,
|
|
2709
|
+
resource_id,
|
|
1658
2710
|
),
|
|
1659
2711
|
)
|
|
1660
2712
|
|
|
1661
2713
|
# end method definition
|
|
1662
2714
|
|
|
1663
|
-
def get_license_for_resource(self, resource_id: str):
|
|
2715
|
+
def get_license_for_resource(self, resource_id: str) -> dict | None:
|
|
1664
2716
|
"""Get a product license for a resource in OTDS.
|
|
1665
2717
|
|
|
1666
2718
|
Args:
|
|
1667
|
-
resource_id (str):
|
|
2719
|
+
resource_id (str):
|
|
2720
|
+
The OTDS resource ID (this is ID not the resource name!).
|
|
2721
|
+
|
|
1668
2722
|
Returns:
|
|
1669
|
-
|
|
2723
|
+
dict | None:
|
|
2724
|
+
Licenses for a resource or None if the REST call fails.
|
|
2725
|
+
|
|
2726
|
+
Example:
|
|
2727
|
+
{
|
|
2728
|
+
'_oTLicenseType': 'NON-PRODUCTION',
|
|
2729
|
+
'_oTLicenseResource': '7382094f-a434-4714-9696-82864b6803da',
|
|
2730
|
+
'_oTLicenseResourceName': 'cs',
|
|
2731
|
+
'_oTLicenseProduct': 'EXTENDED_ECM',
|
|
2732
|
+
'name': 'EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da',
|
|
2733
|
+
'location': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
|
|
2734
|
+
'id': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
|
|
2735
|
+
'description': 'CS license',
|
|
2736
|
+
'values': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, ...]
|
|
2737
|
+
}
|
|
1670
2738
|
|
|
1671
|
-
licenses have this format:
|
|
1672
|
-
{
|
|
1673
|
-
'_oTLicenseType': 'NON-PRODUCTION',
|
|
1674
|
-
'_oTLicenseResource': '7382094f-a434-4714-9696-82864b6803da',
|
|
1675
|
-
'_oTLicenseResourceName': 'cs',
|
|
1676
|
-
'_oTLicenseProduct': 'EXTENDED_ECM',
|
|
1677
|
-
'name': 'EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da',
|
|
1678
|
-
'location': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
|
|
1679
|
-
'id': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
|
|
1680
|
-
'description': 'CS license',
|
|
1681
|
-
'values': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, ...]
|
|
1682
|
-
}
|
|
1683
2739
|
"""
|
|
1684
2740
|
|
|
1685
|
-
request_url = (
|
|
1686
|
-
self.license_url()
|
|
1687
|
-
+ "/assignedlicenses?resourceID="
|
|
1688
|
-
+ resource_id
|
|
1689
|
-
+ "&validOnly=false"
|
|
1690
|
-
)
|
|
2741
|
+
request_url = self.license_url() + "/assignedlicenses?resourceID=" + resource_id + "&validOnly=false"
|
|
1691
2742
|
|
|
1692
|
-
logger.debug(
|
|
1693
|
-
"Get license for resource -> %s; calling -> %s",
|
|
2743
|
+
self.logger.debug(
|
|
2744
|
+
"Get license for resource -> %s; calling -> %s",
|
|
2745
|
+
resource_id,
|
|
2746
|
+
request_url,
|
|
1694
2747
|
)
|
|
1695
2748
|
|
|
1696
2749
|
response = self.do_request(
|
|
@@ -1698,7 +2751,7 @@ class OTDS:
|
|
|
1698
2751
|
method="GET",
|
|
1699
2752
|
timeout=None,
|
|
1700
2753
|
failure_message="Failed to get license for resource -> '{}'".format(
|
|
1701
|
-
resource_id
|
|
2754
|
+
resource_id,
|
|
1702
2755
|
),
|
|
1703
2756
|
)
|
|
1704
2757
|
|
|
@@ -1713,15 +2766,20 @@ class OTDS:
|
|
|
1713
2766
|
"""Delete a product license for a resource in OTDS.
|
|
1714
2767
|
|
|
1715
2768
|
Args:
|
|
1716
|
-
resource_id (str):
|
|
1717
|
-
|
|
2769
|
+
resource_id (str):
|
|
2770
|
+
The OTDS resource ID (this is ID not the resource name!).
|
|
2771
|
+
license_id (str):
|
|
2772
|
+
The OTDS license ID (this is the ID not the license name!).
|
|
2773
|
+
|
|
1718
2774
|
Returns:
|
|
1719
|
-
bool:
|
|
2775
|
+
bool:
|
|
2776
|
+
True if successful or False if the REST call fails
|
|
2777
|
+
|
|
1720
2778
|
"""
|
|
1721
2779
|
|
|
1722
2780
|
request_url = "{}/{}".format(self.license_url(), license_id)
|
|
1723
2781
|
|
|
1724
|
-
logger.debug(
|
|
2782
|
+
self.logger.debug(
|
|
1725
2783
|
"Deleting product license -> '%s' from resource -> '%s'; calling -> %s",
|
|
1726
2784
|
license_id,
|
|
1727
2785
|
resource_id,
|
|
@@ -1733,15 +2791,13 @@ class OTDS:
|
|
|
1733
2791
|
method="DELETE",
|
|
1734
2792
|
timeout=None,
|
|
1735
2793
|
failure_message="Failed to delete license -> '{}' for resource -> '{}'".format(
|
|
1736
|
-
license_id,
|
|
2794
|
+
license_id,
|
|
2795
|
+
resource_id,
|
|
1737
2796
|
),
|
|
1738
2797
|
parse_request_response=False,
|
|
1739
2798
|
)
|
|
1740
2799
|
|
|
1741
|
-
|
|
1742
|
-
return True
|
|
1743
|
-
|
|
1744
|
-
return False
|
|
2800
|
+
return bool(response and response.ok)
|
|
1745
2801
|
|
|
1746
2802
|
# end method definition
|
|
1747
2803
|
|
|
@@ -1756,27 +2812,53 @@ class OTDS:
|
|
|
1756
2812
|
) -> bool:
|
|
1757
2813
|
"""Assign an OTDS user to a product license (feature) in OTDS.
|
|
1758
2814
|
|
|
2815
|
+
licenses have this format:
|
|
2816
|
+
{
|
|
2817
|
+
'_oTLicenseType': 'NON-PRODUCTION',
|
|
2818
|
+
'_oTLicenseResource': '7382094f-a434-4714-9696-82864b6803da',
|
|
2819
|
+
'_oTLicenseResourceName': 'cs',
|
|
2820
|
+
'_oTLicenseProduct': 'EXTENDED_ECM',
|
|
2821
|
+
'name': 'EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da',
|
|
2822
|
+
'location': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
|
|
2823
|
+
'id': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
|
|
2824
|
+
'description': 'CS license',
|
|
2825
|
+
'values': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, ...]
|
|
2826
|
+
}
|
|
2827
|
+
|
|
1759
2828
|
Args:
|
|
1760
|
-
partition (str):
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
2829
|
+
partition (str):
|
|
2830
|
+
The user partition in OTDS, e.g. "Content Server Members".
|
|
2831
|
+
user_id (str):
|
|
2832
|
+
The ID of the user (= login name) to assign to the license.
|
|
2833
|
+
resource_id (str):
|
|
2834
|
+
The OTDS resource ID (this is ID not the resource name!).
|
|
2835
|
+
license_feature (str):
|
|
2836
|
+
The name of the license feature.
|
|
2837
|
+
license_name (str):
|
|
2838
|
+
The name of the license to assign.
|
|
2839
|
+
license_type (str, optional):
|
|
2840
|
+
The type of the license. Default is "Full", Extended ECM also has "Occasional".
|
|
2841
|
+
|
|
1766
2842
|
Returns:
|
|
1767
|
-
bool:
|
|
2843
|
+
bool:
|
|
2844
|
+
True if successful or False if the REST call fails or the license is not found.
|
|
2845
|
+
|
|
1768
2846
|
"""
|
|
1769
2847
|
|
|
1770
2848
|
licenses = self.get_license_for_resource(resource_id)
|
|
2849
|
+
if not licenses:
|
|
2850
|
+
self.logger.error(
|
|
2851
|
+
"Resource with ID -> '%s' does not exist or has no licenses",
|
|
2852
|
+
resource_id,
|
|
2853
|
+
)
|
|
2854
|
+
return False
|
|
1771
2855
|
|
|
1772
2856
|
for lic in licenses:
|
|
1773
2857
|
if lic["_oTLicenseProduct"] == license_name:
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
except UnboundLocalError:
|
|
1779
|
-
logger.error(
|
|
2858
|
+
license_id = lic["id"]
|
|
2859
|
+
break
|
|
2860
|
+
else:
|
|
2861
|
+
self.logger.error(
|
|
1780
2862
|
"Cannot find license -> '%s' for resource -> %s",
|
|
1781
2863
|
license_name,
|
|
1782
2864
|
resource_id,
|
|
@@ -1787,7 +2869,7 @@ class OTDS:
|
|
|
1787
2869
|
if user:
|
|
1788
2870
|
user_location = user["location"]
|
|
1789
2871
|
else:
|
|
1790
|
-
logger.error("Cannot find location for user -> '%s'", user_id)
|
|
2872
|
+
self.logger.error("Cannot find location for user -> '%s'", user_id)
|
|
1791
2873
|
return False
|
|
1792
2874
|
|
|
1793
2875
|
license_post_body_json = {
|
|
@@ -1797,12 +2879,12 @@ class OTDS:
|
|
|
1797
2879
|
"values": [{"name": "counter", "values": [license_feature]}],
|
|
1798
2880
|
}
|
|
1799
2881
|
|
|
1800
|
-
request_url = self.license_url() + "/object/" +
|
|
2882
|
+
request_url = self.license_url() + "/object/" + license_id
|
|
1801
2883
|
|
|
1802
|
-
logger.debug(
|
|
2884
|
+
self.logger.debug(
|
|
1803
2885
|
"Assign license feature -> '%s' of license -> '%s' associated with resource -> '%s' to user -> '%s'; calling -> %s",
|
|
1804
2886
|
license_feature,
|
|
1805
|
-
|
|
2887
|
+
license_id,
|
|
1806
2888
|
resource_id,
|
|
1807
2889
|
user_id,
|
|
1808
2890
|
request_url,
|
|
@@ -1814,13 +2896,15 @@ class OTDS:
|
|
|
1814
2896
|
json_data=license_post_body_json,
|
|
1815
2897
|
timeout=None,
|
|
1816
2898
|
failure_message="Failed to add license feature -> '{}' associated with resource -> '{}' to user -> '{}'".format(
|
|
1817
|
-
license_feature,
|
|
2899
|
+
license_feature,
|
|
2900
|
+
resource_id,
|
|
2901
|
+
user_id,
|
|
1818
2902
|
),
|
|
1819
2903
|
parse_request_response=False,
|
|
1820
2904
|
)
|
|
1821
2905
|
|
|
1822
2906
|
if response and response.ok:
|
|
1823
|
-
logger.debug(
|
|
2907
|
+
self.logger.debug(
|
|
1824
2908
|
"Added license feature -> '%s' to user -> '%s'",
|
|
1825
2909
|
license_feature,
|
|
1826
2910
|
user_id,
|
|
@@ -1841,44 +2925,51 @@ class OTDS:
|
|
|
1841
2925
|
) -> bool:
|
|
1842
2926
|
"""Assign an OTDS partition to a product license (feature).
|
|
1843
2927
|
|
|
2928
|
+
licenses have this format:
|
|
2929
|
+
{
|
|
2930
|
+
'_oTLicenseType': 'NON-PRODUCTION',
|
|
2931
|
+
'_oTLicenseResource': '7382094f-a434-4714-9696-82864b6803da',
|
|
2932
|
+
'_oTLicenseResourceName': 'cs',
|
|
2933
|
+
'_oTLicenseProduct': 'EXTENDED_ECM',
|
|
2934
|
+
'name': 'EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da',
|
|
2935
|
+
'location': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
|
|
2936
|
+
'id': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
|
|
2937
|
+
'description': 'CS license',
|
|
2938
|
+
'values': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, ...]
|
|
2939
|
+
}
|
|
2940
|
+
|
|
1844
2941
|
Args:
|
|
1845
|
-
partition_name (str):
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
2942
|
+
partition_name (str):
|
|
2943
|
+
The user partition in OTDS, e.g. "Content Server Members".
|
|
2944
|
+
resource_id (str):
|
|
2945
|
+
The OTDS resource ID (this is ID not the resource name!).
|
|
2946
|
+
license_feature (str):
|
|
2947
|
+
The name of the license feature, e.g. "X2" or "ADDON_ENGINEERING".
|
|
2948
|
+
license_name (str):
|
|
2949
|
+
The name of the license to assign, e.g. "EXTENDED_ECM" or "INTELLGENT_VIEWIMG".
|
|
2950
|
+
license_type (str, optional):
|
|
2951
|
+
The license type. Default is "Full", Extended ECM also has "Occasional"
|
|
2952
|
+
|
|
1850
2953
|
Returns:
|
|
1851
|
-
bool:
|
|
2954
|
+
bool:
|
|
2955
|
+
True if successful or False if the REST call fails or the license is not found.
|
|
2956
|
+
|
|
1852
2957
|
"""
|
|
1853
2958
|
|
|
1854
2959
|
licenses = self.get_license_for_resource(resource_id)
|
|
1855
2960
|
if not licenses:
|
|
1856
|
-
logger.error(
|
|
2961
|
+
self.logger.error(
|
|
1857
2962
|
"Resource with ID -> '%s' does not exist or has no licenses",
|
|
1858
2963
|
resource_id,
|
|
1859
2964
|
)
|
|
1860
2965
|
return False
|
|
1861
2966
|
|
|
1862
|
-
# licenses have this format:
|
|
1863
|
-
# {
|
|
1864
|
-
# '_oTLicenseType': 'NON-PRODUCTION',
|
|
1865
|
-
# '_oTLicenseResource': '7382094f-a434-4714-9696-82864b6803da',
|
|
1866
|
-
# '_oTLicenseResourceName': 'cs',
|
|
1867
|
-
# '_oTLicenseProduct': 'EXTENDED_ECM',
|
|
1868
|
-
# 'name': 'EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da',
|
|
1869
|
-
# 'location': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
|
|
1870
|
-
# 'id': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
|
|
1871
|
-
# 'description': 'CS license',
|
|
1872
|
-
# 'values': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, ...]
|
|
1873
|
-
# }
|
|
1874
2967
|
for lic in licenses:
|
|
1875
2968
|
if lic["_oTLicenseProduct"] == license_name:
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
except UnboundLocalError:
|
|
1881
|
-
logger.error(
|
|
2969
|
+
license_id = lic["id"]
|
|
2970
|
+
break
|
|
2971
|
+
else:
|
|
2972
|
+
self.logger.error(
|
|
1882
2973
|
"Cannot find license -> %s for resource -> %s",
|
|
1883
2974
|
license_name,
|
|
1884
2975
|
resource_id,
|
|
@@ -1892,12 +2983,12 @@ class OTDS:
|
|
|
1892
2983
|
"values": [{"name": "counter", "values": [license_feature]}],
|
|
1893
2984
|
}
|
|
1894
2985
|
|
|
1895
|
-
request_url = self.license_url() + "/object/" +
|
|
2986
|
+
request_url = self.license_url() + "/object/" + license_id
|
|
1896
2987
|
|
|
1897
|
-
logger.debug(
|
|
2988
|
+
self.logger.debug(
|
|
1898
2989
|
"Assign license feature -> '%s' of license -> '%s' associated with resource -> '%s' to partition -> '%s'; calling -> %s",
|
|
1899
2990
|
license_feature,
|
|
1900
|
-
|
|
2991
|
+
license_id,
|
|
1901
2992
|
resource_id,
|
|
1902
2993
|
partition_name,
|
|
1903
2994
|
request_url,
|
|
@@ -1909,13 +3000,15 @@ class OTDS:
|
|
|
1909
3000
|
json_data=license_post_body_json,
|
|
1910
3001
|
timeout=None,
|
|
1911
3002
|
failure_message="Failed to add license feature -> '{}' associated with resource -> '{}' to partition -> '{}'".format(
|
|
1912
|
-
license_feature,
|
|
3003
|
+
license_feature,
|
|
3004
|
+
resource_id,
|
|
3005
|
+
partition_name,
|
|
1913
3006
|
),
|
|
1914
3007
|
parse_request_response=False,
|
|
1915
3008
|
)
|
|
1916
3009
|
|
|
1917
3010
|
if response and response.ok:
|
|
1918
|
-
logger.debug(
|
|
3011
|
+
self.logger.debug(
|
|
1919
3012
|
"Added license feature -> '%s' to partition -> '%s'",
|
|
1920
3013
|
license_feature,
|
|
1921
3014
|
partition_name,
|
|
@@ -1932,73 +3025,89 @@ class OTDS:
|
|
|
1932
3025
|
license_feature: str,
|
|
1933
3026
|
license_name: str,
|
|
1934
3027
|
) -> dict | None:
|
|
1935
|
-
"""Return the licensed objects
|
|
1936
|
-
|
|
3028
|
+
"""Return the licensed objects for a license + license feature associated with an OTDS resource (like "cs").
|
|
3029
|
+
|
|
3030
|
+
Licensed objects can be users, groups, or partitions.
|
|
3031
|
+
|
|
3032
|
+
licenses have this format:
|
|
3033
|
+
{
|
|
3034
|
+
'_oTLicenseType': 'NON-PRODUCTION',
|
|
3035
|
+
'_oTLicenseResource': '7382094f-a434-4714-9696-82864b6803da',
|
|
3036
|
+
'_oTLicenseResourceName': 'cs',
|
|
3037
|
+
'_oTLicenseProduct': 'EXTENDED_ECM',
|
|
3038
|
+
'name': 'EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da',
|
|
3039
|
+
'location': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
|
|
3040
|
+
'id': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
|
|
3041
|
+
'description': 'CS license',
|
|
3042
|
+
'values': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, ...]
|
|
3043
|
+
}
|
|
1937
3044
|
|
|
1938
3045
|
Args:
|
|
1939
|
-
resource_id (str):
|
|
1940
|
-
|
|
1941
|
-
|
|
3046
|
+
resource_id (str):
|
|
3047
|
+
The OTDS resource ID (this is ID not the resource name!).
|
|
3048
|
+
license_feature (str):
|
|
3049
|
+
The name of the license feature, e.g. "X2" or "ADDON_ENGINEERING".
|
|
3050
|
+
license_name (str):
|
|
3051
|
+
The name of the license to assign, e.g. "EXTENDED_ECM" or "INTELLGENT_VIEWIMG".
|
|
3052
|
+
|
|
1942
3053
|
Returns:
|
|
1943
|
-
dict
|
|
3054
|
+
dict | None:
|
|
3055
|
+
The data structure of licensed objects or None in case of an error.
|
|
1944
3056
|
|
|
1945
|
-
|
|
3057
|
+
Example:
|
|
1946
3058
|
{
|
|
1947
3059
|
'status': 0,
|
|
1948
3060
|
'displayString': 'Success',
|
|
1949
3061
|
'exceptions': None,
|
|
1950
3062
|
'retValue': 0,
|
|
1951
|
-
'listGroupsResults': {
|
|
1952
|
-
|
|
1953
|
-
|
|
3063
|
+
'listGroupsResults': {
|
|
3064
|
+
'groups': [...],
|
|
3065
|
+
'actualPageSize': 0,
|
|
3066
|
+
'nextPageCookie': None,
|
|
3067
|
+
'requestedPageSize': 250
|
|
3068
|
+
},
|
|
3069
|
+
'listUsersResults': {
|
|
3070
|
+
'users': [...],
|
|
3071
|
+
'actualPageSize': 53,
|
|
3072
|
+
'nextPageCookie': None,
|
|
3073
|
+
'requestedPageSize': 250
|
|
3074
|
+
},
|
|
3075
|
+
'listUserPartitionResult': {
|
|
3076
|
+
'_userPartitions': [...],
|
|
3077
|
+
'warningMessage': None,
|
|
3078
|
+
'actualPageSize': 0,
|
|
3079
|
+
'nextPageCookie': None,
|
|
3080
|
+
'requestedPageSize': 250
|
|
3081
|
+
},
|
|
1954
3082
|
'version': 1
|
|
1955
3083
|
}
|
|
3084
|
+
|
|
1956
3085
|
"""
|
|
1957
3086
|
|
|
1958
3087
|
licenses = self.get_license_for_resource(resource_id)
|
|
1959
3088
|
if not licenses:
|
|
1960
|
-
logger.error(
|
|
3089
|
+
self.logger.error(
|
|
1961
3090
|
"Resource with ID -> '%s' does not exist or has no licenses",
|
|
1962
3091
|
resource_id,
|
|
1963
3092
|
)
|
|
1964
3093
|
return False
|
|
1965
3094
|
|
|
1966
|
-
# licenses have this format:
|
|
1967
|
-
# {
|
|
1968
|
-
# '_oTLicenseType': 'NON-PRODUCTION',
|
|
1969
|
-
# '_oTLicenseResource': '7382094f-a434-4714-9696-82864b6803da',
|
|
1970
|
-
# '_oTLicenseResourceName': 'cs',
|
|
1971
|
-
# '_oTLicenseProduct': 'EXTENDED_ECM',
|
|
1972
|
-
# 'name': 'EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da',
|
|
1973
|
-
# 'location': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
|
|
1974
|
-
# 'id': 'cn=EXTENDED_ECM¹7382094f-a434-4714-9696-82864b6803da,ou=Licenses,dc=identity,dc=opentext,dc=net',
|
|
1975
|
-
# 'description': 'CS license',
|
|
1976
|
-
# 'values': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, ...]
|
|
1977
|
-
# }
|
|
1978
3095
|
for lic in licenses:
|
|
1979
3096
|
if lic["_oTLicenseProduct"] == license_name:
|
|
1980
3097
|
license_location = lic["location"]
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
except UnboundLocalError:
|
|
1985
|
-
logger.error(
|
|
3098
|
+
break
|
|
3099
|
+
else:
|
|
3100
|
+
self.logger.error(
|
|
1986
3101
|
"Cannot find license -> %s for resource -> %s",
|
|
1987
3102
|
license_name,
|
|
1988
3103
|
resource_id,
|
|
1989
3104
|
)
|
|
1990
3105
|
return False
|
|
1991
3106
|
|
|
1992
|
-
request_url = (
|
|
1993
|
-
self.license_url()
|
|
1994
|
-
+ "/object/"
|
|
1995
|
-
+ license_location
|
|
1996
|
-
+ "?counter="
|
|
1997
|
-
+ license_feature
|
|
1998
|
-
)
|
|
3107
|
+
request_url = self.license_url() + "/object/" + license_location + "?counter=" + license_feature
|
|
1999
3108
|
|
|
2000
|
-
logger.debug(
|
|
2001
|
-
"Get licensed objects for license -> %s and license feature -> %s associated with resource -> %s; calling -> %s",
|
|
3109
|
+
self.logger.debug(
|
|
3110
|
+
"Get licensed objects for license -> '%s' and license feature -> '%s' associated with resource -> '%s'; calling -> %s",
|
|
2002
3111
|
license_name,
|
|
2003
3112
|
license_feature,
|
|
2004
3113
|
resource_id,
|
|
@@ -2010,25 +3119,37 @@ class OTDS:
|
|
|
2010
3119
|
method="GET",
|
|
2011
3120
|
timeout=None,
|
|
2012
3121
|
failure_message="Failed to get licensed objects for license -> '{}' and license feature -> '{}' associated with resource -> '{}'".format(
|
|
2013
|
-
license_name,
|
|
3122
|
+
license_name,
|
|
3123
|
+
license_feature,
|
|
3124
|
+
resource_id,
|
|
2014
3125
|
),
|
|
2015
3126
|
)
|
|
2016
3127
|
|
|
2017
3128
|
# end method definition
|
|
2018
3129
|
|
|
2019
3130
|
def is_user_licensed(
|
|
2020
|
-
self,
|
|
3131
|
+
self,
|
|
3132
|
+
user_name: str,
|
|
3133
|
+
resource_id: str,
|
|
3134
|
+
license_feature: str,
|
|
3135
|
+
license_name: str,
|
|
2021
3136
|
) -> bool:
|
|
2022
3137
|
"""Check if a user is licensed for a license and license feature associated with a particular OTDS resource.
|
|
2023
3138
|
|
|
2024
3139
|
Args:
|
|
2025
|
-
user_name (str):
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
3140
|
+
user_name (str):
|
|
3141
|
+
The login name of the OTDS user.
|
|
3142
|
+
resource_id (str):
|
|
3143
|
+
The OTDS resource ID (this is ID not the resource name!).
|
|
3144
|
+
license_feature (str):
|
|
3145
|
+
The name of the license feature, e.g. "X2" or "ADDON_ENGINEERING".
|
|
3146
|
+
license_name (str):
|
|
3147
|
+
The name of the license to assign, e.g. "EXTENDED_ECM" or "INTELLGENT_VIEWIMG".
|
|
2029
3148
|
|
|
2030
3149
|
Returns:
|
|
2031
|
-
bool:
|
|
3150
|
+
bool:
|
|
3151
|
+
True if the user is licensed and False otherwise.
|
|
3152
|
+
|
|
2032
3153
|
"""
|
|
2033
3154
|
|
|
2034
3155
|
response = self.get_licensed_objects(
|
|
@@ -2050,26 +3171,33 @@ class OTDS:
|
|
|
2050
3171
|
None,
|
|
2051
3172
|
)
|
|
2052
3173
|
|
|
2053
|
-
|
|
2054
|
-
return True
|
|
2055
|
-
|
|
2056
|
-
return False
|
|
3174
|
+
return bool(user)
|
|
2057
3175
|
|
|
2058
3176
|
# end method definition
|
|
2059
3177
|
|
|
2060
3178
|
def is_group_licensed(
|
|
2061
|
-
self,
|
|
3179
|
+
self,
|
|
3180
|
+
group_name: str,
|
|
3181
|
+
resource_id: str,
|
|
3182
|
+
license_feature: str,
|
|
3183
|
+
license_name: str,
|
|
2062
3184
|
) -> bool:
|
|
2063
3185
|
"""Check if a group is licensed for a license and license feature associated with a particular OTDS resource.
|
|
2064
3186
|
|
|
2065
3187
|
Args:
|
|
2066
|
-
group_name (str):
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
3188
|
+
group_name (str):
|
|
3189
|
+
The name of the OTDS user group.
|
|
3190
|
+
resource_id (str):
|
|
3191
|
+
The OTDS resource ID (this is ID not the resource name!).
|
|
3192
|
+
license_feature (str):
|
|
3193
|
+
The name of the license feature, e.g. "X2" or "ADDON_ENGINEERING".
|
|
3194
|
+
license_name (str):
|
|
3195
|
+
The name of the license to assign, e.g. "EXTENDED_ECM" or "INTELLGENT_VIEWIMG".
|
|
2070
3196
|
|
|
2071
3197
|
Returns:
|
|
2072
|
-
bool:
|
|
3198
|
+
bool:
|
|
3199
|
+
True if the group is licensed and False otherwise.
|
|
3200
|
+
|
|
2073
3201
|
"""
|
|
2074
3202
|
|
|
2075
3203
|
response = self.get_licensed_objects(
|
|
@@ -2091,10 +3219,7 @@ class OTDS:
|
|
|
2091
3219
|
None,
|
|
2092
3220
|
)
|
|
2093
3221
|
|
|
2094
|
-
|
|
2095
|
-
return True
|
|
2096
|
-
|
|
2097
|
-
return False
|
|
3222
|
+
return bool(group)
|
|
2098
3223
|
|
|
2099
3224
|
# end method definition
|
|
2100
3225
|
|
|
@@ -2105,16 +3230,22 @@ class OTDS:
|
|
|
2105
3230
|
license_feature: str,
|
|
2106
3231
|
license_name: str,
|
|
2107
3232
|
) -> bool:
|
|
2108
|
-
"""Check if a partition is licensed for a license
|
|
3233
|
+
"""Check if a partition is licensed for a license feature associated with a particular OTDS resource.
|
|
2109
3234
|
|
|
2110
3235
|
Args:
|
|
2111
|
-
partition_name (str):
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
3236
|
+
partition_name (str):
|
|
3237
|
+
The name of the OTDS user partition, e.g. "Content Server Members".
|
|
3238
|
+
resource_id (str):
|
|
3239
|
+
The OTDS resource ID (this is ID not the resource name!).
|
|
3240
|
+
license_feature (str):
|
|
3241
|
+
The name of the license feature, e.g. "X2" or "ADDON_ENGINEERING".
|
|
3242
|
+
license_name (str):
|
|
3243
|
+
The name of the license to assign, e.g. "EXTENDED_ECM" or "INTELLGENT_VIEWIMG".
|
|
2115
3244
|
|
|
2116
3245
|
Returns:
|
|
2117
|
-
bool:
|
|
3246
|
+
bool:
|
|
3247
|
+
True if the partition is licensed and False otherwise.
|
|
3248
|
+
|
|
2118
3249
|
"""
|
|
2119
3250
|
|
|
2120
3251
|
response = self.get_licensed_objects(
|
|
@@ -2136,124 +3267,117 @@ class OTDS:
|
|
|
2136
3267
|
None,
|
|
2137
3268
|
)
|
|
2138
3269
|
|
|
2139
|
-
|
|
2140
|
-
return True
|
|
2141
|
-
|
|
2142
|
-
return False
|
|
3270
|
+
return bool(partition)
|
|
2143
3271
|
|
|
2144
3272
|
# end method definition
|
|
2145
|
-
|
|
2146
|
-
def import_synchronized_partition_members(self, name: str) ->
|
|
2147
|
-
"""Import users and groups to partition
|
|
3273
|
+
|
|
3274
|
+
def import_synchronized_partition_members(self, name: str) -> bool:
|
|
3275
|
+
"""Import users and groups to partition.
|
|
2148
3276
|
|
|
2149
3277
|
Args:
|
|
2150
|
-
name (str):
|
|
3278
|
+
name (str):
|
|
3279
|
+
The name of the partition in which users need to be imported.
|
|
3280
|
+
|
|
2151
3281
|
Returns:
|
|
2152
|
-
|
|
3282
|
+
bool:
|
|
3283
|
+
True = Success, False = Error.
|
|
3284
|
+
|
|
2153
3285
|
"""
|
|
3286
|
+
|
|
2154
3287
|
command = {"command": "import"}
|
|
2155
|
-
request_url = self.synchronized_partition_url() + f
|
|
2156
|
-
|
|
2157
|
-
|
|
3288
|
+
request_url = self.synchronized_partition_url() + f"/{name}/command"
|
|
3289
|
+
|
|
3290
|
+
self.logger.debug(
|
|
3291
|
+
"Importing users and groups into partition -> '%s'; calling -> %s",
|
|
2158
3292
|
name,
|
|
2159
3293
|
request_url,
|
|
2160
3294
|
)
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
)
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
self.authenticate(revalidate=True)
|
|
2176
|
-
retries += 1
|
|
2177
|
-
else:
|
|
2178
|
-
logger.error(
|
|
2179
|
-
"Failed to Import users and groups to synchronized partition -> %s; error -> %s (%s)",
|
|
2180
|
-
name,
|
|
2181
|
-
response.text,
|
|
2182
|
-
response.status_code,
|
|
2183
|
-
)
|
|
2184
|
-
return None
|
|
2185
|
-
|
|
3295
|
+
|
|
3296
|
+
response = self.do_request(
|
|
3297
|
+
url=request_url,
|
|
3298
|
+
method="POST",
|
|
3299
|
+
json_data=command,
|
|
3300
|
+
timeout=None,
|
|
3301
|
+
failure_message="Failed to import users and groups to synchronized partition -> '{}'".format(
|
|
3302
|
+
name,
|
|
3303
|
+
),
|
|
3304
|
+
parse_request_response=False,
|
|
3305
|
+
)
|
|
3306
|
+
|
|
3307
|
+
return bool(response and response.ok and response.status_code == 204)
|
|
3308
|
+
|
|
2186
3309
|
# end of method definition
|
|
2187
|
-
|
|
2188
|
-
def add_synchronized_partition(
|
|
2189
|
-
|
|
3310
|
+
|
|
3311
|
+
def add_synchronized_partition(
|
|
3312
|
+
self,
|
|
3313
|
+
name: str,
|
|
3314
|
+
description: str,
|
|
3315
|
+
data: dict,
|
|
3316
|
+
) -> dict | None:
|
|
3317
|
+
"""Add a new synchronized partition to OTDS.
|
|
2190
3318
|
|
|
2191
3319
|
Args:
|
|
2192
|
-
name (str):
|
|
2193
|
-
|
|
2194
|
-
|
|
3320
|
+
name (str):
|
|
3321
|
+
The name of the new synchronized partition.
|
|
3322
|
+
description (str):
|
|
3323
|
+
The description of the new synchronized partition.
|
|
3324
|
+
data (dict):
|
|
3325
|
+
The data for creating synchronized partition
|
|
3326
|
+
|
|
2195
3327
|
Returns:
|
|
2196
|
-
dict
|
|
3328
|
+
dict | None:
|
|
3329
|
+
Request response or None if the creation fails.
|
|
3330
|
+
|
|
2197
3331
|
"""
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
],
|
|
2206
|
-
"basicInfo": {
|
|
2207
|
-
},
|
|
2208
|
-
"basicAttributes": []
|
|
3332
|
+
|
|
3333
|
+
synchronized_partition_post_body_json = {
|
|
3334
|
+
"ipConnectionParameter": [],
|
|
3335
|
+
"ipAuthentication": {},
|
|
3336
|
+
"objectClassNameMapping": [],
|
|
3337
|
+
"basicInfo": {},
|
|
3338
|
+
"basicAttributes": [],
|
|
2209
3339
|
}
|
|
2210
|
-
|
|
3340
|
+
synchronized_partition_post_body_json.update(data)
|
|
3341
|
+
|
|
2211
3342
|
request_url = self.synchronized_partition_url()
|
|
2212
|
-
logger.debug(
|
|
2213
|
-
"Adding synchronized partition -> %s (%s); calling -> %s",
|
|
3343
|
+
self.logger.debug(
|
|
3344
|
+
"Adding synchronized partition -> '%s' ('%s'); calling -> %s",
|
|
2214
3345
|
name,
|
|
2215
3346
|
description,
|
|
2216
3347
|
request_url,
|
|
2217
3348
|
)
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
if response.ok:
|
|
2229
|
-
return self.parse_request_response(response)
|
|
2230
|
-
# Check if Session has expired - then re-authenticate and try once more
|
|
2231
|
-
elif response.status_code == 401 and retries == 0:
|
|
2232
|
-
logger.debug("Session has expired - try to re-authenticate...")
|
|
2233
|
-
self.authenticate(revalidate=True)
|
|
2234
|
-
retries += 1
|
|
2235
|
-
else:
|
|
2236
|
-
logger.error(
|
|
2237
|
-
"Failed to add synchronized partition -> %s; error -> %s (%s)",
|
|
2238
|
-
name,
|
|
2239
|
-
response.text,
|
|
2240
|
-
response.status_code,
|
|
2241
|
-
)
|
|
2242
|
-
return None
|
|
2243
|
-
|
|
3349
|
+
synchronized_partition_post_body_json["ipAuthentication"]["bindPassword"] = self.config()["bindPassword"]
|
|
3350
|
+
|
|
3351
|
+
return self.do_request(
|
|
3352
|
+
url=request_url,
|
|
3353
|
+
method="POST",
|
|
3354
|
+
json_data=synchronized_partition_post_body_json,
|
|
3355
|
+
timeout=None,
|
|
3356
|
+
failure_message="Failed to add synchronized partition -> '{}'".format(name),
|
|
3357
|
+
)
|
|
3358
|
+
|
|
2244
3359
|
# end of method definition
|
|
2245
3360
|
|
|
2246
3361
|
def add_system_attribute(
|
|
2247
|
-
self,
|
|
3362
|
+
self,
|
|
3363
|
+
name: str,
|
|
3364
|
+
value: str,
|
|
3365
|
+
description: str = "",
|
|
2248
3366
|
) -> dict | None:
|
|
2249
|
-
"""Add a new system attribute to OTDS
|
|
3367
|
+
"""Add a new system attribute to OTDS.
|
|
2250
3368
|
|
|
2251
3369
|
Args:
|
|
2252
|
-
name (str):
|
|
2253
|
-
|
|
2254
|
-
|
|
3370
|
+
name (str):
|
|
3371
|
+
The name of the new system attribute.
|
|
3372
|
+
value (str):
|
|
3373
|
+
The value of the system attribute.
|
|
3374
|
+
description (str, optional):
|
|
3375
|
+
The optional description of the system attribute.
|
|
3376
|
+
|
|
2255
3377
|
Returns:
|
|
2256
|
-
dict
|
|
3378
|
+
dict | None:
|
|
3379
|
+
Request response (dictionary) or None if the REST call fails.
|
|
3380
|
+
|
|
2257
3381
|
"""
|
|
2258
3382
|
|
|
2259
3383
|
system_attribute_post_body_json = {
|
|
@@ -2265,7 +3389,7 @@ class OTDS:
|
|
|
2265
3389
|
request_url = "{}/system_attributes".format(self.config()["systemConfigUrl"])
|
|
2266
3390
|
|
|
2267
3391
|
if description:
|
|
2268
|
-
logger.debug(
|
|
3392
|
+
self.logger.debug(
|
|
2269
3393
|
"Add system attribute -> '%s' ('%s') with value -> %s; calling -> %s",
|
|
2270
3394
|
name,
|
|
2271
3395
|
description,
|
|
@@ -2273,7 +3397,7 @@ class OTDS:
|
|
|
2273
3397
|
request_url,
|
|
2274
3398
|
)
|
|
2275
3399
|
else:
|
|
2276
|
-
logger.debug(
|
|
3400
|
+
self.logger.debug(
|
|
2277
3401
|
"Add system attribute -> '%s' with value -> %s; calling -> %s",
|
|
2278
3402
|
name,
|
|
2279
3403
|
value,
|
|
@@ -2286,24 +3410,28 @@ class OTDS:
|
|
|
2286
3410
|
json_data=system_attribute_post_body_json,
|
|
2287
3411
|
timeout=None,
|
|
2288
3412
|
failure_message="Failed to add system attribute -> '{}' with value -> '{}'".format(
|
|
2289
|
-
name,
|
|
3413
|
+
name,
|
|
3414
|
+
value,
|
|
2290
3415
|
),
|
|
2291
3416
|
)
|
|
2292
3417
|
|
|
2293
3418
|
# end method definition
|
|
2294
3419
|
|
|
2295
3420
|
def get_trusted_sites(self) -> dict | None:
|
|
2296
|
-
"""Get all configured OTDS trusted sites
|
|
3421
|
+
"""Get all configured OTDS trusted sites.
|
|
2297
3422
|
|
|
2298
3423
|
Args:
|
|
2299
3424
|
None
|
|
3425
|
+
|
|
2300
3426
|
Returns:
|
|
2301
|
-
dict
|
|
3427
|
+
dict | None:
|
|
3428
|
+
Request response or None if the REST call fails.
|
|
3429
|
+
|
|
2302
3430
|
"""
|
|
2303
3431
|
|
|
2304
3432
|
request_url = "{}/whitelist".format(self.config()["systemConfigUrl"])
|
|
2305
3433
|
|
|
2306
|
-
logger.debug("Get trusted sites; calling -> %s", request_url)
|
|
3434
|
+
self.logger.debug("Get trusted sites; calling -> %s", request_url)
|
|
2307
3435
|
|
|
2308
3436
|
return self.do_request(
|
|
2309
3437
|
url=request_url,
|
|
@@ -2315,12 +3443,16 @@ class OTDS:
|
|
|
2315
3443
|
# end method definition
|
|
2316
3444
|
|
|
2317
3445
|
def add_trusted_site(self, trusted_site: str) -> dict | None:
|
|
2318
|
-
"""Add a new OTDS trusted site
|
|
3446
|
+
"""Add a new OTDS trusted site.
|
|
2319
3447
|
|
|
2320
3448
|
Args:
|
|
2321
|
-
trusted_site (str):
|
|
2322
|
-
|
|
2323
|
-
|
|
3449
|
+
trusted_site (str):
|
|
3450
|
+
The name of the new trusted site.
|
|
3451
|
+
|
|
3452
|
+
Returns:
|
|
3453
|
+
dict | None:
|
|
3454
|
+
Request response or None if the REST call fails.
|
|
3455
|
+
|
|
2324
3456
|
"""
|
|
2325
3457
|
|
|
2326
3458
|
trusted_site_post_body_json = {"stringList": [trusted_site]}
|
|
@@ -2331,13 +3463,15 @@ class OTDS:
|
|
|
2331
3463
|
|
|
2332
3464
|
if existing_trusted_sites:
|
|
2333
3465
|
trusted_site_post_body_json["stringList"].extend(
|
|
2334
|
-
existing_trusted_sites["stringList"]
|
|
3466
|
+
existing_trusted_sites["stringList"],
|
|
2335
3467
|
)
|
|
2336
3468
|
|
|
2337
3469
|
request_url = "{}/whitelist".format(self.config()["systemConfigUrl"])
|
|
2338
3470
|
|
|
2339
|
-
logger.debug(
|
|
2340
|
-
"Add trusted site -> '%s'; calling -> %s",
|
|
3471
|
+
self.logger.debug(
|
|
3472
|
+
"Add trusted site -> '%s'; calling -> %s",
|
|
3473
|
+
trusted_site,
|
|
3474
|
+
request_url,
|
|
2341
3475
|
)
|
|
2342
3476
|
|
|
2343
3477
|
response = self.do_request(
|
|
@@ -2349,27 +3483,38 @@ class OTDS:
|
|
|
2349
3483
|
parse_request_response=False, # don't parse it!
|
|
2350
3484
|
)
|
|
2351
3485
|
|
|
2352
|
-
if not response.ok:
|
|
3486
|
+
if not response or not response.ok:
|
|
2353
3487
|
return None
|
|
2354
3488
|
|
|
2355
3489
|
return response
|
|
2356
3490
|
|
|
2357
3491
|
# end method definition
|
|
2358
3492
|
|
|
2359
|
-
def enable_audit(
|
|
2360
|
-
|
|
3493
|
+
def enable_audit(
|
|
3494
|
+
self,
|
|
3495
|
+
enable: bool = True,
|
|
3496
|
+
days_to_keep: int = 7,
|
|
3497
|
+
event_types: list | None = None,
|
|
3498
|
+
) -> dict | None:
|
|
3499
|
+
"""Enable the OTDS Audit.
|
|
2361
3500
|
|
|
2362
3501
|
Args:
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
3502
|
+
enable (bool, optional):
|
|
3503
|
+
True = enable audit, False = disable audit.
|
|
3504
|
+
days_to_keep (int, optional):
|
|
3505
|
+
Days to keep the audit information. Default = 7 Days.
|
|
3506
|
+
event_types (list, optional):
|
|
3507
|
+
A list of event types to record.
|
|
3508
|
+
If None then a default list will be used.
|
|
3509
|
+
|
|
3510
|
+
Returns:
|
|
3511
|
+
dict | None:
|
|
3512
|
+
Request response or None if the REST call fails.
|
|
3513
|
+
|
|
2366
3514
|
"""
|
|
2367
3515
|
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
"enabled": "true",
|
|
2371
|
-
"auditTo": "DATABASE",
|
|
2372
|
-
"eventIDs": [
|
|
3516
|
+
if event_types is None:
|
|
3517
|
+
event_types = [
|
|
2373
3518
|
"User Create",
|
|
2374
3519
|
"Group Create",
|
|
2375
3520
|
"User Delete",
|
|
@@ -2417,19 +3562,30 @@ class OTDS:
|
|
|
2417
3562
|
"Tenant Delete",
|
|
2418
3563
|
"Tenant Modify",
|
|
2419
3564
|
"Migration",
|
|
2420
|
-
]
|
|
3565
|
+
]
|
|
3566
|
+
|
|
3567
|
+
audit_put_body_json = {
|
|
3568
|
+
"daysToKeep": str(days_to_keep),
|
|
3569
|
+
"enabled": enable,
|
|
3570
|
+
"auditTo": "DATABASE",
|
|
3571
|
+
"eventIDs": event_types,
|
|
2421
3572
|
}
|
|
2422
3573
|
|
|
2423
3574
|
request_url = "{}/audit".format(self.config()["systemConfigUrl"])
|
|
2424
3575
|
|
|
2425
|
-
|
|
3576
|
+
if enable:
|
|
3577
|
+
self.logger.debug("Enable audit; calling -> %s", request_url)
|
|
3578
|
+
failure_message = "Failed to enable audit"
|
|
3579
|
+
else:
|
|
3580
|
+
self.logger.debug("Disable audit; calling -> %s", request_url)
|
|
3581
|
+
failure_message = "Failed to disable audit"
|
|
2426
3582
|
|
|
2427
3583
|
return self.do_request(
|
|
2428
3584
|
url=request_url,
|
|
2429
3585
|
method="PUT",
|
|
2430
3586
|
json_data=audit_put_body_json,
|
|
2431
3587
|
timeout=None,
|
|
2432
|
-
failure_message=
|
|
3588
|
+
failure_message=failure_message,
|
|
2433
3589
|
parse_request_response=False,
|
|
2434
3590
|
)
|
|
2435
3591
|
|
|
@@ -2447,21 +3603,33 @@ class OTDS:
|
|
|
2447
3603
|
default_scopes: list | None = None, # in OTDS UI: Default scopes
|
|
2448
3604
|
secret: str = "",
|
|
2449
3605
|
) -> dict | None:
|
|
2450
|
-
"""Add a new OAuth client to OTDS
|
|
3606
|
+
"""Add a new OAuth client to OTDS.
|
|
2451
3607
|
|
|
2452
3608
|
Args:
|
|
2453
|
-
client_id (str):
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
3609
|
+
client_id (str):
|
|
3610
|
+
The name of the new OAuth client (should not have blanks).
|
|
3611
|
+
description (str):
|
|
3612
|
+
The description of the OAuth client.
|
|
3613
|
+
redirect_urls (list):
|
|
3614
|
+
A list of redirect URLs (strings).
|
|
3615
|
+
allow_impersonation (bool, optional):
|
|
3616
|
+
Whether or not to allow impersonation.
|
|
3617
|
+
confidential (bool, optional):
|
|
3618
|
+
is confidential
|
|
3619
|
+
auth_scopes (list, optional):
|
|
3620
|
+
The authorization scope. If empty then "Global" is assumed.
|
|
3621
|
+
allowed_scopes (list, optional):
|
|
3622
|
+
In the OTDS UI this is called Permissible scopes.
|
|
3623
|
+
default_scopes (list, optional):
|
|
3624
|
+
In the OTDS UI this is called Default scopes.
|
|
3625
|
+
secret (str, optional):
|
|
3626
|
+
Predefined OAuth client secret. If empty a new secret is generated.
|
|
3627
|
+
|
|
2462
3628
|
Returns:
|
|
2463
|
-
dict
|
|
2464
|
-
|
|
3629
|
+
dict | None:
|
|
3630
|
+
Request response or None if the creation fails.
|
|
3631
|
+
|
|
3632
|
+
Example:
|
|
2465
3633
|
{
|
|
2466
3634
|
"description": "string",
|
|
2467
3635
|
"redirectURLs": [
|
|
@@ -2503,6 +3671,7 @@ class OTDS:
|
|
|
2503
3671
|
"urlId": "string",
|
|
2504
3672
|
"urlLocation": "string"
|
|
2505
3673
|
}
|
|
3674
|
+
|
|
2506
3675
|
"""
|
|
2507
3676
|
|
|
2508
3677
|
# Avoid linter warning W0102:
|
|
@@ -2537,7 +3706,7 @@ class OTDS:
|
|
|
2537
3706
|
|
|
2538
3707
|
request_url = self.oauth_client_url()
|
|
2539
3708
|
|
|
2540
|
-
logger.debug(
|
|
3709
|
+
self.logger.debug(
|
|
2541
3710
|
"Adding oauth client -> '%s' (%s); calling -> %s",
|
|
2542
3711
|
description,
|
|
2543
3712
|
client_id,
|
|
@@ -2555,18 +3724,27 @@ class OTDS:
|
|
|
2555
3724
|
# end method definition
|
|
2556
3725
|
|
|
2557
3726
|
def get_oauth_client(self, client_id: str, show_error: bool = True) -> dict | None:
|
|
2558
|
-
"""Get an existing OAuth client from OTDS
|
|
3727
|
+
"""Get an existing OAuth client from OTDS.
|
|
2559
3728
|
|
|
2560
3729
|
Args:
|
|
2561
|
-
client_id (str):
|
|
2562
|
-
|
|
3730
|
+
client_id (str):
|
|
3731
|
+
The name (= ID) of the OAuth client to retrieve
|
|
3732
|
+
show_error (bool, optional):
|
|
3733
|
+
Whether or not we want to log an error if partion is not found.
|
|
3734
|
+
|
|
2563
3735
|
Returns:
|
|
2564
|
-
dict
|
|
3736
|
+
dict | None:
|
|
3737
|
+
Request response (dictionary) or None if the client is not found.
|
|
3738
|
+
|
|
2565
3739
|
"""
|
|
2566
3740
|
|
|
2567
3741
|
request_url = "{}/{}".format(self.oauth_client_url(), client_id)
|
|
2568
3742
|
|
|
2569
|
-
logger.debug(
|
|
3743
|
+
self.logger.debug(
|
|
3744
|
+
"Get oauth client -> '%s'; calling -> %s",
|
|
3745
|
+
client_id,
|
|
3746
|
+
request_url,
|
|
3747
|
+
)
|
|
2570
3748
|
|
|
2571
3749
|
return self.do_request(
|
|
2572
3750
|
url=request_url,
|
|
@@ -2579,22 +3757,26 @@ class OTDS:
|
|
|
2579
3757
|
# end method definition
|
|
2580
3758
|
|
|
2581
3759
|
def update_oauth_client(self, client_id: str, updates: dict) -> dict | None:
|
|
2582
|
-
"""
|
|
3760
|
+
"""Update an OAuth client with new values.
|
|
2583
3761
|
|
|
2584
3762
|
Args:
|
|
2585
|
-
client_id (str):
|
|
2586
|
-
|
|
2587
|
-
|
|
3763
|
+
client_id (str):
|
|
3764
|
+
The name (= ID) of the OAuth client.
|
|
3765
|
+
updates (dict):
|
|
3766
|
+
New values for OAuth client, e.g.
|
|
3767
|
+
{"description": "this is the new value"}
|
|
2588
3768
|
|
|
2589
3769
|
Returns:
|
|
2590
|
-
dict
|
|
3770
|
+
dict | None:
|
|
3771
|
+
Request response (json) or None if the REST call fails.
|
|
3772
|
+
|
|
2591
3773
|
"""
|
|
2592
3774
|
|
|
2593
3775
|
oauth_client_patch_body_json = updates
|
|
2594
3776
|
|
|
2595
3777
|
request_url = "{}/{}".format(self.oauth_client_url(), client_id)
|
|
2596
3778
|
|
|
2597
|
-
logger.debug(
|
|
3779
|
+
self.logger.debug(
|
|
2598
3780
|
"Update OAuth client -> '%s' with -> %s; calling -> %s",
|
|
2599
3781
|
client_id,
|
|
2600
3782
|
str(updates),
|
|
@@ -2611,19 +3793,25 @@ class OTDS:
|
|
|
2611
3793
|
|
|
2612
3794
|
# end method definition
|
|
2613
3795
|
|
|
2614
|
-
def add_oauth_clients_to_access_role(self, access_role_name: str):
|
|
2615
|
-
"""Add
|
|
3796
|
+
def add_oauth_clients_to_access_role(self, access_role_name: str) -> dict | None:
|
|
3797
|
+
"""Add OAuth clients (in the "OAuthClients" partition) to an OTDS access role.
|
|
2616
3798
|
|
|
2617
3799
|
Args:
|
|
2618
|
-
access_role_name (str):
|
|
3800
|
+
access_role_name (str):
|
|
3801
|
+
The name of the OTDS access role.
|
|
3802
|
+
|
|
2619
3803
|
Returns:
|
|
2620
|
-
|
|
3804
|
+
dict | None:
|
|
3805
|
+
Response of REST call or None in case of an error.
|
|
3806
|
+
|
|
2621
3807
|
"""
|
|
2622
3808
|
|
|
2623
3809
|
request_url = self.config()["accessRoleUrl"] + "/" + access_role_name
|
|
2624
3810
|
|
|
2625
|
-
logger.debug(
|
|
2626
|
-
"Get access role -> '%s'; calling -> %s",
|
|
3811
|
+
self.logger.debug(
|
|
3812
|
+
"Get access role -> '%s'; calling -> %s",
|
|
3813
|
+
access_role_name,
|
|
3814
|
+
request_url,
|
|
2627
3815
|
)
|
|
2628
3816
|
|
|
2629
3817
|
access_role = self.do_request(
|
|
@@ -2631,7 +3819,7 @@ class OTDS:
|
|
|
2631
3819
|
method="GET",
|
|
2632
3820
|
timeout=None,
|
|
2633
3821
|
failure_message="Failed to retrieve access role -> '{}'".format(
|
|
2634
|
-
access_role_name
|
|
3822
|
+
access_role_name,
|
|
2635
3823
|
),
|
|
2636
3824
|
)
|
|
2637
3825
|
if not access_role:
|
|
@@ -2641,7 +3829,7 @@ class OTDS:
|
|
|
2641
3829
|
user_partitions = access_role["accessRoleMembers"]["userPartitions"]
|
|
2642
3830
|
for user_partition in user_partitions:
|
|
2643
3831
|
if user_partition["userPartition"] == "OAuthClients":
|
|
2644
|
-
logger.error(
|
|
3832
|
+
self.logger.error(
|
|
2645
3833
|
"OAuthClients partition already added to role -> %s",
|
|
2646
3834
|
access_role_name,
|
|
2647
3835
|
)
|
|
@@ -2656,7 +3844,7 @@ class OTDS:
|
|
|
2656
3844
|
method="GET",
|
|
2657
3845
|
timeout=None,
|
|
2658
3846
|
failure_message="Failed to get partition info for OAuthClients for role -> '{}'".format(
|
|
2659
|
-
access_role_name
|
|
3847
|
+
access_role_name,
|
|
2660
3848
|
),
|
|
2661
3849
|
)
|
|
2662
3850
|
if not response:
|
|
@@ -2671,7 +3859,7 @@ class OTDS:
|
|
|
2671
3859
|
"userPartition": None,
|
|
2672
3860
|
}
|
|
2673
3861
|
access_role["accessRoleMembers"]["organizationalUnits"].append(
|
|
2674
|
-
oauth_clients_ou_block
|
|
3862
|
+
oauth_clients_ou_block,
|
|
2675
3863
|
)
|
|
2676
3864
|
|
|
2677
3865
|
return self.do_request(
|
|
@@ -2679,7 +3867,7 @@ class OTDS:
|
|
|
2679
3867
|
method="PUT",
|
|
2680
3868
|
timeout=None,
|
|
2681
3869
|
warning_message="Failed to add OAuthClients to access role -> '{}'".format(
|
|
2682
|
-
access_role_name
|
|
3870
|
+
access_role_name,
|
|
2683
3871
|
),
|
|
2684
3872
|
show_error=False,
|
|
2685
3873
|
show_warning=True,
|
|
@@ -2689,39 +3877,42 @@ class OTDS:
|
|
|
2689
3877
|
# end method definition
|
|
2690
3878
|
|
|
2691
3879
|
def get_access_token(self, client_id: str, client_secret: str) -> str | None:
|
|
2692
|
-
"""Get the access token
|
|
3880
|
+
"""Get the access token.
|
|
2693
3881
|
|
|
2694
3882
|
Args:
|
|
2695
|
-
client_id (str):
|
|
2696
|
-
|
|
2697
|
-
|
|
3883
|
+
client_id (str):
|
|
3884
|
+
The OAuth client name (= ID).
|
|
3885
|
+
client_secret (str):
|
|
3886
|
+
The OAuth client secret. This is typically returned
|
|
3887
|
+
by add_oauth_client() method in ["secret"] field.
|
|
2698
3888
|
|
|
2699
3889
|
Returns:
|
|
2700
|
-
str
|
|
3890
|
+
str | None:
|
|
3891
|
+
The access token, or None in case of an error.
|
|
3892
|
+
|
|
2701
3893
|
"""
|
|
2702
3894
|
|
|
2703
3895
|
encoded_client_secret = "{}:{}".format(client_id, client_secret).encode("utf-8")
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
+ base64.b64encode(encoded_client_secret).decode("utf-8"),
|
|
3896
|
+
|
|
3897
|
+
request_header = {
|
|
3898
|
+
"Authorization": "Basic " + base64.b64encode(encoded_client_secret).decode("utf-8"),
|
|
2707
3899
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
2708
3900
|
}
|
|
2709
|
-
|
|
2710
3901
|
request_url = self.token_url()
|
|
2711
3902
|
|
|
2712
3903
|
response = requests.post(
|
|
2713
3904
|
url=request_url,
|
|
2714
3905
|
data={"grant_type": "client_credentials"},
|
|
2715
|
-
headers=
|
|
2716
|
-
timeout=
|
|
3906
|
+
headers=request_header,
|
|
3907
|
+
timeout=REQUEST_TIMEOUT,
|
|
2717
3908
|
)
|
|
2718
3909
|
|
|
2719
3910
|
access_token = None
|
|
2720
3911
|
if response.ok:
|
|
2721
|
-
|
|
3912
|
+
access_token = self.parse_request_response(response)
|
|
2722
3913
|
|
|
2723
|
-
if "access_token" in
|
|
2724
|
-
access_token =
|
|
3914
|
+
if "access_token" in access_token:
|
|
3915
|
+
access_token = access_token["access_token"]
|
|
2725
3916
|
else:
|
|
2726
3917
|
return None
|
|
2727
3918
|
|
|
@@ -2733,12 +3924,17 @@ class OTDS:
|
|
|
2733
3924
|
"""Get the OTDS auth handler with a given name.
|
|
2734
3925
|
|
|
2735
3926
|
Args:
|
|
2736
|
-
name (str):
|
|
3927
|
+
name (str):
|
|
3928
|
+
The name of the authentication handler
|
|
3929
|
+
show_error (bool, optional):
|
|
3930
|
+
Whether or not an error should be logged in case of a failed REST call.
|
|
3931
|
+
If False, then only a warning is logged. Defaults to True.
|
|
2737
3932
|
|
|
2738
3933
|
Returns:
|
|
2739
|
-
dict | None:
|
|
3934
|
+
dict | None:
|
|
3935
|
+
The auth handler dictionary, or None in case of an error.
|
|
2740
3936
|
|
|
2741
|
-
|
|
3937
|
+
Example:
|
|
2742
3938
|
{
|
|
2743
3939
|
'_name': 'Salesforce',
|
|
2744
3940
|
'_id': 'Salesforce',
|
|
@@ -2775,8 +3971,10 @@ class OTDS:
|
|
|
2775
3971
|
|
|
2776
3972
|
request_url = "{}/{}".format(self.auth_handler_url(), name)
|
|
2777
3973
|
|
|
2778
|
-
logger.debug(
|
|
2779
|
-
"Getting authentication handler -> '%s'; calling -> %s",
|
|
3974
|
+
self.logger.debug(
|
|
3975
|
+
"Getting authentication handler -> '%s'; calling -> %s",
|
|
3976
|
+
name,
|
|
3977
|
+
request_url,
|
|
2780
3978
|
)
|
|
2781
3979
|
|
|
2782
3980
|
return self.do_request(
|
|
@@ -2803,26 +4001,40 @@ class OTDS:
|
|
|
2803
4001
|
auth_principal_attributes: list | None = None,
|
|
2804
4002
|
nameid_format: str = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
|
|
2805
4003
|
) -> dict | None:
|
|
2806
|
-
"""Add a new SAML authentication handler
|
|
4004
|
+
"""Add a new SAML authentication handler.
|
|
2807
4005
|
|
|
2808
4006
|
Args:
|
|
2809
|
-
name (str):
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
4007
|
+
name (str):
|
|
4008
|
+
The name of the new authentication handler.
|
|
4009
|
+
description (str):
|
|
4010
|
+
The description of the new authentication handler.
|
|
4011
|
+
scope (str):
|
|
4012
|
+
The name of the user partition (to define a scope of the auth handler)
|
|
4013
|
+
provider_name (str):
|
|
4014
|
+
The description of the new authentication handler.
|
|
4015
|
+
saml_url (str):
|
|
4016
|
+
The SAML URL.
|
|
4017
|
+
otds_sp_endpoint (str):
|
|
4018
|
+
The external(!) service provider URL of OTDS.
|
|
4019
|
+
enabled (bool, optional):
|
|
4020
|
+
Defines if the handler should be enabled or disabled. Default is True = enabled.
|
|
4021
|
+
priority (int, optional):
|
|
4022
|
+
Priority of the Authentical Handler (compared to others). Default is 5
|
|
4023
|
+
active_by_default (bool, optional):
|
|
4024
|
+
Defines whether OTDS should redirect immediately to provider page
|
|
4025
|
+
(not showing the OTDS login at all).
|
|
4026
|
+
auth_principal_attributes (list, optional):
|
|
4027
|
+
List of Authentication principal attributes
|
|
4028
|
+
nameid_format (str, optional):
|
|
4029
|
+
Specifies which NameID format supported by the identity provider
|
|
4030
|
+
contains the desired user identifier. The value in this identifier
|
|
4031
|
+
must correspond to the value of the user attribute specified for the
|
|
4032
|
+
authentication principal attribute.
|
|
4033
|
+
|
|
2824
4034
|
Returns:
|
|
2825
|
-
dict
|
|
4035
|
+
dict | None:
|
|
4036
|
+
Request response (dictionary) or None if the REST call fails.
|
|
4037
|
+
|
|
2826
4038
|
"""
|
|
2827
4039
|
|
|
2828
4040
|
if auth_principal_attributes is None:
|
|
@@ -3110,7 +4322,7 @@ class OTDS:
|
|
|
3110
4322
|
|
|
3111
4323
|
request_url = self.auth_handler_url()
|
|
3112
4324
|
|
|
3113
|
-
logger.debug(
|
|
4325
|
+
self.logger.debug(
|
|
3114
4326
|
"Adding SAML auth handler -> '%s' ('%s'); calling -> %s",
|
|
3115
4327
|
name,
|
|
3116
4328
|
description,
|
|
@@ -3137,23 +4349,33 @@ class OTDS:
|
|
|
3137
4349
|
enabled: bool = True,
|
|
3138
4350
|
priority: int = 10,
|
|
3139
4351
|
auth_principal_attributes: list | None = None,
|
|
3140
|
-
):
|
|
3141
|
-
"""Add a new SAP authentication handler
|
|
4352
|
+
) -> dict | None:
|
|
4353
|
+
"""Add a new SAP authentication handler.
|
|
3142
4354
|
|
|
3143
4355
|
Args:
|
|
3144
|
-
name (str):
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
4356
|
+
name (str):
|
|
4357
|
+
The name of the new authentication handler.
|
|
4358
|
+
description (str):
|
|
4359
|
+
The description of the new authentication handler.
|
|
4360
|
+
scope (str):
|
|
4361
|
+
The name of the user partition (to define a scope of the auth handler)
|
|
4362
|
+
certificate_file (str):
|
|
4363
|
+
A fully qualified file name (with path) to the certificate file.
|
|
4364
|
+
certificate_password (str):
|
|
4365
|
+
The password of the certificate.
|
|
4366
|
+
enabled (bool, optional):
|
|
4367
|
+
Defines if the handler should be enabled or disabled. Default is True = enabled.
|
|
4368
|
+
priority (int, optional):
|
|
4369
|
+
Priority of the Authentical Handler (compared to others). Default is 10.
|
|
4370
|
+
auth_principal_attributes (list, optional):
|
|
4371
|
+
List of Authentication principal attributes.
|
|
4372
|
+
|
|
3152
4373
|
Returns:
|
|
3153
|
-
Request response (json) or None if the REST call fails.
|
|
4374
|
+
dict | None: Request response (json) or None if the REST call fails.
|
|
4375
|
+
|
|
3154
4376
|
"""
|
|
3155
4377
|
|
|
3156
|
-
# Avoid linter warning W0102:
|
|
4378
|
+
# Avoid linter warning W0102 by establishing the default value inside the method:
|
|
3157
4379
|
if auth_principal_attributes is None:
|
|
3158
4380
|
auth_principal_attributes = ["oTExternalID1"]
|
|
3159
4381
|
|
|
@@ -3176,7 +4398,7 @@ class OTDS:
|
|
|
3176
4398
|
"_fileName": False,
|
|
3177
4399
|
"_fileExtensions": None,
|
|
3178
4400
|
"_value": os.path.basename(
|
|
3179
|
-
certificate_file
|
|
4401
|
+
certificate_file,
|
|
3180
4402
|
), # "TM6_Sandbox.pse" - file name only
|
|
3181
4403
|
"_allowedValues": None,
|
|
3182
4404
|
"_confidential": False,
|
|
@@ -3214,7 +4436,7 @@ class OTDS:
|
|
|
3214
4436
|
# 2. Create the auth handler in OTDS
|
|
3215
4437
|
request_url = self.auth_handler_url()
|
|
3216
4438
|
|
|
3217
|
-
logger.debug(
|
|
4439
|
+
self.logger.debug(
|
|
3218
4440
|
"Adding SAP auth handler -> '%s' ('%s'); calling -> %s",
|
|
3219
4441
|
name,
|
|
3220
4442
|
description,
|
|
@@ -3235,21 +4457,21 @@ class OTDS:
|
|
|
3235
4457
|
# 3. Upload the certificate file:
|
|
3236
4458
|
|
|
3237
4459
|
# Check that the certificate (PSE) file is readable:
|
|
3238
|
-
logger.debug("Reading certificate file -> '%s'...", certificate_file)
|
|
4460
|
+
self.logger.debug("Reading certificate file -> '%s'...", certificate_file)
|
|
3239
4461
|
try:
|
|
3240
4462
|
# PSE files are binary - so we need to open with "rb":
|
|
3241
4463
|
with open(certificate_file, "rb") as cert_file:
|
|
3242
4464
|
cert_content = cert_file.read()
|
|
3243
4465
|
if not cert_content:
|
|
3244
|
-
logger.error(
|
|
3245
|
-
"No data in certificate file -> '%s'",
|
|
4466
|
+
self.logger.error(
|
|
4467
|
+
"No data in certificate file -> '%s'",
|
|
4468
|
+
certificate_file,
|
|
3246
4469
|
)
|
|
3247
4470
|
return None
|
|
3248
|
-
except
|
|
3249
|
-
logger.error(
|
|
3250
|
-
"Unable to open certificate file -> '%s'
|
|
4471
|
+
except OSError:
|
|
4472
|
+
self.logger.error(
|
|
4473
|
+
"Unable to open certificate file -> '%s'!",
|
|
3251
4474
|
certificate_file,
|
|
3252
|
-
exception.strerror,
|
|
3253
4475
|
)
|
|
3254
4476
|
return None
|
|
3255
4477
|
|
|
@@ -3257,56 +4479,49 @@ class OTDS:
|
|
|
3257
4479
|
# base64 encoded we will decode it and write it back into the same file
|
|
3258
4480
|
try:
|
|
3259
4481
|
# If file is not base64 encoded the next statement will throw an exception
|
|
3260
|
-
# (this is good)
|
|
3261
4482
|
cert_content_decoded = base64.b64decode(cert_content, validate=True)
|
|
3262
4483
|
cert_content_encoded = base64.b64encode(cert_content_decoded).decode(
|
|
3263
|
-
"utf-8"
|
|
4484
|
+
"utf-8",
|
|
3264
4485
|
)
|
|
3265
4486
|
if cert_content_encoded == cert_content.decode("utf-8"):
|
|
3266
|
-
logger.debug(
|
|
3267
|
-
"Certificate file -> '%s' is base64 encoded",
|
|
4487
|
+
self.logger.debug(
|
|
4488
|
+
"Certificate file -> '%s' is base64 encoded",
|
|
4489
|
+
certificate_file,
|
|
3268
4490
|
)
|
|
3269
4491
|
cert_file_encoded = True
|
|
3270
4492
|
else:
|
|
3271
4493
|
cert_file_encoded = False
|
|
3272
4494
|
except TypeError:
|
|
3273
|
-
logger.debug(
|
|
3274
|
-
"Certificate file -> '%s' is not base64 encoded",
|
|
4495
|
+
self.logger.debug(
|
|
4496
|
+
"Certificate file -> '%s' is not base64 encoded",
|
|
4497
|
+
certificate_file,
|
|
3275
4498
|
)
|
|
3276
4499
|
cert_file_encoded = False
|
|
3277
4500
|
|
|
3278
4501
|
if cert_file_encoded:
|
|
3279
|
-
certificate_file =
|
|
3280
|
-
logger.debug(
|
|
4502
|
+
certificate_file = os.path.join(tempfile.gettempdir(), os.path.basename(certificate_file))
|
|
4503
|
+
self.logger.debug(
|
|
4504
|
+
"Writing decoded certificate file -> %s...",
|
|
4505
|
+
certificate_file,
|
|
4506
|
+
)
|
|
3281
4507
|
try:
|
|
3282
4508
|
# PSE files need to be binary - so we need to open with "wb":
|
|
3283
4509
|
with open(certificate_file, "wb") as cert_file:
|
|
3284
4510
|
cert_file.write(base64.b64decode(cert_content))
|
|
3285
|
-
except
|
|
3286
|
-
logger.error(
|
|
3287
|
-
"Failed writing to file -> '%s'
|
|
4511
|
+
except OSError:
|
|
4512
|
+
self.logger.error(
|
|
4513
|
+
"Failed writing to file -> '%s'!",
|
|
3288
4514
|
certificate_file,
|
|
3289
|
-
exception.strerror,
|
|
3290
4515
|
)
|
|
3291
4516
|
return None
|
|
3292
4517
|
|
|
3293
4518
|
auth_handler_post_data = {
|
|
3294
|
-
"file1_property": "com.opentext.otds.as.drivers.sapssoext.certificate1"
|
|
3295
|
-
}
|
|
3296
|
-
|
|
3297
|
-
# It is important to send the file pointer and not the actual file content
|
|
3298
|
-
# otherwise the file is send base64 encoded which we don't want:
|
|
3299
|
-
auth_handler_post_files = {
|
|
3300
|
-
"file1": (
|
|
3301
|
-
os.path.basename(certificate_file),
|
|
3302
|
-
open(certificate_file, "rb"),
|
|
3303
|
-
"application/octet-stream",
|
|
3304
|
-
)
|
|
4519
|
+
"file1_property": "com.opentext.otds.as.drivers.sapssoext.certificate1",
|
|
3305
4520
|
}
|
|
3306
4521
|
|
|
3307
4522
|
request_url = self.auth_handler_url() + "/" + name + "/files"
|
|
3308
4523
|
|
|
3309
|
-
logger.debug(
|
|
4524
|
+
self.logger.debug(
|
|
3310
4525
|
"Uploading certificate file -> '%s' for SAP auth handler -> '%s' ('%s'); calling -> %s",
|
|
3311
4526
|
certificate_file,
|
|
3312
4527
|
name,
|
|
@@ -3314,18 +4529,30 @@ class OTDS:
|
|
|
3314
4529
|
request_url,
|
|
3315
4530
|
)
|
|
3316
4531
|
|
|
3317
|
-
#
|
|
3318
|
-
#
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
4532
|
+
# It is important to send the file pointer and not the actual file content
|
|
4533
|
+
# otherwise the file is sent base64 encoded, which we don't want:
|
|
4534
|
+
with open(certificate_file, "rb") as file_obj:
|
|
4535
|
+
auth_handler_post_files = {
|
|
4536
|
+
"file1": (
|
|
4537
|
+
os.path.basename(certificate_file),
|
|
4538
|
+
file_obj,
|
|
4539
|
+
"application/octet-stream",
|
|
4540
|
+
),
|
|
4541
|
+
}
|
|
4542
|
+
|
|
4543
|
+
# It is important to NOT pass the headers parameter here!
|
|
4544
|
+
# Basically, if you specify a files parameter (a dictionary),
|
|
4545
|
+
# then requests will send a multipart/form-data POST automatically:
|
|
4546
|
+
response = requests.post(
|
|
4547
|
+
url=request_url,
|
|
4548
|
+
data=auth_handler_post_data,
|
|
4549
|
+
files=auth_handler_post_files,
|
|
4550
|
+
cookies=self.cookie(),
|
|
4551
|
+
timeout=REQUEST_TIMEOUT,
|
|
4552
|
+
)
|
|
4553
|
+
|
|
3327
4554
|
if not response.ok:
|
|
3328
|
-
logger.error(
|
|
4555
|
+
self.logger.error(
|
|
3329
4556
|
"Failed to upload certificate file -> '%s' for SAP auth handler -> '%s'; error -> %s (%s)",
|
|
3330
4557
|
certificate_file,
|
|
3331
4558
|
name,
|
|
@@ -3354,28 +4581,44 @@ class OTDS:
|
|
|
3354
4581
|
priority: int = 10,
|
|
3355
4582
|
auth_principal_attributes: list | None = None,
|
|
3356
4583
|
) -> dict | None:
|
|
3357
|
-
"""Add a new OAuth authentication handler
|
|
3358
|
-
|
|
3359
|
-
Args:
|
|
3360
|
-
name (str):
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
4584
|
+
"""Add a new OAuth authentication handler.
|
|
4585
|
+
|
|
4586
|
+
Args:
|
|
4587
|
+
name (str):
|
|
4588
|
+
The name of the new authentication handler.
|
|
4589
|
+
description (str):
|
|
4590
|
+
The description of the new authentication handler.
|
|
4591
|
+
scope (str):
|
|
4592
|
+
The name of the user partition (to define a scope of the auth handler).
|
|
4593
|
+
provider_name (str):
|
|
4594
|
+
The name of the authentication provider. This name is displayed on the login page.
|
|
4595
|
+
client_id (str):
|
|
4596
|
+
The client ID.
|
|
4597
|
+
client_secret (str):
|
|
4598
|
+
The client secret.
|
|
4599
|
+
active_by_default (bool, optional):
|
|
4600
|
+
Defines, whether to activate this handler for any request to the OTDS login page.
|
|
4601
|
+
If True, any login request to the OTDS login page will be redirected to this OAuth provider.
|
|
4602
|
+
If False, the user has to select the provider on the login page.
|
|
4603
|
+
authorization_endpoint (str, optional):
|
|
4604
|
+
The URL to redirect the browser to for authentication.
|
|
4605
|
+
It is used to retrieve the authorization code or an OIDC id_token.
|
|
4606
|
+
token_endpoint (str, optional):
|
|
4607
|
+
The URL from which to retrieve the access token.
|
|
4608
|
+
Not strictly required with OpenID Connect if using the implicit flow.
|
|
4609
|
+
scope_string (str, optional):
|
|
4610
|
+
Space delimited scope values to send. Include 'openid' to use OpenID Connect.
|
|
4611
|
+
enabled (bool, optional):
|
|
4612
|
+
Defines if the handler should be enabled or disabled. Default is True = enabled.
|
|
4613
|
+
priority (int, optional):
|
|
4614
|
+
Priority of the Authentical Handler (compared to others). Default is 5.
|
|
4615
|
+
auth_principal_attributes (list, optional):
|
|
4616
|
+
List of Authentication principal attributes.
|
|
4617
|
+
|
|
4618
|
+
Returns:
|
|
4619
|
+
dict | None:
|
|
4620
|
+
Request response (dictionary) or None if the REST call fails.
|
|
4621
|
+
|
|
3379
4622
|
"""
|
|
3380
4623
|
|
|
3381
4624
|
# Avoid linter warning W0102:
|
|
@@ -3722,7 +4965,7 @@ class OTDS:
|
|
|
3722
4965
|
|
|
3723
4966
|
request_url = self.auth_handler_url()
|
|
3724
4967
|
|
|
3725
|
-
logger.debug(
|
|
4968
|
+
self.logger.debug(
|
|
3726
4969
|
"Adding OAuth auth handler -> '%s' ('%s'); calling -> %s",
|
|
3727
4970
|
name,
|
|
3728
4971
|
description,
|
|
@@ -3740,24 +4983,29 @@ class OTDS:
|
|
|
3740
4983
|
# end method definition
|
|
3741
4984
|
|
|
3742
4985
|
def consolidate(self, resource_name: str) -> bool:
|
|
3743
|
-
"""Consolidate an OTDS resource
|
|
4986
|
+
"""Consolidate an OTDS resource.
|
|
3744
4987
|
|
|
3745
4988
|
Args:
|
|
3746
|
-
resource_name (str):
|
|
4989
|
+
resource_name (str):
|
|
4990
|
+
The name of the resource to be consolidated.
|
|
4991
|
+
|
|
3747
4992
|
Returns:
|
|
3748
|
-
bool:
|
|
4993
|
+
bool:
|
|
4994
|
+
True, if the consolidation succeeded or False if it failed.
|
|
4995
|
+
|
|
3749
4996
|
"""
|
|
3750
4997
|
|
|
3751
4998
|
resource = self.get_resource(resource_name)
|
|
3752
4999
|
if not resource:
|
|
3753
|
-
logger.error(
|
|
3754
|
-
"Resource -> '%s' not found - cannot consolidate",
|
|
5000
|
+
self.logger.error(
|
|
5001
|
+
"Resource -> '%s' not found - cannot consolidate",
|
|
5002
|
+
resource_name,
|
|
3755
5003
|
)
|
|
3756
5004
|
return False
|
|
3757
5005
|
|
|
3758
5006
|
resource_dn = resource["resourceDN"]
|
|
3759
5007
|
if not resource_dn:
|
|
3760
|
-
logger.error("Resource DN is empty - cannot consolidate")
|
|
5008
|
+
self.logger.error("Resource DN is empty - cannot consolidate")
|
|
3761
5009
|
return False
|
|
3762
5010
|
|
|
3763
5011
|
consolidation_post_body_json = {
|
|
@@ -3769,8 +5017,8 @@ class OTDS:
|
|
|
3769
5017
|
|
|
3770
5018
|
request_url = "{}".format(self.consolidation_url())
|
|
3771
5019
|
|
|
3772
|
-
logger.debug(
|
|
3773
|
-
"Consolidation of resource -> %s (%s); calling -> %s",
|
|
5020
|
+
self.logger.debug(
|
|
5021
|
+
"Consolidation of resource -> '%s' (%s); calling -> %s",
|
|
3774
5022
|
resource_name,
|
|
3775
5023
|
resource_dn,
|
|
3776
5024
|
request_url,
|
|
@@ -3782,15 +5030,12 @@ class OTDS:
|
|
|
3782
5030
|
json_data=consolidation_post_body_json,
|
|
3783
5031
|
timeout=None,
|
|
3784
5032
|
failure_message="Failed to consolidate resource -> '{}'".format(
|
|
3785
|
-
resource_name
|
|
5033
|
+
resource_name,
|
|
3786
5034
|
),
|
|
3787
5035
|
parse_request_response=False,
|
|
3788
5036
|
)
|
|
3789
5037
|
|
|
3790
|
-
|
|
3791
|
-
return True
|
|
3792
|
-
|
|
3793
|
-
return False
|
|
5038
|
+
return bool(response and response.ok)
|
|
3794
5039
|
|
|
3795
5040
|
# end method definition
|
|
3796
5041
|
|
|
@@ -3800,15 +5045,20 @@ class OTDS:
|
|
|
3800
5045
|
allow_impersonation: bool = True,
|
|
3801
5046
|
impersonation_list: list | None = None,
|
|
3802
5047
|
) -> bool:
|
|
3803
|
-
"""Configure impersonation for an OTDS resource
|
|
5048
|
+
"""Configure impersonation for an OTDS resource.
|
|
3804
5049
|
|
|
3805
5050
|
Args:
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
5051
|
+
resource_name (str):
|
|
5052
|
+
Name of the resource to configure impersonation for.
|
|
5053
|
+
allow_impersonation (bool, optional):
|
|
5054
|
+
Whether to turn on or off impersonation (default = True)
|
|
5055
|
+
impersonation_list (list, optional):
|
|
5056
|
+
A list of users to restrict it to (default = empty list = all users)
|
|
5057
|
+
|
|
3810
5058
|
Returns:
|
|
3811
|
-
|
|
5059
|
+
bool:
|
|
5060
|
+
True if the impersonation setting succeeded or False if it failed.
|
|
5061
|
+
|
|
3812
5062
|
"""
|
|
3813
5063
|
|
|
3814
5064
|
# Avoid linter warning W0102:
|
|
@@ -3822,7 +5072,7 @@ class OTDS:
|
|
|
3822
5072
|
|
|
3823
5073
|
request_url = "{}/{}/impersonation".format(self.resource_url(), resource_name)
|
|
3824
5074
|
|
|
3825
|
-
logger.debug(
|
|
5075
|
+
self.logger.debug(
|
|
3826
5076
|
"Impersonation settings for resource -> '%s'; calling -> %s",
|
|
3827
5077
|
resource_name,
|
|
3828
5078
|
request_url,
|
|
@@ -3834,15 +5084,12 @@ class OTDS:
|
|
|
3834
5084
|
json_data=impersonation_put_body_json,
|
|
3835
5085
|
timeout=None,
|
|
3836
5086
|
failure_message="Failed to set impersonation for resource -> '{}'".format(
|
|
3837
|
-
resource_name
|
|
5087
|
+
resource_name,
|
|
3838
5088
|
),
|
|
3839
5089
|
parse_request_response=False,
|
|
3840
5090
|
)
|
|
3841
5091
|
|
|
3842
|
-
|
|
3843
|
-
return True
|
|
3844
|
-
|
|
3845
|
-
return False
|
|
5092
|
+
return bool(response and response.ok)
|
|
3846
5093
|
|
|
3847
5094
|
# end method definition
|
|
3848
5095
|
|
|
@@ -3852,17 +5099,23 @@ class OTDS:
|
|
|
3852
5099
|
allow_impersonation: bool = True,
|
|
3853
5100
|
impersonation_list: list | None = None,
|
|
3854
5101
|
) -> bool:
|
|
3855
|
-
"""Configure impersonation for an OTDS OAuth Client
|
|
5102
|
+
"""Configure impersonation for an OTDS OAuth Client.
|
|
3856
5103
|
|
|
3857
5104
|
Args:
|
|
3858
|
-
client_id (str):
|
|
3859
|
-
|
|
3860
|
-
|
|
5105
|
+
client_id (str):
|
|
5106
|
+
The ID of the OAuth Client to configure impersonation for.
|
|
5107
|
+
allow_impersonation (bool | None, optional):
|
|
5108
|
+
Defines whether to turn on or off impersonation (default = True).
|
|
5109
|
+
impersonation_list (list | None, optional):
|
|
5110
|
+
A list of users to restrict it to; (default = empty list = all users).
|
|
5111
|
+
|
|
3861
5112
|
Returns:
|
|
3862
|
-
bool:
|
|
5113
|
+
bool:
|
|
5114
|
+
True if the impersonation setting succeeded or False if it failed.
|
|
5115
|
+
|
|
3863
5116
|
"""
|
|
3864
5117
|
|
|
3865
|
-
# Avoid linter warning W0102:
|
|
5118
|
+
# Avoid linter warning W0102 by establishing the default inside the method:
|
|
3866
5119
|
if impersonation_list is None:
|
|
3867
5120
|
impersonation_list = []
|
|
3868
5121
|
|
|
@@ -3873,7 +5126,7 @@ class OTDS:
|
|
|
3873
5126
|
|
|
3874
5127
|
request_url = "{}/{}/impersonation".format(self.oauth_client_url(), client_id)
|
|
3875
5128
|
|
|
3876
|
-
logger.debug(
|
|
5129
|
+
self.logger.debug(
|
|
3877
5130
|
"Impersonation settings for OAuth Client -> '%s'; calling -> %s",
|
|
3878
5131
|
client_id,
|
|
3879
5132
|
request_url,
|
|
@@ -3885,27 +5138,26 @@ class OTDS:
|
|
|
3885
5138
|
json_data=impersonation_put_body_json,
|
|
3886
5139
|
timeout=None,
|
|
3887
5140
|
failure_message="Failed to set impersonation for OAuth Client -> '{}'".format(
|
|
3888
|
-
client_id
|
|
5141
|
+
client_id,
|
|
3889
5142
|
),
|
|
3890
5143
|
parse_request_response=False,
|
|
3891
5144
|
)
|
|
3892
5145
|
|
|
3893
|
-
|
|
3894
|
-
return True
|
|
3895
|
-
|
|
3896
|
-
return False
|
|
5146
|
+
return bool(response and response.ok)
|
|
3897
5147
|
|
|
3898
5148
|
# end method definition
|
|
3899
5149
|
|
|
3900
|
-
def get_password_policy(self):
|
|
3901
|
-
"""Get the global password policy
|
|
5150
|
+
def get_password_policy(self) -> dict | None:
|
|
5151
|
+
"""Get the global password policy.
|
|
3902
5152
|
|
|
3903
5153
|
Args:
|
|
3904
5154
|
None
|
|
5155
|
+
|
|
3905
5156
|
Returns:
|
|
3906
|
-
dict
|
|
5157
|
+
dict | None:
|
|
5158
|
+
Request response or None if the REST call fails.
|
|
3907
5159
|
|
|
3908
|
-
|
|
5160
|
+
Example:
|
|
3909
5161
|
{
|
|
3910
5162
|
'passwordHistoryMaximumCount': 3,
|
|
3911
5163
|
'daysBeforeNewPasswordMayBeChanged': 1,
|
|
@@ -3924,11 +5176,12 @@ class OTDS:
|
|
|
3924
5176
|
'blockCommonPassword': False
|
|
3925
5177
|
...
|
|
3926
5178
|
}
|
|
5179
|
+
|
|
3927
5180
|
"""
|
|
3928
5181
|
|
|
3929
5182
|
request_url = "{}/passwordpolicy".format(self.config()["systemConfigUrl"])
|
|
3930
5183
|
|
|
3931
|
-
logger.debug("Getting password policy; calling -> %s", request_url)
|
|
5184
|
+
self.logger.debug("Getting password policy; calling -> %s", request_url)
|
|
3932
5185
|
|
|
3933
5186
|
return self.do_request(
|
|
3934
5187
|
url=request_url,
|
|
@@ -3940,13 +5193,14 @@ class OTDS:
|
|
|
3940
5193
|
# end method definition
|
|
3941
5194
|
|
|
3942
5195
|
def update_password_policy(self, update_values: dict) -> bool:
|
|
3943
|
-
"""Update the global password policy
|
|
5196
|
+
"""Update the global password policy.
|
|
3944
5197
|
|
|
3945
5198
|
Args:
|
|
3946
|
-
update_values (dict):
|
|
3947
|
-
|
|
5199
|
+
update_values (dict):
|
|
5200
|
+
New values for selected settings.
|
|
5201
|
+
A value of 0 means the settings is deactivated.
|
|
3948
5202
|
|
|
3949
|
-
|
|
5203
|
+
Example:
|
|
3950
5204
|
{
|
|
3951
5205
|
'passwordHistoryMaximumCount': 3,
|
|
3952
5206
|
'daysBeforeNewPasswordMayBeChanged': 1,
|
|
@@ -3965,15 +5219,17 @@ class OTDS:
|
|
|
3965
5219
|
'blockCommonPassword': False
|
|
3966
5220
|
...
|
|
3967
5221
|
}
|
|
5222
|
+
|
|
3968
5223
|
Returns:
|
|
3969
|
-
bool:
|
|
3970
|
-
|
|
5224
|
+
bool:
|
|
5225
|
+
True if the REST call succeeds, otherwise False. We use a boolean return
|
|
5226
|
+
value as the response of the REST call does not have meaningful content.
|
|
3971
5227
|
|
|
3972
5228
|
"""
|
|
3973
5229
|
|
|
3974
5230
|
request_url = "{}/passwordpolicy".format(self.config()["systemConfigUrl"])
|
|
3975
5231
|
|
|
3976
|
-
logger.debug(
|
|
5232
|
+
self.logger.debug(
|
|
3977
5233
|
"Update password policy with these new values -> %s; calling -> %s",
|
|
3978
5234
|
str(update_values),
|
|
3979
5235
|
request_url,
|
|
@@ -3985,14 +5241,11 @@ class OTDS:
|
|
|
3985
5241
|
json_data=update_values,
|
|
3986
5242
|
timeout=None,
|
|
3987
5243
|
failure_message="Failed to update password policy with values -> {}".format(
|
|
3988
|
-
update_values
|
|
5244
|
+
update_values,
|
|
3989
5245
|
),
|
|
3990
5246
|
parse_request_response=False,
|
|
3991
5247
|
)
|
|
3992
5248
|
|
|
3993
|
-
|
|
3994
|
-
return True
|
|
3995
|
-
|
|
3996
|
-
return False
|
|
5249
|
+
return bool(response and response.ok)
|
|
3997
5250
|
|
|
3998
5251
|
# end method definition
|