-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathcomanage_utils.py
256 lines (197 loc) · 8.29 KB
/
comanage_utils.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
#!/usr/bin/env python3
import os
import re
import json
import time
import urllib.error
import urllib.request
from ldap3 import Server, Connection, ALL, ALL_ATTRIBUTES, SAFE_SYNC
#PRODUCTION VALUES
PRODUCTION_ENDPOINT = "https://registry.cilogon.org/registry/"
PRODUCTION_LDAP_SERVER = "ldaps://ldap.cilogon.org"
PRODUCTION_LDAP_USER = "uid=readonly_user,ou=system,o=OSG,o=CO,dc=cilogon,dc=org"
PRODUCTION_OSG_CO_ID = 7
PRODUCTION_UNIX_CLUSTER_ID = 1
PRODUCTION_LDAP_TARGET_ID = 6
#TEST VALUES
TEST_ENDPOINT = "https://registry-test.cilogon.org/registry/"
TEST_LDAP_SERVER = "ldaps://ldap-test.cilogon.org"
TEST_LDAP_USER ="uid=registry_user,ou=system,o=OSG,o=CO,dc=cilogon,dc=org"
TEST_OSG_CO_ID = 8
TEST_UNIX_CLUSTER_ID = 10
TEST_LDAP_TARGET_ID = 9
# Value for the base of the exponential backoff
TIMEOUT_BASE = 5
MAX_ATTEMPTS = 5
GET = "GET"
PUT = "PUT"
POST = "POST"
DELETE = "DELETE"
#Exceptions
class Error(Exception):
"""Base exception class for all exceptions defined"""
pass
class URLRequestError(Error):
"""Class for exceptions due to not being able to fulfill a URLRequest"""
pass
def getpw(user, passfd, passfile):
if ":" in user:
user, pw = user.split(":", 1)
elif passfd is not None:
pw = os.fdopen(passfd).readline().rstrip("\n")
elif passfile is not None:
pw = open(passfile).readline().rstrip("\n")
elif "PASS" in os.environ:
pw = os.environ["PASS"]
else:
raise PermissionError
#when script needs to say PASS required, raise a permission error
#usage("PASS required")
return user, pw
def mkauthstr(user, passwd):
from base64 import encodebytes
raw_authstr = "%s:%s" % (user, passwd)
return encodebytes(raw_authstr.encode()).decode().replace("\n", "")
def mkrequest(method, target, data, endpoint, authstr, **kw):
url = os.path.join(endpoint, target)
if kw:
url += "?" + "&".join("{}={}".format(k,v) for k,v in kw.items())
req = urllib.request.Request(url, json.dumps(data).encode("utf-8"))
req.add_header("Authorization", "Basic %s" % authstr)
req.add_header("Content-Type", "application/json")
req.get_method = lambda: method
return req
def call_api(target, endpoint, authstr, **kw):
return call_api2(GET, target, endpoint, authstr, **kw)
def call_api2(method, target, endpoint, authstr, **kw):
return call_api3(method, target, data=None, endpoint=endpoint, authstr=authstr, **kw)
def call_api3(method, target, data, endpoint, authstr, **kw):
req = mkrequest(method, target, data, endpoint, authstr, **kw)
req_attempts = 0
current_timeout = TIMEOUT_BASE
total_timeout = 0
payload = None
while req_attempts < MAX_ATTEMPTS:
try:
resp = urllib.request.urlopen(req, timeout=current_timeout)
# exception catching, mainly for request timeouts, "Service Temporarily Unavailable" (Rate limiting), and DNS failures.
except urllib.error.URLError as exception:
req_attempts += 1
if req_attempts >= MAX_ATTEMPTS:
raise URLRequestError(
"Exception raised after maximum number of retries reached after total backoff of " +
f"{total_timeout} seconds. Retries: {req_attempts}. "
+ f"Exception reason: {exception}.\n Request: {req.full_url}"
)
time.sleep(current_timeout)
total_timeout += current_timeout
current_timeout *= TIMEOUT_BASE
else:
payload = resp.read()
break
return json.loads(payload) if payload else None
def get_osg_co_groups(osg_co_id, endpoint, authstr):
return call_api("co_groups.json", endpoint, authstr, coid=osg_co_id)
def get_co_group_identifiers(gid, endpoint, authstr):
return call_api("identifiers.json", endpoint, authstr, cogroupid=gid)
def get_co_group_members(gid, endpoint, authstr):
return call_api("co_group_members.json", endpoint, authstr, cogroupid=gid)
def get_co_person_identifiers(pid, endpoint, authstr):
return call_api("identifiers.json", endpoint, authstr, copersonid=pid)
def get_co_group(gid, endpoint, authstr):
resp_data = call_api("co_groups/%s.json" % gid, endpoint, authstr)
grouplist = get_datalist(resp_data, "CoGroups")
if not grouplist:
raise RuntimeError("No such CO Group Id: %s" % gid)
return grouplist[0]
def get_identifier(id_, endpoint, authstr):
resp_data = call_api("identifiers/%s.json" % id_, endpoint, authstr)
idfs = get_datalist(resp_data, "Identifiers")
if not idfs:
raise RuntimeError("No such Identifier Id: %s" % id_)
return idfs[0]
def get_unix_cluster_groups(ucid, endpoint, authstr):
return call_api("unix_cluster/unix_cluster_groups.json", endpoint, authstr, unix_cluster_id=ucid)
def get_unix_cluster_groups_ids(ucid, endpoint, authstr):
unix_cluster_groups = get_unix_cluster_groups(ucid, endpoint, authstr)
return set(group["CoGroupId"] for group in unix_cluster_groups["UnixClusterGroups"])
def delete_identifier(id_, endpoint, authstr):
return call_api2(DELETE, "identifiers/%s.json" % id_, endpoint, authstr)
def get_datalist(data, listname):
return data[listname] if data else []
def get_ldap_groups(ldap_server, ldap_user, ldap_authtok):
ldap_group_osggids = set()
server = Server(ldap_server, get_info=ALL)
connection = Connection(server, ldap_user, ldap_authtok, client_strategy=SAFE_SYNC, auto_bind=True)
_, _, response, _ = connection.search("ou=groups,o=OSG,o=CO,dc=cilogon,dc=org", "(cn=*)", attributes=ALL_ATTRIBUTES)
for group in response:
ldap_group_osggids.add(group["attributes"]["gidNumber"])
return ldap_group_osggids
def identifier_from_list(id_list, id_type):
id_type_list = [id["Type"] for id in id_list]
try:
id_index = id_type_list.index(id_type)
return id_list[id_index]["Identifier"]
except ValueError:
return None
def identifier_matches(id_list, id_type, regex_string):
pattern = re.compile(regex_string)
value = identifier_from_list(id_list, id_type)
return (value is not None) and (pattern.match(value) is not None)
def rename_co_group(gid, group, newname, endpoint, authstr):
# minimal edit CoGroup Request includes Name+CoId+Status+Version
new_group_info = {
"Name" : newname,
"CoId" : group["CoId"],
"Status" : group["Status"],
"Version" : group["Version"]
}
data = {
"CoGroups" : [new_group_info],
"RequestType" : "CoGroups",
"Version" : "1.0"
}
return call_api3(PUT, "co_groups/%s.json" % gid, data, endpoint, authstr)
def add_identifier_to_group(gid, type, identifier_value, endpoint, authstr):
new_identifier_info = {
"Version": "1.0",
"Type": type,
"Identifier": identifier_value,
"Login": False,
"Person": {"Type": "Group", "Id": str(gid)},
"Status": "Active",
}
data = {
"RequestType": "Identifiers",
"Version": "1.0",
"Identifiers": [new_identifier_info],
}
return call_api3(POST, "identifiers.json", data, endpoint, authstr)
def add_unix_cluster_group(gid, ucid, endpoint, authstr):
data = {
"RequestType": "UnixClusterGroups",
"Version": "1.0",
"UnixClusterGroups": [{"Version": "1.0", "UnixClusterId": ucid, "CoGroupId": gid}],
}
return call_api3(POST, "unix_cluster/unix_cluster_groups.json", data, endpoint, authstr)
def provision_group(gid, provision_target, endpoint, authstr):
path = f"co_provisioning_targets/provision/{provision_target}/cogroupid:{gid}.json"
data = {
"RequestType" : "CoGroupProvisioning",
"Version" : "1.0",
"Synchronous" : True
}
return call_api3(POST, path, data, endpoint, authstr)
def provision_group_members(gid, prov_id, endpoint, authstr):
data = {
"RequestType" : "CoPersonProvisioning",
"Version" : "1.0",
"Synchronous" : True
}
responses = {}
for member in get_co_group_members(gid, endpoint, authstr)["CoGroupMembers"]:
if member["Person"]["Type"] == "CO":
pid = member["Person"]["Id"]
path = f"co_provisioning_targets/provision/{prov_id}/copersonid:{pid}.json"
responses[pid] = call_api3(POST, path, data, endpoint, authstr)
return responses