blob: 51c8fb84a34d4382910581d4ea9b57e70f95c18b [file] [log] [blame]
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import re
import ldap
import ezt
class FunkyLDAP(Exception):
def __init__(self, cn):
self.cn = cn
class LDAPClient:
"An augmented connection to our LDAP servers."
# Extract UIDs from an LDAP response.
UID_RE = re.compile(rb'^uid=([^,]*),.*')
# Disable cert check. The self-signed cert throws off python-ldap.
### global option, not per connection? ugh.
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
def __init__(self, url):
# Easy to front-load client handle creation. It will lazy connect.
self.handle = ldap.initialize(url)
def get_members(self, cn, dn, attr):
if attr:
attrlist = [ attr ]
else:
attrlist = None
results = self.handle.search_s(dn, scope=ldap.SCOPE_ONELEVEL,
filterstr='(cn=%s)' % (cn,),
attrlist=attrlist)
# Should be a single result.
if len(results) != 1:
### any data beyond the CN ?
raise FunkyLDAP(cn)
_, data = results[0]
if attr is None:
if 'memberUid' in data:
members = data['memberUid']
elif 'member' in data:
members = data['member']
else:
members = data[attr]
# Sometimes the result items look like: uid=FOO,ou=people,...
# Trim to just the uid values.
if members[0].startswith(b'uid='):
return [ self.UID_RE.match(m).group(1) for m in members ]
return members
class Generator:
# Query patterns for LDAP
QUERY_MAIN = ('ou=project,ou=groups,dc=apache,dc=org', 'member')
QUERY_PMC = ('ou=project,ou=groups,dc=apache,dc=org', 'owner')
QUERY_COMMITTERS = ('ou=groups,dc=apache,dc=org', 'memberUid')
def __init__(self, ldap_url, special, explicit):
self.client = LDAPClient(ldap_url)
self.special = special
self.explicit = explicit
def group_members(self, group):
"Given an authz @GROUP, return its members."
if group in self.explicit:
# This is an explicitly-defined authz group; not LDAP.
return self.explicit[group]
# Trim the authz group down to a {cn} value.
if group.endswith('-pmc'):
cn = group[:-4]
elif group.endswith('-ppmc'):
cn = group[:-5]
else:
cn = group
if group == 'committers':
# Special case this one. It uses a different attribute.
dn, attr = self.QUERY_COMMITTERS
elif group in self.special:
# These are defined in [special]
dn = self.special[group]
attr = None
elif group != cn:
# cn has had -(p)pmc sliced off. Look up the PMC.
dn, attr = self.QUERY_PMC
else:
# Not explicit, committers, special, or a PMC. Thus, it is
# a list of a project's committers.
dn, attr = self.QUERY_MAIN
# Find the group members within LDAP.
# Note: all member IDs are ascii, so convert to simple strings.
return [m.decode() for m in self.client.get_members(cn, dn, attr)]
def write_file(self, t_lines, output):
print(f'WRITE_FILE: writing to "{output}"')
new_z = [ ]
for line in t_lines:
if line.startswith(':readonly:'):
# FORMAT:
# :readonly:/some/random/path
# :readonly:/root/path/(alt1|alt2|alt3)
if '(' in line:
root, rest = line[10:].split('(')
subdirs = [ root+p for p in rest[:-1].split('|') ]
else:
subdirs = [ line[10:] ]
for s in subdirs:
new_z.append(f'[{s}]\n* = r')
elif line.startswith('#') or '={' not in line:
new_z.append(line)
else:
# Only GROUP={auth} is allowed here.
assert '={auth}' in line
group = line.split('=')[0]
### Place this specific auth, at this point in the authz file.
### This is temporary, as we manage this forward.
members = self.group_members(group)
new_z.append(f'{group}={",".join(members)}')
#print('AUTH:', repr(new_z))
# Write to an intermediate file, then do an atomic move into place.
### TODO: throw an alert if the new file is "too different" from the old
tmp = '%s.%d' % (output, os.getpid())
open(tmp, 'w').write('\n'.join(new_z) + '\n')
os.rename(tmp, output)