| #!/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: |
| args.verbose = max(1, args.verbose) |
| |
| main(args) |