blob: 11a32a51e73a4747b41d7523a3b521c8490134b0 [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.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:
template = requests.get(t, auth=self.auth, timeout=30).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)