Skip to content

Commit c4f552e

Browse files
authored
Merge pull request #27 from mwestphall/INF-1060-member-removals
INF-1060: Generate project mapfile from topology/LDAP instead of COManage
2 parents 5236b3e + f048953 commit c4f552e

File tree

2 files changed

+82
-64
lines changed

2 files changed

+82
-64
lines changed

comanage_utils.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
import time
77
import urllib.error
88
import urllib.request
9-
from ldap3 import Server, Connection, ALL, ALL_ATTRIBUTES, SAFE_SYNC
9+
from ldap3 import Server, Connection, ALL, SAFE_SYNC, Tls
10+
from dataclasses import dataclass
1011

1112
#PRODUCTION VALUES
1213

@@ -16,6 +17,7 @@
1617
PRODUCTION_OSG_CO_ID = 7
1718
PRODUCTION_UNIX_CLUSTER_ID = 1
1819
PRODUCTION_LDAP_TARGET_ID = 6
20+
LDAP_BASE_DN = "o=OSG,o=CO,dc=cilogon,dc=org"
1921

2022
#TEST VALUES
2123

@@ -69,6 +71,14 @@ def mkauthstr(user, passwd):
6971
return encodebytes(raw_authstr.encode()).decode().replace("\n", "")
7072

7173

74+
def get_ldap_authtok(ldap_authfile):
75+
if ldap_authfile is not None:
76+
ldap_authtok = open(ldap_authfile).readline().strip()
77+
else:
78+
raise PermissionError
79+
return ldap_authtok
80+
81+
7282
def mkrequest(method, target, data, endpoint, authstr, **kw):
7383
url = os.path.join(endpoint, target)
7484
if kw:
@@ -165,16 +175,43 @@ def get_datalist(data, listname):
165175
return data[listname] if data else []
166176

167177

178+
class LDAPSearch:
179+
""" Wrapper class for LDAP searches. """
180+
server: Server = None
181+
connection: Connection = None
182+
183+
def __init__(self, ldap_server, ldap_user, ldap_authtok):
184+
self.server = Server(ldap_server, get_info=ALL)
185+
self.connection = Connection(self.server, ldap_user, ldap_authtok, client_strategy=SAFE_SYNC, auto_bind=True)
186+
187+
def search(self, ou, filter_str, attrs):
188+
_, _, response, _ = self.connection.search(f"ou={ou},{LDAP_BASE_DN}", filter_str, attributes=attrs)
189+
return response
190+
168191
def get_ldap_groups(ldap_server, ldap_user, ldap_authtok):
169192
ldap_group_osggids = set()
170-
server = Server(ldap_server, get_info=ALL)
171-
connection = Connection(server, ldap_user, ldap_authtok, client_strategy=SAFE_SYNC, auto_bind=True)
172-
_, _, response, _ = connection.search("ou=groups,o=OSG,o=CO,dc=cilogon,dc=org", "(cn=*)", attributes=ALL_ATTRIBUTES)
193+
searcher = LDAPSearch(ldap_server, ldap_user, ldap_authtok)
194+
response = searcher.search("groups", "(cn=*)", ["gidNumber"])
173195
for group in response:
174196
ldap_group_osggids.add(group["attributes"]["gidNumber"])
175197
return ldap_group_osggids
176198

177199

200+
def get_ldap_active_users_and_groups(ldap_server, ldap_user, ldap_authtok, filter_group_name=None):
201+
""" Retrieve a dictionary of active users from LDAP, with their group memberships. """
202+
ldap_active_users = dict()
203+
filter_str = ("(isMemberOf=CO:members:active)" if filter_group_name is None
204+
else f"(&(isMemberOf={filter_group_name})(isMemberOf=CO:members:active))")
205+
206+
searcher = LDAPSearch(ldap_server, ldap_user, ldap_authtok)
207+
response = searcher.search("people", filter_str, ["employeeNumber", "isMemberOf"])
208+
209+
for person in response:
210+
ldap_active_users[person["attributes"]["employeeNumber"]] = person["attributes"].get("isMemberOf", [])
211+
212+
return ldap_active_users
213+
214+
178215
def identifier_from_list(id_list, id_type):
179216
id_type_list = [id["Type"] for id in id_list]
180217
try:

osg-comanage-project-usermap.py

Lines changed: 41 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,37 @@
44
import re
55
import sys
66
import getopt
7-
import collections
7+
import requests
88
import comanage_utils as utils
99

1010

1111
SCRIPT = os.path.basename(__file__)
12-
ENDPOINT = "https://registry.cilogon.org/registry/"
13-
OSG_CO_ID = 7
12+
ENDPOINT = "https://registry-test.cilogon.org/registry/"
13+
TOPOLOGY_ENDPOINT = "https://topology.opensciencegrid.org/"
14+
LDAP_SERVER = "ldaps://ldap-test.cilogon.org"
15+
LDAP_USER = "uid=registry_user,ou=system,o=OSG,o=CO,dc=cilogon,dc=org"
16+
OSG_CO_ID = 8
17+
CACHE_FILENAME = "COmanage_Projects_cache.txt"
18+
CACHE_LIFETIME_HOURS = 0.5
1419

1520

1621
_usage = f"""\
17-
usage: [PASS=...] {SCRIPT} [OPTIONS]
22+
usage: {SCRIPT} [OPTIONS]
1823
1924
OPTIONS:
2025
-u USER[:PASS] specify USER and optionally PASS on command line
2126
-c OSG_CO_ID specify OSG CO ID (default = {OSG_CO_ID})
27+
-s LDAP_SERVER specify LDAP server to read data from
28+
-l LDAP_USER specify LDAP user for reading data from LDAP server
29+
-a ldap_authfile specify path to file to open and read LDAP authtok
2230
-d passfd specify open fd to read PASS
2331
-f passfile specify path to file to open and read PASS
2432
-e ENDPOINT specify REST endpoint
2533
(default = {ENDPOINT})
2634
-o outfile specify output file (default: write to stdout)
2735
-g filter_group filter users by group name (eg, 'ap1-login')
28-
-l localmaps specify a comma-delimited list of local HTCondor mapfiles to merge into outfile
36+
-m localmaps specify a comma-delimited list of local HTCondor mapfiles to merge into outfile
37+
-n min_users Specify minimum number of users required to update the output file (default: 100)
2938
-h display this help text
3039
3140
PASS for USER is taken from the first of:
@@ -49,7 +58,11 @@ class Options:
4958
osg_co_id = OSG_CO_ID
5059
outfile = None
5160
authstr = None
61+
ldap_server = LDAP_SERVER
62+
ldap_user = LDAP_USER
63+
ldap_authtok = None
5264
filtergrp = None
65+
min_users = 100 # Bail out before updating the file if we have fewer than this many users
5366
localmaps = []
5467

5568

@@ -62,35 +75,12 @@ def get_osg_co_groups__map():
6275
#print("get_osg_co_groups__map()")
6376
resp_data = utils.get_osg_co_groups(options.osg_co_id, options.endpoint, options.authstr)
6477
data = utils.get_datalist(resp_data, "CoGroups")
65-
return { g["Id"]: g["Name"] for g in data }
66-
67-
68-
def co_group_is_ospool(gid):
69-
#print(f"co_group_is_ospool({gid})")
70-
resp_data = utils.get_co_group_identifiers(gid, options.endpoint, options.authstr)
71-
data = utils.get_datalist(resp_data, "Identifiers")
72-
return any( i["Type"] == "ospoolproject" for i in data )
73-
74-
75-
def get_co_group_members__pids(gid):
76-
#print(f"get_co_group_members__pids({gid})")
77-
resp_data = utils.get_co_group_members(gid, options.endpoint, options.authstr)
78-
data = utils.get_datalist(resp_data, "CoGroupMembers")
79-
# For INF-1060: Temporary Fix until "The Great Project Provisioning" is finished
80-
return [ m["Person"]["Id"] for m in data if m["Member"] == True]
81-
82-
83-
def get_co_person_osguser(pid):
84-
#print(f"get_co_person_osguser({pid})")
85-
resp_data = utils.get_co_person_identifiers(pid, options.endpoint, options.authstr)
86-
data = utils.get_datalist(resp_data, "Identifiers")
87-
typemap = { i["Type"]: i["Identifier"] for i in data }
88-
return typemap.get("osguser")
78+
return { g["Name"]: g["Id"] for g in data }
8979

9080

9181
def parse_options(args):
9282
try:
93-
ops, args = getopt.getopt(args, 'u:c:d:f:g:e:o:l:h')
83+
ops, args = getopt.getopt(args, 'u:c:s:l:a:d:f:g:e:o:h:n:m')
9484
except getopt.GetoptError:
9585
usage()
9686

@@ -99,21 +89,27 @@ def parse_options(args):
9989

10090
passfd = None
10191
passfile = None
92+
ldap_authfile = None
10293

10394
for op, arg in ops:
10495
if op == '-h': usage()
10596
if op == '-u': options.user = arg
10697
if op == '-c': options.osg_co_id = int(arg)
98+
if op == '-s': options.ldap_server= arg
99+
if op == '-l': options.ldap_user = arg
100+
if op == '-a': ldap_authfile = arg
107101
if op == '-d': passfd = int(arg)
108102
if op == '-f': passfile = arg
109103
if op == '-e': options.endpoint = arg
110104
if op == '-o': options.outfile = arg
111105
if op == '-g': options.filtergrp = arg
112-
if op == '-l': options.localmaps = arg.split(",")
106+
if op == '-m': options.localmaps = arg.split(",")
107+
if op == '-n': options.min_users = int(arg)
113108

114109
try:
115110
user, passwd = utils.getpw(options.user, passfd, passfile)
116111
options.authstr = utils.mkauthstr(user, passwd)
112+
options.ldap_authtok = utils.get_ldap_authtok(ldap_authfile)
117113
except PermissionError:
118114
usage("PASS required")
119115

@@ -123,36 +119,18 @@ def _deduplicate_list(items):
123119
"""
124120
return list(dict.fromkeys(items))
125121

126-
def gid_pids_to_osguser_pid_gids(gid_pids, pid_osguser):
127-
pid_gids = collections.defaultdict(list)
128-
129-
for gid in gid_pids:
130-
for pid in gid_pids[gid]:
131-
if pid_osguser[pid] is not None and gid not in pid_gids[pid]:
132-
pid_gids[pid].append(gid)
133-
134-
return pid_gids
135-
136-
137-
def filter_by_group(pid_gids, groups, filter_group_name):
138-
groups_idx = { v: k for k,v in groups.items() }
139-
filter_gid = groups_idx[filter_group_name] # raises KeyError if missing
140-
filter_group_pids = set(get_co_group_members__pids(filter_gid))
141-
return { p: g for p,g in pid_gids.items() if p in filter_group_pids }
142-
143-
144122
def get_osguser_groups(filter_group_name=None):
145-
groups = get_osg_co_groups__map()
146-
ospool_gids = filter(co_group_is_ospool, groups)
147-
gid_pids = { gid: get_co_group_members__pids(gid) for gid in ospool_gids }
148-
all_pids = set( pid for gid in gid_pids for pid in gid_pids[gid] )
149-
pid_osguser = { pid: get_co_person_osguser(pid) for pid in all_pids }
150-
pid_gids = gid_pids_to_osguser_pid_gids(gid_pids, pid_osguser)
151-
if filter_group_name is not None:
152-
pid_gids = filter_by_group(pid_gids, groups, filter_group_name)
153-
154-
return { pid_osguser[pid]: map(groups.get, gids)
155-
for pid, gids in pid_gids.items() }
123+
ldap_users = utils.get_ldap_active_users_and_groups(options.ldap_server, options.ldap_user, options.ldap_authtok, filter_group_name)
124+
topology_projects = requests.get(f"{TOPOLOGY_ENDPOINT}/miscproject/json").json()
125+
project_names = topology_projects.keys()
126+
127+
# Get COManage group IDs to preserve ordering from pre-LDAP migration script behavior
128+
groups_ids = get_osg_co_groups__map()
129+
return {
130+
user: sorted([g for g in groups if g in project_names], key = lambda g: groups_ids.get(g, 0))
131+
for user, groups in ldap_users.items()
132+
if any(g in project_names for g in groups)
133+
}
156134

157135

158136
def parse_localmap(inputfile):
@@ -204,6 +182,9 @@ def main(args):
204182
maps.append(parse_localmap(localmap))
205183
osguser_groups_merged = merge_maps(maps)
206184

185+
# Sanity check, confirm we have generated a "sane" amount of user -> group mappings
186+
if len(osguser_groups_merged) < options.min_users:
187+
raise RuntimeError(f"Refusing to update output file: only {len(osguser_groups_merged)} users found")
207188
print_usermap(osguser_groups_merged)
208189

209190

0 commit comments

Comments
 (0)