Import Upstream version 4.12.4
This commit is contained in:
142
ipaserver/custodia/server/__init__.py
Normal file
142
ipaserver/custodia/server/__init__.py
Normal file
@@ -0,0 +1,142 @@
|
||||
# Copyright (C) 2015 Custodia Project Contributors - see LICENSE file
|
||||
from __future__ import absolute_import
|
||||
|
||||
import importlib
|
||||
import os
|
||||
|
||||
import pkg_resources
|
||||
|
||||
import six
|
||||
|
||||
from ipaserver.custodia import log
|
||||
from ipaserver.custodia.httpd.server import HTTPServer
|
||||
|
||||
from .args import default_argparser
|
||||
from .args import parse_args as _parse_args
|
||||
from .config import parse_config as _parse_config
|
||||
|
||||
logger = log.getLogger('custodia')
|
||||
|
||||
__all__ = ['default_argparser', 'main']
|
||||
|
||||
|
||||
def attach_store(typename, plugins, stores):
|
||||
for name, c in six.iteritems(plugins):
|
||||
if getattr(c, 'store_name', None) is None:
|
||||
continue
|
||||
try:
|
||||
c.store = stores[c.store_name]
|
||||
except KeyError:
|
||||
raise ValueError('[%s%s] references unexisting store '
|
||||
'"%s"' % (typename, name, c.store_name))
|
||||
|
||||
|
||||
def _load_plugin_class(menu, name):
|
||||
"""Load Custodia plugin
|
||||
|
||||
Entry points are preferred over dotted import path.
|
||||
"""
|
||||
group = 'custodia.{}'.format(menu)
|
||||
eps = list(pkg_resources.iter_entry_points(group, name))
|
||||
if len(eps) > 1:
|
||||
raise ValueError(
|
||||
"Multiple entry points for {} {}: {}".format(menu, name, eps))
|
||||
elif len(eps) == 1:
|
||||
# backwards compatibility with old setuptools
|
||||
ep = eps[0]
|
||||
if hasattr(ep, 'resolve'):
|
||||
return ep.resolve()
|
||||
else:
|
||||
return ep.load(require=False)
|
||||
elif '.' in name:
|
||||
# fall back to old style dotted name
|
||||
module, classname = name.rsplit('.', 1)
|
||||
m = importlib.import_module(module)
|
||||
return getattr(m, classname)
|
||||
else:
|
||||
raise ValueError("{}: {} not found".format(menu, name))
|
||||
|
||||
|
||||
def _create_plugin(cfgparser, section, menu):
|
||||
if not cfgparser.has_option(section, 'handler'):
|
||||
raise ValueError('Invalid section, missing "handler"')
|
||||
|
||||
handler_name = cfgparser.get(section, 'handler')
|
||||
hconf = {'facility_name': section}
|
||||
try:
|
||||
handler = _load_plugin_class(menu, handler_name)
|
||||
classname = handler.__name__
|
||||
hconf['facility_name'] = '%s-[%s]' % (classname, section)
|
||||
except Exception as e:
|
||||
raise ValueError('Invalid format for "handler" option '
|
||||
'[%r]: %s' % (e, handler_name))
|
||||
|
||||
if handler._options is not None:
|
||||
# new-style plugin with parser and section
|
||||
plugin = handler(cfgparser, section)
|
||||
else:
|
||||
# old-style plugin with config dict
|
||||
hconf.update(cfgparser.items(section))
|
||||
hconf.pop('handler')
|
||||
plugin = handler(hconf)
|
||||
plugin.section = section
|
||||
return plugin
|
||||
|
||||
|
||||
def _load_plugins(config, cfgparser):
|
||||
"""Load and initialize plugins
|
||||
"""
|
||||
# set umask before any plugin gets a chance to create a file
|
||||
os.umask(config['umask'])
|
||||
|
||||
for s in cfgparser.sections():
|
||||
if s in {'ENV', 'global'}:
|
||||
# ENV section is only used for interpolation
|
||||
continue
|
||||
|
||||
if s.startswith('/'):
|
||||
menu = 'consumers'
|
||||
path_chain = s.split('/')
|
||||
if path_chain[-1] == '':
|
||||
path_chain = path_chain[:-1]
|
||||
name = tuple(path_chain)
|
||||
else:
|
||||
if s.startswith('auth:'):
|
||||
menu = 'authenticators'
|
||||
name = s[5:]
|
||||
elif s.startswith('authz:'):
|
||||
menu = 'authorizers'
|
||||
name = s[6:]
|
||||
elif s.startswith('store:'):
|
||||
menu = 'stores'
|
||||
name = s[6:]
|
||||
else:
|
||||
raise ValueError('Invalid section name [%s].\n' % s)
|
||||
|
||||
try:
|
||||
config[menu][name] = _create_plugin(cfgparser, s, menu)
|
||||
except Exception as e:
|
||||
logger.debug("Plugin '%s' failed to load.", name, exc_info=True)
|
||||
raise RuntimeError(menu, name, e)
|
||||
|
||||
# 2nd initialization stage
|
||||
for menu in ['authenticators', 'authorizers', 'consumers', 'stores']:
|
||||
plugins = config[menu]
|
||||
for name in sorted(plugins):
|
||||
plugin = plugins[name]
|
||||
plugin.finalize_init(config, cfgparser, context=None)
|
||||
|
||||
|
||||
def main(argparser=None):
|
||||
args = _parse_args(argparser=argparser)
|
||||
# parse arguments and populate config with basic settings
|
||||
cfgparser, config = _parse_config(args)
|
||||
# initialize logging
|
||||
log.setup_logging(config['debug'], config['auditlog'])
|
||||
logger.info('Custodia instance %s', args.instance or '<main>')
|
||||
logger.debug('Config file(s) %s loaded', config['configfiles'])
|
||||
# load plugins after logging
|
||||
_load_plugins(config, cfgparser)
|
||||
# create and run server
|
||||
httpd = HTTPServer(config['server_url'], config)
|
||||
httpd.serve()
|
||||
7
ipaserver/custodia/server/__main__.py
Normal file
7
ipaserver/custodia/server/__main__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# Copyright (C) 2015 Custodia Project Contributors - see LICENSE file
|
||||
from __future__ import absolute_import
|
||||
|
||||
from ipaserver.custodia.server import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
80
ipaserver/custodia/server/args.py
Normal file
80
ipaserver/custodia/server/args.py
Normal file
@@ -0,0 +1,80 @@
|
||||
# Copyright (C) 2015-2017 Custodia Project Contributors - see LICENSE file
|
||||
from __future__ import absolute_import
|
||||
|
||||
import argparse
|
||||
import os
|
||||
|
||||
|
||||
class AbsFileType(argparse.FileType):
|
||||
"""argparse file type with absolute path
|
||||
"""
|
||||
def __call__(self, string):
|
||||
if string != '-':
|
||||
string = os.path.abspath(string)
|
||||
return super(AbsFileType, self).__call__(string)
|
||||
|
||||
|
||||
class ConfigfileAction(argparse.Action):
|
||||
"""Default action handler for configfile
|
||||
"""
|
||||
default_path = '/etc/custodia/custodia.conf'
|
||||
default_instance = '/etc/custodia/{instance}.conf'
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
if values is None:
|
||||
if namespace.instance is not None:
|
||||
values = self.default_instance.format(
|
||||
instance=namespace.instance
|
||||
)
|
||||
else:
|
||||
values = self.default_path
|
||||
values = self.type(values)
|
||||
setattr(namespace, self.dest, values)
|
||||
|
||||
|
||||
def instance_name(string):
|
||||
"""Check for valid instance name
|
||||
"""
|
||||
invalid = ':/@'
|
||||
if set(string).intersection(invalid):
|
||||
msg = 'Invalid instance name {}'.format(string)
|
||||
raise argparse.ArgumentTypeError(msg)
|
||||
return string
|
||||
|
||||
|
||||
default_argparser = argparse.ArgumentParser(
|
||||
prog='custodia',
|
||||
description='Custodia server'
|
||||
)
|
||||
default_argparser.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
help='Debug mode'
|
||||
)
|
||||
default_argparser.add_argument(
|
||||
'--instance',
|
||||
type=instance_name,
|
||||
help='Instance name',
|
||||
default=None
|
||||
)
|
||||
default_argparser.add_argument(
|
||||
'configfile',
|
||||
nargs='?',
|
||||
action=ConfigfileAction,
|
||||
type=AbsFileType('r'),
|
||||
help=('Path to custodia server config (default: '
|
||||
'/etc/custodia/{instance}/custodia.conf)'),
|
||||
)
|
||||
|
||||
|
||||
def parse_args(args=None, argparser=None):
|
||||
if argparser is None:
|
||||
argparser = default_argparser
|
||||
|
||||
# namespace with default values
|
||||
namespace = argparse.Namespace(
|
||||
debug=False,
|
||||
instance=None,
|
||||
)
|
||||
|
||||
return argparser.parse_args(args, namespace)
|
||||
160
ipaserver/custodia/server/config.py
Normal file
160
ipaserver/custodia/server/config.py
Normal file
@@ -0,0 +1,160 @@
|
||||
# Copyright (C) 2015-2017 Custodia Project Contributors - see LICENSE file
|
||||
from __future__ import absolute_import
|
||||
|
||||
import configparser
|
||||
import glob
|
||||
import os
|
||||
import socket
|
||||
from urllib.parse import quote as url_escape
|
||||
|
||||
|
||||
class CustodiaConfig:
|
||||
CONFIG_SPECIALS = ['authenticators', 'authorizers', 'consumers', 'stores']
|
||||
|
||||
DEFAULT_PATHS = [
|
||||
('libdir', '/var/lib/custodia/{instance}'),
|
||||
('logdir', '/var/log/custodia/{instance}'),
|
||||
('rundir', '/var/run/custodia/{instance}'),
|
||||
('socketdir', '/var/run/custodia'),
|
||||
]
|
||||
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
self.config = {}
|
||||
self.defaults = None
|
||||
self.parser = None
|
||||
|
||||
def get_defaults(self):
|
||||
configpath = self.args.configfile.name
|
||||
instance = self.args.instance
|
||||
defaults = {
|
||||
# Do not use getfqdn(). Internaly it calls gethostbyaddr which
|
||||
# might perform a DNS query.
|
||||
'hostname': socket.gethostname(),
|
||||
'configdir': os.path.dirname(configpath),
|
||||
'confdpattern': os.path.join(configpath + '.d', '*.conf'),
|
||||
'instance': instance if instance else '',
|
||||
}
|
||||
for name, path in self.DEFAULT_PATHS:
|
||||
defaults[name] = os.path.abspath(path.format(**defaults))
|
||||
return defaults
|
||||
|
||||
def create_parser(self):
|
||||
parser = configparser.ConfigParser(
|
||||
interpolation=configparser.ExtendedInterpolation(),
|
||||
defaults=self.defaults
|
||||
)
|
||||
parser.optionxform = str
|
||||
|
||||
# add env
|
||||
parser.add_section(u'ENV')
|
||||
for k, v in os.environ.items():
|
||||
if set(v).intersection('\r\n\x00'):
|
||||
continue
|
||||
parser.set(u'ENV', k, v.replace(u'$', u'$$'))
|
||||
|
||||
# default globals
|
||||
parser.add_section(u'global')
|
||||
parser.set(u'global', u'auditlog', u'${logdir}/audit.log')
|
||||
parser.set(u'global', u'debug', u'false')
|
||||
parser.set(u'global', u'umask', u'027')
|
||||
parser.set(u'global', u'makedirs', u'false')
|
||||
|
||||
return parser
|
||||
|
||||
def read_configs(self):
|
||||
with self.args.configfile as f:
|
||||
self.parser.read_file(f)
|
||||
|
||||
configfiles = [self.args.configfile.name]
|
||||
|
||||
pattern = self.parser.get(u'DEFAULT', u'confdpattern')
|
||||
if pattern:
|
||||
confdfiles = glob.glob(pattern)
|
||||
confdfiles.sort()
|
||||
for confdfile in confdfiles:
|
||||
with open(confdfile) as f:
|
||||
self.parser.read_file(f)
|
||||
configfiles.append(confdfile)
|
||||
|
||||
return configfiles
|
||||
|
||||
def makedirs(self):
|
||||
for name, _path in self.DEFAULT_PATHS:
|
||||
path = self.parser.get(u'DEFAULT', name)
|
||||
parent = os.path.dirname(path)
|
||||
# create parents according to umask
|
||||
if not os.path.isdir(parent):
|
||||
os.makedirs(parent)
|
||||
# create final directory with restricted permissions
|
||||
if not os.path.isdir(path):
|
||||
os.mkdir(path, 0o700)
|
||||
|
||||
def populate_config(self):
|
||||
config = self.config
|
||||
|
||||
for s in self.CONFIG_SPECIALS:
|
||||
config[s] = {}
|
||||
|
||||
for opt, val in self.parser.items(u'global'):
|
||||
if opt in self.CONFIG_SPECIALS:
|
||||
raise ValueError('"%s" is an invalid '
|
||||
'[global] option' % opt)
|
||||
config[opt] = val
|
||||
|
||||
config['tls_verify_client'] = self.parser.getboolean(
|
||||
'global', 'tls_verify_client', fallback=False)
|
||||
config['debug'] = self.parser.getboolean(
|
||||
'global', 'debug', fallback=False)
|
||||
config['makedirs'] = self.parser.getboolean(
|
||||
'global', 'makedirs', fallback=False)
|
||||
if self.args.debug:
|
||||
config['debug'] = self.args.debug
|
||||
|
||||
config['auditlog'] = os.path.abspath(config.get('auditlog'))
|
||||
config['umask'] = int(config.get('umask', '027'), 8)
|
||||
|
||||
url = config.get('server_url')
|
||||
sock = config.get('server_socket')
|
||||
|
||||
if url and sock:
|
||||
raise ValueError(
|
||||
"'server_url' and 'server_socket' are mutually exclusive.")
|
||||
|
||||
if not url and not sock:
|
||||
# no option but, use default socket path
|
||||
socketdir = self.parser.get(u'DEFAULT', u'socketdir')
|
||||
name = self.args.instance if self.args.instance else 'custodia'
|
||||
sock = os.path.join(socketdir, name + '.sock')
|
||||
|
||||
if sock:
|
||||
server_socket = os.path.abspath(sock)
|
||||
config['server_url'] = 'http+unix://{}/'.format(
|
||||
url_escape(server_socket, ''))
|
||||
|
||||
def __call__(self):
|
||||
self.defaults = self.get_defaults()
|
||||
self.parser = self.create_parser()
|
||||
self.config['configfiles'] = self.read_configs()
|
||||
self.populate_config()
|
||||
if self.config[u'makedirs']:
|
||||
self.makedirs()
|
||||
return self.parser, self.config
|
||||
|
||||
|
||||
def parse_config(args):
|
||||
ccfg = CustodiaConfig(args)
|
||||
return ccfg()
|
||||
|
||||
|
||||
def test(arglist):
|
||||
from pprint import pprint
|
||||
from .args import parse_args
|
||||
args = parse_args(arglist)
|
||||
parser, config = parse_config(args)
|
||||
pprint(parser.items("DEFAULT"))
|
||||
pprint(config)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test(['--instance=demo', './tests/empty.conf'])
|
||||
Reference in New Issue
Block a user