From f0e50eac2e890c234ec809f36186a1cd33de4d81 Mon Sep 17 00:00:00 2001 From: Narayan Desai Date: Wed, 6 May 2009 01:27:05 +0000 Subject: Get basic auth working again with new ssl framework git-svn-id: https://svn.mcs.anl.gov/repos/bcfg/trunk/bcfg2@5189 ce84e21b-d406-0410-9b95-82705330c041 --- src/lib/Component.py | 12 +- src/lib/SSLServer.py | 55 +++++---- src/lib/Server/Plugins/Metadata.py | 7 +- src/lib/Server/XMLRPC.py | 159 +++++++++++++++++++++++++ src/sbin/bcfg2-server | 230 ++----------------------------------- 5 files changed, 207 insertions(+), 256 deletions(-) create mode 100644 src/lib/Server/XMLRPC.py (limited to 'src') diff --git a/src/lib/Component.py b/src/lib/Component.py index d35759603..b028f0dea 100644 --- a/src/lib/Component.py +++ b/src/lib/Component.py @@ -19,7 +19,8 @@ import Bcfg2.Logger from Bcfg2.SSLServer import XMLRPCServer def run_component (component_cls, argv=None, register=True, state_name=False, - cls_kwargs={}, extra_getopt='', time_out=10): + cls_kwargs={}, extra_getopt='', time_out=10, certfile=None, keyfile=None, + ca=None): if argv is None: argv = sys.argv try: @@ -46,7 +47,6 @@ def run_component (component_cls, argv=None, register=True, state_name=False, level = logging.DEBUG logging.getLogger().setLevel(level) - Bcfg2.Logger.log_to_stderr(logging.getLogger()) Bcfg2.Logger.setup_logging(component_cls.implementation, True, True) if daemon: @@ -73,13 +73,11 @@ def run_component (component_cls, argv=None, register=True, state_name=False, pidfile.close() component = component_cls(**cls_kwargs) - + # FIXME location = ('', 6789) - keypath = '/etc/bcfg2.key' - certfile = '/etc/bcfg2.key' - server = XMLRPCServer(location, keyfile=keypath, certfile=keypath, - register=register, timeout=time_out) + server = XMLRPCServer(location, keyfile=keyfile, certfile=certfile, + register=register, timeout=time_out, ca=ca) server.register_instance(component) try: diff --git a/src/lib/SSLServer.py b/src/lib/SSLServer.py index 99d6410b4..b9ab81f81 100644 --- a/src/lib/SSLServer.py +++ b/src/lib/SSLServer.py @@ -30,10 +30,11 @@ class XMLRPCDispatcher (SimpleXMLRPCServer.SimpleXMLRPCDispatcher): self.allow_none = allow_none self.encoding = encoding - def _marshaled_dispatch (self, data): + def _marshaled_dispatch (self, address, data): method_func = None params, method = xmlrpclib.loads(data) try: + params = (address, ) + params response = self.instance._dispatch(method, params, self.funcs) response = (response,) raw_response = xmlrpclib.dumps(response, methodresponse=1, @@ -89,11 +90,16 @@ class SSLServer (SocketServer.TCPServer, object): self.certfile = certfile self.ca = ca self.reqCert = reqCert + if ca and certfile: + self.mode = ssl.CERT_OPTIONAL + else: + self.mode = ssl.CERT_NONE def get_request(self): (sock, sockinfo) = self.socket.accept() sslsock = ssl.wrap_socket(sock, server_side=True, certfile=self.certfile, - keyfile=self.keyfile) + keyfile=self.keyfile, cert_reqs=self.mode, + ca_certs=self.ca) return sslsock, sockinfo def _get_url (self): @@ -119,19 +125,12 @@ class XMLRPCRequestHandler (SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): """ logger = logging.getLogger("Cobalt.Server.XMLRPCRequestHandler") - class CouldNotAuthenticate (Exception): - """Client did not present acceptible authentication information.""" - - require_auth = True - credentials = {'root':'default'} - def authenticate (self): - """Authenticate the credentials of the latest client.""" try: header = self.headers['Authorization'] except KeyError: self.logger.error("No authentication data presented") - raise self.CouldNotAuthenticate("client did not present credentials") + return False auth_type, auth_content = header.split() auth_content = base64.standard_b64decode(auth_content) try: @@ -139,12 +138,10 @@ class XMLRPCRequestHandler (SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): except ValueError: username = auth_content password = "" - try: - valid_password = self.credentials[username] - except KeyError: - raise self.CouldNotAuthenticate("unknown user: %s" % username) - if password != valid_password: - raise self.CouldNotAuthenticate("invalid password for %s" % username) + cert = self.request.getpeercert() + client_address = self.request.getpeername() + return self.server.instance.authenticate(cert, username, + password, client_address) def parse_request (self): """Extends parse_request. @@ -152,18 +149,18 @@ class XMLRPCRequestHandler (SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): Optionally check HTTP authentication when parsing.""" if not SimpleXMLRPCServer.SimpleXMLRPCRequestHandler.parse_request(self): return False - if self.require_auth: - try: - self.authenticate() - except self.CouldNotAuthenticate, e: - self.logger.error("Authentication failed: %s" % e.args[0]) - code = 401 - message, _ = self.responses[401] - self.send_error(code, message) + try: + if not self.authenticate(): + self.logger.error("Authentication Failure") + self.send_error(401, self.responses[401][0]) return False + except: + self.logger.error("Unexpected Authentication Failure", exc_info=1) + self.send_error(401, self.responses[401][0]) + return False return True - ### FIXME need to override do_POST here + ### need to override do_POST here def do_POST(self): try: max_chunk_size = 10*1024*1024 @@ -175,7 +172,7 @@ class XMLRPCRequestHandler (SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): size_remaining -= len(L[-1]) data = ''.join(L) - response = self.server._marshaled_dispatch(data) + response = self.server._marshaled_dispatch(self.client_address, data) except: raise self.send_response(500) @@ -215,7 +212,7 @@ class XMLRPCServer (SocketServer.ThreadingMixIn, SSLServer, """ def __init__ (self, server_address, RequestHandlerClass=None, - keyfile=None, certfile=None, + keyfile=None, certfile=None, ca=None, timeout=10, logRequests=False, register=True, allow_none=True, encoding=None): @@ -242,7 +239,7 @@ class XMLRPCServer (SocketServer.ThreadingMixIn, SSLServer, """A subclassed request handler to prevent class-attribute conflicts.""" SSLServer.__init__(self, - server_address, RequestHandlerClass, + server_address, RequestHandlerClass, ca=ca, timeout=timeout, keyfile=keyfile, certfile=certfile) self.logRequests = logRequests self.serve = False @@ -291,7 +288,7 @@ class XMLRPCServer (SocketServer.ThreadingMixIn, SSLServer, except AttributeError: name = "unknown" self.logger.info("serving %s at %s" % (name, self.url)) - + def serve_forever (self): """Serve single requests until (self.serve == False).""" self.serve = True diff --git a/src/lib/Server/Plugins/Metadata.py b/src/lib/Server/Plugins/Metadata.py index 86d06d820..f7cf196fc 100644 --- a/src/lib/Server/Plugins/Metadata.py +++ b/src/lib/Server/Plugins/Metadata.py @@ -421,8 +421,11 @@ class Metadata(Bcfg2.Server.Plugin.Plugin, setattr(imd, source, data) imd.connectors.append(source) - def AuthenticateConnection(self, user, password, address): - '''This function checks user and password''' + def AuthenticateConnection(self, cert, user, password, address): + '''This function checks auth creds''' + if cert: + self.logger.error("Cert checking not yet implemented") + return False if user == 'root': # we aren't using per-client keys try: diff --git a/src/lib/Server/XMLRPC.py b/src/lib/Server/XMLRPC.py new file mode 100644 index 000000000..5788901cc --- /dev/null +++ b/src/lib/Server/XMLRPC.py @@ -0,0 +1,159 @@ + +import hashlib +import logging +import lxml.etree +import select +import socket +import time +import xmlrpclib + +from Bcfg2.Component import Component, exposed +import Bcfg2.Server.Core + +logger = logging.getLogger('server') + +def critical_error(operation): + '''Log and err, traceback and return an xmlrpc fault to client''' + logger.error(operation, exc_info=1) + raise xmlrpclib.Fault(7, "Critical unexpected failure: %s" % (operation)) + +class SetupError(Exception): + '''Used when the server cant be setup''' + pass + +class bcfg2_server(Component, + Bcfg2.Server.Core.Core): + '''XML RPC interfaces for the server core''' + name = 'bcfg2-server' + + def __init__(self, setup): + Component.__init__(self) + Bcfg2.Server.Core.Core.__init__(self, setup['repo'], setup['plugins'], + setup['password'], + setup['encoding'], setup['filemonitor']) + self.process_initial_fam_events() + + def process_initial_fam_events(self): + events = False + while True: + try: + rsockinfo = select.select([self.fam.fileno()], [], [], 15)[0] + if not rsockinfo: + if events: + break + else: + logger.error("Hit event timeout without getting " + "any events; GAMIN/FAM problem?") + continue + events = True + i = 0 + while self.fam.Service() or i < 10: + i += 1 + time.sleep(0.1) + except socket.error: + continue + + @exposed + def GetProbes(self, address): + '''Fetch probes for a particular client''' + resp = lxml.etree.Element('probes') + try: + name = self.metadata.resolve_client(address) + meta = self.build_metadata(name) + + for plugin in [p for p in list(self.plugins.values()) \ + if isinstance(p, Bcfg2.Server.Plugin.Probing)]: + for probe in plugin.GetProbes(meta): + resp.append(probe) + return lxml.etree.tostring(resp, encoding='UTF-8', + xml_declaration=True) + except Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError: + warning = 'Client metadata resolution error for %s; check server log' % address[0] + self.logger.warning(warning) + raise xmlrpclib.Fault(6, warning) + except: + critical_error("error determining client probes") + + @exposed + def RecvProbeData(self, address, probedata): + '''Receive probe data from clients''' + try: + name = self.metadata.resolve_client(address) + meta = self.build_metadata(name) + except Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError: + warning = 'metadata consistency error' + self.logger.warning(warning) + raise xmlrpclib.Fault(6, warning) + # clear dynamic groups + self.metadata.cgroups[meta.hostname] = [] + try: + xpdata = lxml.etree.XML(probedata) + except: + self.logger.error("Failed to parse probe data from client %s" % \ + (address[0])) + return False + + sources = [] + [sources.append(data.get('source')) for data in xpdata + if data.get('source') not in sources] + for source in sources: + if source not in self.plugins: + self.logger.warning("Failed to locate plugin %s" % (source)) + continue + dl = [data for data in xpdata if data.get('source') == source] + try: + self.plugins[source].ReceiveData(meta, dl) + except: + logger.error("Failed to process probe data from client %s" % \ + (address[0]), exc_info=1) + return True + + @exposed + def AssertProfile(self, address, profile): + '''Set profile for a client''' + try: + client = self.metadata.resolve_client(address) + self.metadata.set_profile(client, profile, address) + except (Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError, + Bcfg2.Server.Plugins.Metadata.MetadataRuntimeError): + warning = 'metadata consistency error' + self.logger.warning(warning) + raise xmlrpclib.Fault(6, warning) + return True + + @exposed + def GetConfig(self, address, checksum=False): + '''Build config for a client''' + try: + client = self.metadata.resolve_client(address) + config = self.BuildConfiguration(client) + if checksum: + for cfile in config.findall('.//ConfigFile'): + if cfile.text != None: + csum = hashlib.md5() + csum.update(cfile.text) + cfile.set('checksum', csum.hexdigest()) + cfile.text = None + return lxml.etree.tostring(config, encoding='UTF-8', + xml_declaration=True) + except Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError: + self.logger.warning("Metadata consistency failure for %s" % (address)) + raise xmlrpclib.Fault(6, "Metadata consistency failure") + + @exposed + def RecvStats(self, address, stats): + '''Act on statistics upload''' + sdata = lxml.etree.XML(stats) + client = self.metadata.resolve_client(address) + self.process_statistics(client, sdata) + return "" + + def authenticate(self, cert, user, password, address): + print cert, user, password, address + return self.metadata.AuthenticateConnection(cert, user, password, address) + + @exposed + def GetDecisionList(self, address, mode): + client = self.metadata.resolve_client(address) + meta = self.build_metadata(client) + return self.GetDecisions(meta, mode) diff --git a/src/sbin/bcfg2-server b/src/sbin/bcfg2-server index fa6605f1c..6086f4c2c 100755 --- a/src/sbin/bcfg2-server +++ b/src/sbin/bcfg2-server @@ -4,209 +4,19 @@ __revision__ = '$Revision$' import logging -import md5 -import select -import socket import sys -import time from xmlrpclib import Fault -from lxml.etree import XML, Element, tostring import Bcfg2.Logger import Bcfg2.Options import Bcfg2.Component import Bcfg2.Daemon import Bcfg2.Server.Plugins.Metadata -from Bcfg2.Server.Core import Core, CoreInitError +from Bcfg2.Server.Core import CoreInitError +import Bcfg2.Server.XMLRPC logger = logging.getLogger('bcfg2-server') -def critical_error(operation): - '''Log and err, traceback and return an xmlrpc fault to client''' - logger.error(operation, exc_info=1) - raise Fault(7, "Critical unexpected failure: %s" % (operation)) - -class SetupError(Exception): - '''Used when the server cant be setup''' - pass - -class Bcfg2Serv(Bcfg2.Component.Component): - """The Bcfg2 Server component providing XML-RPC access to Bcfg2 methods""" - __name__ = 'bcfg2' - __implementation__ = 'bcfg2' - fork_funcs = ['GetConfig', 'GetProbes'] - - request_queue_size = 15 - - def __init__(self, setup): - try: - self.Core = Core(setup['repo'], setup['plugins'], - setup['password'], - setup['encoding'], setup['filemonitor']) - except CoreInitError, msg: - logger.critical("Fatal error: %s" % (msg)) - raise SystemExit(1) - - if 'DBStats' in self.Core.plugins: - self.fork_funcs.append("RecvStats") - - famfd = self.Core.fam.fileno() - events = False - while True: - try: - rsockinfo = select.select([famfd], [], [], 15)[0] - if not rsockinfo: - if events: - break - else: - logger.error("Hit event timeout without getting " - "any events; GAMIN/FAM problem?") - continue - events = True - i = 0 - while self.Core.fam.Service() or i < 10: - i += 1 - time.sleep(0.1) - except socket.error: - continue - try: - Bcfg2.Component.Component.__init__(self, setup['key'], - setup['cert'], - setup['password'], - setup['location']) - except Bcfg2.Component.ComponentInitError: - raise SetupError - - self.funcs.update({ - "AssertProfile" : self.Bcfg2AssertProfile, - "GetConfig" : self.Bcfg2GetConfig, - "GetProbes" : self.Bcfg2GetProbes, - "RecvProbeData" : self.Bcfg2RecvProbeData, - "RecvStats" : self.Bcfg2RecvStats, - "GetDecisionList" : self.Bcfg2GetDecisionList - }) - - # init functions to be exposed as XML-RPC functions - for plugin in list(self.Core.plugins.values()): - for method in plugin.__rmi__: - self.register_function(getattr(self.Core.plugins[plugin.__name__], method), - "%s.%s" % (plugin.__name__, method)) - - - - def get_request(self): - ''' - We need to do work between requests, so select - with timeout instead of blocking in accept - ''' - rsockinfo = [] - famfd = self.Core.fam.fileno() - while self.socket not in rsockinfo: - self.clean_up_children() - if self.shut: - raise socket.error - try: - rsockinfo = select.select([self.socket, famfd], [], [], 15)[0] - except select.error: - continue - - if famfd in rsockinfo: - self.Core.Service() - if self.socket in rsockinfo: - return self.socket.accept() - - def Bcfg2GetProbes(self, address): - '''Fetch probes for a particular client''' - resp = Element('probes') - try: - name = self.Core.metadata.resolve_client(address) - meta = self.Core.build_metadata(name) - - for plugin in [p for p in list(self.Core.plugins.values()) \ - if isinstance(p, Bcfg2.Server.Plugin.Probing)]: - for probe in plugin.GetProbes(meta): - resp.append(probe) - return tostring(resp, encoding='UTF-8', xml_declaration=True) - except Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError: - warning = 'Client metadata resolution error for %s; check server log' % address[0] - self.logger.warning(warning) - raise Fault(6, warning) - except: - critical_error("error determining client probes") - - def Bcfg2RecvProbeData(self, address, probedata): - '''Receive probe data from clients''' - try: - name = self.Core.metadata.resolve_client(address) - meta = self.Core.build_metadata(name) - except Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError: - warning = 'metadata consistency error' - self.logger.warning(warning) - raise Fault(6, warning) - # clear dynamic groups - self.Core.metadata.cgroups[meta.hostname] = [] - try: - xpdata = XML(probedata) - except: - self.logger.error("Failed to parse probe data from client %s" % (address[0])) - return False - - sources = [] - [sources.append(data.get('source')) for data in xpdata - if data.get('source') not in sources] - for source in sources: - if source not in self.Core.plugins: - self.logger.warning("Failed to locate plugin %s" % (source)) - continue - dl = [data for data in xpdata if data.get('source') == source] - try: - self.Core.plugins[source].ReceiveData(meta, dl) - except: - self.logger.error("Failed to process probe data from client %s" % (address[0]), exc_info=1) - return True - - def Bcfg2AssertProfile(self, address, profile): - '''Set profile for a client''' - try: - client = self.Core.metadata.resolve_client(address) - self.Core.metadata.set_profile(client, profile, address) - except (Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError, Bcfg2.Server.Plugins.Metadata.MetadataRuntimeError): - warning = 'metadata consistency error' - self.logger.warning(warning) - raise Fault(6, warning) - return True - - def Bcfg2GetConfig(self, address, checksum=False): - '''Build config for a client''' - try: - client = self.Core.metadata.resolve_client(address) - config = self.Core.BuildConfiguration(client) - if checksum: - for cfile in config.findall('.//ConfigFile'): - if cfile.text != None: - csum = md5.md5(cfile.text) - cfile.set('checksum', csum.hexdigest()) - cfile.text = None - return tostring(config, encoding='UTF-8', xml_declaration=True) - except Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError: - self.logger.warning("Metadata consistency failure for %s" % (address)) - raise Fault(6, "Metadata consistency failure") - - def Bcfg2RecvStats(self, address, stats): - '''Act on statistics upload''' - sdata = XML(stats) - client = self.Core.metadata.resolve_client(address) - self.Core.process_statistics(client, sdata) - return "" - - def _authenticate_connection(self, _, user, password, address): - return self.Core.metadata.AuthenticateConnection(user, password, address) - - def Bcfg2GetDecisionList(self, address, mode): - client = self.Core.metadata.resolve_client(address) - meta = self.Core.build_metadata(client) - return self.Core.GetDecisions(meta, mode) - if __name__ == '__main__': OPTINFO = { @@ -235,30 +45,14 @@ if __name__ == '__main__': setup = Bcfg2.Options.OptionParser(OPTINFO) setup.parse(sys.argv[1:]) - - level = 0 - if setup['daemon']: - Bcfg2.Logger.setup_logging('bcfg2-server', - to_console=False, - level=level, - to_file=setup['filelog']) - Bcfg2.Daemon.daemonize(setup['daemon']) - else: - Bcfg2.Logger.setup_logging('bcfg2-server', - level=level, - to_file=setup['filelog']) - - if not setup['key']: - print("No key specified in '%s'" % setup['configfile']) - raise SystemExit(1) - try: - BSERV = Bcfg2Serv(setup) - except SetupError: - raise SystemExit(1) - while not BSERV.shut: - try: - BSERV.serve_forever() - except: - critical_error('error in service loop') - logger.info("Shutting down") + Bcfg2.Component.run_component(Bcfg2.Server.XMLRPC.bcfg2_server, + register=False, cls_kwargs={'setup':setup}, + keyfile='/home/desai/tmp/cert2/b2-s/priv.key', + certfile='/home/desai/tmp/cert2/b2-s/cert.pem', + ca='/home/desai/tmp/cert2/ca-cert.pem', + ) + except KeyboardInterrupt: + sys.exit(1) + sys.exit(1) + -- cgit v1.2.3-1-g7c22