Skip to content

Commit fccb47b

Browse files
Merge branch 'master' into INF-2987.mass-person-create-update-script
2 parents 4d4d8b2 + 74dbdae commit fccb47b

6 files changed

Lines changed: 278 additions & 117 deletions

File tree

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM library/python:3.7-alpine
1+
FROM library/python:3.9-alpine
22

33
LABEL maintainer OSG Software <help@opensciencegrid.org>
44

comanage_utils.py

Lines changed: 202 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,63 @@
44
import re
55
import json
66
import time
7+
import configparser
78
import urllib.error
89
import urllib.request
9-
from ldap3 import Server, Connection, ALL, ALL_ATTRIBUTES, SAFE_SYNC
10+
from enum import Enum
11+
from pathlib import Path
12+
from ldap3 import Server, Connection, ALL, SAFE_SYNC, Tls
13+
from ldap3.core.exceptions import LDAPException
14+
from dataclasses import dataclass
1015

1116
#PRODUCTION VALUES
1217

13-
PRODUCTION_ENDPOINT = "https://registry.cilogon.org/registry/"
14-
PRODUCTION_LDAP_SERVER = "ldaps://ldap.cilogon.org"
15-
PRODUCTION_LDAP_USER = "uid=readonly_user,ou=system,o=OSG,o=CO,dc=cilogon,dc=org"
16-
PRODUCTION_OSG_CO_ID = 7
17-
PRODUCTION_UNIX_CLUSTER_ID = 1
18-
PRODUCTION_LDAP_TARGET_ID = 6
18+
# PRODUCTION_ENDPOINT = "https://registry.cilogon.org/registry/"
19+
# PRODUCTION_OSG_CO_ID = 7
20+
# PRODUCTION_UNIX_CLUSTER_ID = 1
21+
# PRODUCTION_LDAP_TARGET_ID = 6
1922

2023
#TEST VALUES
2124

22-
TEST_ENDPOINT = "https://registry-test.cilogon.org/registry/"
23-
TEST_LDAP_SERVER = "ldaps://ldap-test.cilogon.org"
24-
TEST_LDAP_USER ="uid=registry_user,ou=system,o=OSG,o=CO,dc=cilogon,dc=org"
25-
TEST_OSG_CO_ID = 8
26-
TEST_UNIX_CLUSTER_ID = 10
27-
TEST_LDAP_TARGET_ID = 9
25+
# TEST_ENDPOINT = "https://registry-test.cilogon.org/registry/"
26+
# TEST_OSG_CO_ID = 8
27+
# TEST_UNIX_CLUSTER_ID = 10
28+
# TEST_LDAP_TARGET_ID = 9
2829

2930
# Value for the base of the exponential backoff
3031
TIMEOUT_BASE = 5
3132
MAX_ATTEMPTS = 5
3233

3334
# HTTP return codes we shouldn't attempt to retry
3435
HTTP_NO_RETRY_CODES = {401, 403, 404, 405, 500}
36+
# LDAP Server Connection and Search Config, required keys
37+
class LDAP_CONFIG_KEYS(str, Enum):
38+
LDAP_Server_URL = "LDAPServerURl"
39+
LDAP_Search_Base = "SearchBase"
40+
LDAP_User = "User"
41+
LDAP_AuthTok_File = "AuthTokenFile"
42+
43+
def __str__(self):
44+
return f'{self.value}'
45+
46+
LDAP_CONFIG_USAGE_MESSAGE = f"""
47+
LDAP CONNECTION CONFIG:
48+
File at LDAP_CONFIG_PATH should be in ini format, servers will be attempted in descending order.
49+
An example section of this config file follows:
50+
51+
---
52+
53+
[human_server_name] # arbitrary human label for this server's config
54+
{LDAP_CONFIG_KEYS.LDAP_Server_URL} = ldaps://ldap-replica-1.osg.chtc.io # URL to reach this LDAP server from
55+
{LDAP_CONFIG_KEYS.LDAP_Search_Base} = dc=osg-htc,dc=org # LDAP Search Base
56+
{LDAP_CONFIG_KEYS.LDAP_User} = cn=readonly,ou=system,dc=osg-htc,dc=org # full LDAP user DN
57+
{LDAP_CONFIG_KEYS.LDAP_AuthTok_File} = /etc/ldap-secrets/osg-ldap/authtoken # file containing authtoken for access
58+
59+
---
60+
61+
Config file should contain one such section per LDAP server to communicate with.
62+
"""
63+
3564

3665
GET = "GET"
3766
PUT = "PUT"
@@ -43,11 +72,18 @@ class Error(Exception):
4372
"""Base exception class for all exceptions defined"""
4473
pass
4574

46-
4775
class URLRequestError(Error):
4876
"""Class for exceptions due to not being able to fulfill a URLRequest"""
4977
pass
5078

79+
class EmptyConfiguration(Error):
80+
"""Class for exceptions due to loading an empty Config file, or one where every section lacked the required keys"""
81+
pass
82+
83+
class NoLDAPResponse(Error):
84+
"""Class for exceptions due to being unable to get any request from any configured LDAP servers."""
85+
pass
86+
5187

5288
class HTTPRequestError(URLRequestError):
5389
"""Class for exceptions due to not being able to fulfill a HTTPRequest"""
@@ -81,6 +117,60 @@ def mkauthstr(user, passwd):
81117
return encodebytes(raw_authstr.encode()).decode().replace("\n", "")
82118

83119

120+
def get_ldap_authtok(ldap_authfile):
121+
if ldap_authfile is not None:
122+
ldap_authtok = open(ldap_authfile).readline().strip()
123+
else:
124+
raise PermissionError
125+
return ldap_authtok
126+
127+
128+
def read_ldap_conffile(ldap_conffile_path):
129+
config = configparser.ConfigParser(allow_no_value=True)
130+
131+
print(f"Attempting to read config from {ldap_conffile_path}")
132+
config.read(ldap_conffile_path)
133+
misconfigured_sections = list()
134+
for section in config.sections():
135+
for key in LDAP_CONFIG_KEYS:
136+
# All servers must have all required keys for operation
137+
if not config.has_option(section, key) or config.get(section, key) == "":
138+
print(f"Section \"{section}\": required key \"{key}\" missing, ignoring section.")
139+
misconfigured_sections.append(section)
140+
break
141+
# For-Else to only check key values if we know the required ones exist (i.e. we didn't break)
142+
else:
143+
# All server AuthTok files must exist, be files, and not be empty
144+
token_path = Path(config.get(section, LDAP_CONFIG_KEYS.LDAP_AuthTok_File))
145+
try:
146+
if not token_path.exists():
147+
print(f"Section \"{section}\": AuthToken File missing or non-file, ignoring section.")
148+
misconfigured_sections.append(section)
149+
continue
150+
if token_path.stat().st_size == 0 :
151+
print(f"Section \"{section}\": AuthToken File is empty, ignoring section.")
152+
misconfigured_sections.append(section)
153+
continue
154+
except OSError as e:
155+
print(f"Section \"{section}\": Exception raised while checking AuthTok File: {e}, skipping section.")
156+
misconfigured_sections.append(section)
157+
continue
158+
159+
for section in misconfigured_sections:
160+
print(f"Dropping section {section}")
161+
config.remove_section(section)
162+
163+
# if their are no servers in the file that have all required keys
164+
if len(config.sections()) == 0:
165+
#when script needs to say LDAP Config required, raise a EmptyConfiguration error
166+
#usage("LDAP Config File Required")
167+
raise EmptyConfiguration(
168+
f"Config file at {ldap_conffile_path} was empty or all sections lacked required keys."
169+
)
170+
print(f"Finished reading config from {ldap_conffile_path}")
171+
return config
172+
173+
84174
def mkrequest(method, target, data, endpoint, authstr, **kw):
85175
url = os.path.join(endpoint, target)
86176
if kw:
@@ -223,16 +313,109 @@ def get_datalist(data, listname):
223313
return data[listname] if data else []
224314

225315

226-
def get_ldap_groups(ldap_server, ldap_user, ldap_authtok):
316+
class LDAPServer:
317+
""" Wrapper class for LDAP searches. """
318+
server: Server = None
319+
connection: Connection = None
320+
321+
def __init__(self, ldap_server, ldap_user, ldap_authtok):
322+
self.server = Server(ldap_server, get_info=ALL)
323+
self.connection = Connection(self.server, ldap_user, ldap_authtok, client_strategy=SAFE_SYNC, auto_bind=True)
324+
325+
def search(self, ou, search_base, filter_str, attrs):
326+
# simple paged search
327+
# https://github.com/cannatag/ldap3/blob/7991e67d0a2fb2c1f9cbf832d110ad29fc378f9b/docs/manual/source/standard.rst#L4
328+
# https://ldap3.readthedocs.io/en/latest/tutorial_searches.html#simple-paged-search
329+
response = self.connection.extend.standard.paged_search(
330+
f"ou={ou},{search_base}",
331+
filter_str,
332+
attributes=attrs,
333+
paged_size=500,
334+
generator=True
335+
)
336+
337+
return response
338+
339+
340+
# TODO:
341+
# do_ldap_fallback_search, get_ldap_groups, and get_ldap_active_users_and_groups should be a method of the LDAPSearch class
342+
# script calling this lib should init LDAPSearch, then call the method that asks for the info it wants.
343+
# Be able to feed in either one server's config to the LDAPSearch, or a conffile to parse with a list of >=1 LDAP servers to do fallback searches with.
344+
345+
def do_ldap_fallback_search(search_ou, search_filter, attrs, ldap_config: configparser.ConfigParser):
346+
response = None
347+
348+
if ldap_config == None:
349+
raise EmptyConfiguration(
350+
"Search Attempted with \"None\" config object."
351+
)
352+
353+
for section in ldap_config.sections():
354+
print(f"Attempting search with server {section}")
355+
try:
356+
server_url = ldap_config.get(section, LDAP_CONFIG_KEYS.LDAP_Server_URL)
357+
search_base = ldap_config.get(section, LDAP_CONFIG_KEYS.LDAP_Search_Base)
358+
search_user = ldap_config.get(section, LDAP_CONFIG_KEYS.LDAP_User)
359+
authtok_file = ldap_config.get(section, LDAP_CONFIG_KEYS.LDAP_AuthTok_File)
360+
authtok = get_ldap_authtok(authtok_file)
361+
362+
searcher = LDAPServer(ldap_server=server_url, ldap_user=search_user, ldap_authtok=authtok)
363+
response = searcher.search(search_ou, search_base, search_filter, attrs)
364+
365+
#If we get a response from one of the servers, we don't need to check the rest
366+
if not response is None:
367+
print(f"Response found for server {section}.")
368+
break
369+
# Perm issue reading token file
370+
except PermissionError as permError:
371+
print(f"Permission Error when attempting search for {section}: {permError}.")
372+
# Problem getting LDAP Response
373+
except LDAPException as ldapError:
374+
print(f"Exception occurred when attempting search for {section}: {ldapError}.")
375+
continue
376+
377+
if response is None:
378+
raise NoLDAPResponse(
379+
f"No response found via LDAP servers: {[section for section in ldap_config]}."
380+
)
381+
382+
return response
383+
384+
385+
def get_ldap_groups(config=None):
227386
ldap_group_osggids = set()
228-
server = Server(ldap_server, get_info=ALL)
229-
connection = Connection(server, ldap_user, ldap_authtok, client_strategy=SAFE_SYNC, auto_bind=True)
230-
_, _, response, _ = connection.search("ou=groups,o=OSG,o=CO,dc=cilogon,dc=org", "(cn=*)", attributes=ALL_ATTRIBUTES)
387+
388+
response = do_ldap_fallback_search(
389+
search_ou="groups",
390+
search_filter="(cn=*)",
391+
attrs=["gidNumber"],
392+
ldap_config=config
393+
)
394+
231395
for group in response:
232396
ldap_group_osggids.add(group["attributes"]["gidNumber"])
233397
return ldap_group_osggids
234398

235399

400+
def get_ldap_active_users_and_groups(filter_group_name=None, config=None):
401+
""" Retrieve a dictionary of active users from LDAP, with their group memberships. """
402+
ldap_active_users = dict()
403+
filter_str = ("(isMemberOf=CO:members:active)" if filter_group_name is None
404+
else f"(&(isMemberOf={filter_group_name})(isMemberOf=CO:members:active))")
405+
406+
response = do_ldap_fallback_search(
407+
search_ou="people",
408+
search_filter=filter_str,
409+
attrs=["employeeNumber", "isMemberOf"],
410+
ldap_config=config
411+
)
412+
413+
for person in response:
414+
ldap_active_users[person["attributes"]["employeeNumber"]] = person["attributes"].get("isMemberOf", [])
415+
416+
return ldap_active_users
417+
418+
236419
def identifier_from_list(id_list, id_type):
237420
id_type_list = [id["Type"] for id in id_list]
238421
try:
@@ -324,6 +507,7 @@ def provision_group(gid, provision_target, endpoint, authstr):
324507
}
325508
return call_api3(POST, path, data, endpoint, authstr)
326509

510+
327511
def provision_group_members(gid, prov_id, endpoint, authstr):
328512
data = {
329513
"RequestType" : "CoPersonProvisioning",

create_project.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,3 @@ def main(args):
152152
except (RuntimeError, urllib.error.HTTPError) as e:
153153
print(e, file=sys.stderr)
154154
sys.exit(1)
155-

0 commit comments

Comments
 (0)