authz.py (116 lines of code) (raw):

#!/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.path import time import argparse import logging import asfpy.pubsub import asfpy.syslog import yaml import requests import gen LOGGER = logging.getLogger(__name__) # The service will set the working directory, so we can find this. CONFIG_FNAME = 'svnauthz.yaml' # Specify a time in the far future to indicate that we have not # (recently) signaled a need to write the authz files. FAR_FUTURE = 1e13 class Authorization: # There are some groups with custom DN values DN_AUTH = 'ou=auth,ou=groups,dc=apache,dc=org' DN_GROUPS = 'ou=groups,dc=apache,dc=org' DN_SERVICES = 'ou=groups,ou=services,dc=apache,dc=org' def __init__(self, cfg, verbose=0): self.cfg = cfg def verbose2(*args): if verbose >= 2: print(*args) self.verbose2 = verbose2 # Gather up a bunch of changes, then write new files. We want to # avoid writing for each change. Gather them up for a bit of time, # then dump the group of changes into the new authz files. self.delay = cfg['config']['delay'] self.verbose2('DELAY:', self.delay) url = cfg['config']['ldap'] self.verbose2('LDAP:', url) self.verbose2('AUTH:', cfg['special']['auth']) self.verbose2('GROUPS:', cfg['special']['groups']) self.verbose2('SERVICES:', cfg['special']['services']) self.verbose2('EXPLICIT:', cfg['explicit']) special = { a: self.DN_AUTH for a in cfg['special']['auth'] } special.update((g, self.DN_GROUPS) for g in cfg['special']['groups']) special.update((s, self.DN_SERVICES) for s in cfg['special']['services']) self.gen = gen.Generator(url, cfg['config']['binddn'], cfg['config']['bindpw'], special, cfg['explicit'], ) self.auth = (cfg['generate']['template_username'], cfg['generate']['template_password'], ) turl = cfg['generate']['template_url'] odir = cfg['generate']['output_dir'] LOGGER.debug(f'TURL: {turl}\nODIR: {odir}') self.dist_authz = os.path.join(odir, cfg['generate']['dist_output']) self.mappings = { } for name in cfg['generate']: ob = cfg['generate'][name] if isinstance(ob, dict): # Note: NAME is unused, except as a descriptor/grouping t = turl + ob['template'] o = os.path.join(odir, ob['output']) self.mappings[t] = o # Write new authz files on startup. self.write_signal = 0 # epoch def write_needed(self): "Signal that a (re)write of the authz files is needed." # Avoid shifting the time that we first signaled. self.write_signal = min(self.write_signal, time.time()) def handle_commit(self, commit_info): LOGGER.debug(f'COMMIT FILES: {commit_info["files"]}') ### check against cfg/commit/path self.write_needed() def write_files(self): self.write_signal = FAR_FUTURE t0 = time.time() LOGGER.debug(f'WRITE_FILES: beginning at {t0}') for t, o in self.mappings.items(): if t.startswith('/'): # File path. Just read it. template = open(t).read() else: req = requests.get(t, auth=self.auth, timeout=30) req.raise_for_status() # report failure template = req.text self.gen.write_file(template.splitlines(), o) self.gen.write_dist(self.dist_authz) LOGGER.debug(f' DURATION: {time.time() - t0}') def handler(self, payload): #LOGGER.debug(f'PAYLOAD: {payload}') # If a (re)write has been signaled, then wait for a bit before # writing more files. This prevents rewriting on EVERY change. # Given that a heartbeat occurs every 5 seconds (as of this # comment), we'll get an opportunity to check/write. if time.time() > self.write_signal + self.delay: self.write_files() # What kind of packet/payload arrived from PUBSUB ? if 'stillalive' in payload: self.verbose2('HEARTBEAT:', payload) elif 'commit' in payload: self.handle_commit(payload['commit']) elif 'dn' in payload: # LDAP has changed, but we don't need the details. It would # be incredibly difficult to map changes against what LDAP # records are needed by the authz files. So, just rebuild the # files, regardless. LOGGER.info(f'LDAP CHANGE: {payload["dn"]}') self.write_needed() else: # unknown payload. (???) pass def main(args): cfg = yaml.safe_load(open(CONFIG_FNAME)) authz = Authorization(cfg, args.verbose) ### deal with args.templates if args.test: # Generate the files, then exit. No daemon. authz.write_files() return username = cfg['server']['username'] password = cfg['server']['password'] topics = set() topics.add(cfg['commit']['topic']) topics.add(cfg['ldap']['topic']) # FUTURE: can add more topics here. url = cfg['server']['url'] + ','.join(topics) authz.verbose2('URL:', url) # Run forever asfpy.pubsub.listen_forever(authz.handler, url, (username, password), raw=True) if __name__ == '__main__': ### use argparse to change the level logging.basicConfig(level=logging.INFO) parser = argparse.ArgumentParser(description='Monitor/generate svn authz files.') parser.add_argument('-v', '--verbose', action='count', default=0, help= 'Print information during operation.' ' Multiple uses, for additional information.') parser.add_argument('--test', action='store_true', help='Run a test generation of the authz files.') parser.add_argument('--templates', help='Directory containing the (locally-modified) templates.') args = parser.parse_args() # When testing, always produce some of the basic debug output. if args.test: # Switch the root logger to DEBUG. logging.getLogger().setLevel(logging.DEBUG) args.verbose = max(1, args.verbose) main(args)