From 92e35e55d82e7cbb125da0c32eacec080eea2a54 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Tue, 6 Oct 2015 15:44:13 +0200 Subject: Add support for using listening on TCP sockets The server can be now configured using a new parameter called "server_url". Setting server_url to "http://0.0.0.0:80/" will make the server listen on TCP port 80, while setting it to "http+unix://%2fsocket" will make the server listen on the unix socket named "/socket". The backwards compatible "server_socket" is retained and used if no server_url is provided. The request dict has a new field "client_id" that contains either a PID or a peer name. In the future the field can be augmented with a TLS client cert DN or other similar identifier. Signed-off-by: Christian Heimes Signed-off-by: Simo Sorce --- custodia/custodia | 16 ++++--- custodia/httpd/authenticators.py | 25 ++++++----- custodia/httpd/authorizers.py | 10 ++--- custodia/httpd/server.py | 94 +++++++++++++++++++++++++++++----------- custodia/secrets.py | 1 + 5 files changed, 98 insertions(+), 48 deletions(-) (limited to 'custodia') diff --git a/custodia/custodia b/custodia/custodia index 0aa7986..f6a9f3f 100755 --- a/custodia/custodia +++ b/custodia/custodia @@ -4,9 +4,11 @@ try: from ConfigParser import RawConfigParser + from urllib import quote as url_escape except ImportError: from configparser import RawConfigParser -from custodia.httpd.server import LocalHTTPServer + from urllib.parse import quote as url_escape +from custodia.httpd.server import HTTPServer from custodia import log import importlib import os @@ -107,9 +109,11 @@ if __name__ == '__main__': if config.get('debug') == 'True': log.DEBUG = True - if 'server_socket' in config: - address = config['server_socket'] - else: - address = os.path.join(os.getcwd(), 'server_socket') - httpd = LocalHTTPServer(address, config) + url = config.get('server_url', None) + if url is None: + address = config.get('server_socket', + os.path.join(os.getcwd(), 'server_socket')) + url = 'http+unix://%s/' % url_escape(address, '') + + httpd = HTTPServer(url, config) httpd.serve() diff --git a/custodia/httpd/authenticators.py b/custodia/httpd/authenticators.py index bed2bc4..33166ec 100644 --- a/custodia/httpd/authenticators.py +++ b/custodia/httpd/authenticators.py @@ -30,16 +30,19 @@ class SimpleCredsAuth(HTTPAuthenticator): self._gid = int(self.config['gid']) def handle(self, request): - uid = int(request['creds']['gid']) - gid = int(request['creds']['uid']) + creds = request.get('creds') + if creds is None: + return False + uid = int(creds['gid']) + gid = int(creds['uid']) if self._gid == gid or self._uid == uid: self._auditlog.svc_access(log.AUDIT_SVC_AUTH_PASS, - request['creds']['pid'], + request['client_id'], "SCA", "%d, %d" % (uid, gid)) return True else: self._auditlog.svc_access(log.AUDIT_SVC_AUTH_FAIL, - request['creds']['pid'], + request['client_id'], "SCA", "%d, %d" % (uid, gid)) return False @@ -65,23 +68,23 @@ class SimpleHeaderAuth(HTTPAuthenticator): elif isinstance(self.value, str): if value != self.value: self._auditlog.svc_access(log.AUDIT_SVC_AUTH_FAIL, - request['creds']['pid'], + request['client_id'], "SHA", value) return False elif isinstance(self.value, list): if value not in self.value: self._auditlog.svc_access(log.AUDIT_SVC_AUTH_FAIL, - request['creds']['pid'], + request['client_id'], "SHA", value) return False else: self._auditlog.svc_access(log.AUDIT_SVC_AUTH_FAIL, - request['creds']['pid'], + request['client_id'], "SHA", value) return False self._auditlog.svc_access(log.AUDIT_SVC_AUTH_PASS, - request['creds']['pid'], + request['client_id'], "SHA", value) request['remote_user'] = value return True @@ -116,18 +119,18 @@ class SimpleAuthKeys(HTTPAuthenticator): validated = True except Exception: self._auditlog.svc_access(log.AUDIT_SVC_AUTH_FAIL, - request['creds']['pid'], + request['client_id'], "SAK", name) return False if validated: self._auditlog.svc_access(log.AUDIT_SVC_AUTH_PASS, - request['creds']['pid'], + request['client_id'], "SAK", name) request['remote_user'] = name return True self._auditlog.svc_access(log.AUDIT_SVC_AUTH_FAIL, - request['creds']['pid'], + request['client_id'], "SAK", name) return False diff --git a/custodia/httpd/authorizers.py b/custodia/httpd/authorizers.py index d6fe7c7..3758f3c 100644 --- a/custodia/httpd/authorizers.py +++ b/custodia/httpd/authorizers.py @@ -40,14 +40,14 @@ class SimplePathAuthz(HTTPAuthorizer): authz = authz[:-1] if authz == path: self._auditlog.svc_access(log.AUDIT_SVC_AUTHZ_PASS, - request['creds']['pid'], + request['client_id'], "SPA", path) return True while path != '': if path in self.paths: self._auditlog.svc_access(log.AUDIT_SVC_AUTHZ_PASS, - request['creds']['pid'], + request['client_id'], "SPA", path) return True if path == '/': @@ -73,7 +73,7 @@ class UserNameSpace(HTTPAuthorizer): if name is None: # UserNameSpace requires a user ... self._auditlog.svc_access(log.AUDIT_SVC_AUTHZ_FAIL, - request.get('creds', {'pid': 0})['pid'], + request['client_id'], "UNS(%s)" % self.path, path) return False @@ -81,12 +81,12 @@ class UserNameSpace(HTTPAuthorizer): if not path.startswith(namespace): # Not in the namespace self._auditlog.svc_access(log.AUDIT_SVC_AUTHZ_FAIL, - request.get('creds', {'pid': 0})['pid'], + request['client_id'], "UNS(%s)" % self.path, path) return False request['default_namespace'] = name self._auditlog.svc_access(log.AUDIT_SVC_AUTHZ_PASS, - request.get('creds', {'pid': 0})['pid'], + request['client_id'], "UNS(%s)" % self.path, path) return True diff --git a/custodia/httpd/server.py b/custodia/httpd/server.py index 8f02a78..dfc89d6 100644 --- a/custodia/httpd/server.py +++ b/custodia/httpd/server.py @@ -11,13 +11,13 @@ import six try: # pylint: disable=import-error from BaseHTTPServer import BaseHTTPRequestHandler - from SocketServer import ForkingMixIn, UnixStreamServer + from SocketServer import ForkingTCPServer from urlparse import urlparse, parse_qs from urllib import unquote except ImportError: # pylint: disable=import-error,no-name-in-module from http.server import BaseHTTPRequestHandler - from socketserver import ForkingMixIn, UnixStreamServer + from socketserver import ForkingTCPServer from urllib.parse import urlparse, parse_qs, unquote from custodia import log @@ -39,8 +39,7 @@ class HTTPError(Exception): super(HTTPError, self).__init__(errstring) -class ForkingLocalHTTPServer(ForkingMixIn, UnixStreamServer): - +class ForkingHTTPServer(ForkingTCPServer): """ A forking HTTP Server. Each request runs into a forked server so that the whole environment @@ -50,13 +49,12 @@ class ForkingLocalHTTPServer(ForkingMixIn, UnixStreamServer): When a request is received it is parsed by the handler_class provided at server initialization. """ - server_string = "Custodia/0.1" allow_reuse_address = True socket_file = None def __init__(self, server_address, handler_class, config): - UnixStreamServer.__init__(self, server_address, handler_class) + ForkingTCPServer.__init__(self, server_address, handler_class) if 'consumers' not in config: raise ValueError('Configuration does not provide any consumer') self.config = config @@ -64,14 +62,20 @@ class ForkingLocalHTTPServer(ForkingMixIn, UnixStreamServer): self.server_string = self.config['server_string'] self._auditlog = log.AuditLog(self.config) + +class ForkingUnixHTTPServer(ForkingHTTPServer): + address_family = socket.AF_UNIX + def server_bind(self): oldmask = os.umask(000) - UnixStreamServer.server_bind(self) - os.umask(oldmask) + try: + ForkingHTTPServer.server_bind(self) + finally: + os.umask(oldmask) self.socket_file = self.socket.getsockname() -class LocalHTTPRequestHandler(BaseHTTPRequestHandler): +class HTTPRequestHandler(BaseHTTPRequestHandler): """ This request handler is a slight modification of BaseHTTPRequestHandler @@ -107,7 +111,6 @@ class LocalHTTPRequestHandler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.0" def __init__(self, *args, **kwargs): - BaseHTTPRequestHandler.__init__(self, *args, **kwargs) self.requestline = '' self.request_version = '' self.command = '' @@ -118,15 +121,21 @@ class LocalHTTPRequestHandler(BaseHTTPRequestHandler): self.url = None self.body = None self.loginuid = None + self._creds = False + BaseHTTPRequestHandler.__init__(self, *args, **kwargs) def version_string(self): return self.server.server_string def _get_loginuid(self, pid): loginuid = None + # NOTE: Using proc to find the login uid is not reliable + # this is why login uid is fetched separately and not stored + # into 'creds', to avoid giving the false impression it can be + # used to perform access control decisions try: - with open("/proc/" + str(pid) + "/loginuid", "r") as f: - loginuid = int(f.read(), 10) + with open("/proc/%i/loginuid" % pid, "r") as f: + loginuid = int(f.read()) except IOError as e: if e.errno != errno.ENOENT: raise @@ -136,6 +145,12 @@ class LocalHTTPRequestHandler(BaseHTTPRequestHandler): @property def peer_creds(self): + if self._creds is not False: + return self._creds + # works only for unix sockets + if self.request.family != socket.AF_UNIX: + self._creds = None + return self._creds creds = self.request.getsockopt(socket.SOL_SOCKET, SO_PEERCRED, struct.calcsize('3i')) pid, uid, gid = struct.unpack('3i', creds) @@ -147,7 +162,16 @@ class LocalHTTPRequestHandler(BaseHTTPRequestHandler): log.debug("Couldn't retrieve SELinux Context: (%s)" % str(e)) context = None - return {'pid': pid, 'uid': uid, 'gid': gid, 'context': context} + self._creds = {'pid': pid, 'uid': uid, 'gid': gid, 'context': context} + return self._creds + + @property + def peer_info(self): + if self.peer_creds is not None: + return self._creds['pid'] + elif self.request.family in {socket.AF_INET, socket.AF_INET6}: + return self.request.getpeername() + return None def parse_request(self, *args, **kwargs): if not BaseHTTPRequestHandler.parse_request(self, *args, **kwargs): @@ -155,7 +179,8 @@ class LocalHTTPRequestHandler(BaseHTTPRequestHandler): # grab the loginuid from `/proc` as soon as possible creds = self.peer_creds - self.loginuid = self._get_loginuid(creds['pid']) + if creds is not None: + self.loginuid = self._get_loginuid(creds['pid']) # after basic parsing also use urlparse to retrieve individual # elements of a request. @@ -182,8 +207,9 @@ class LocalHTTPRequestHandler(BaseHTTPRequestHandler): self.body = self.rfile.read(length) def handle_one_request(self): - # Set a fake client address to make log functions happy - self.client_address = ['127.0.0.1', 0] + if self.request.family == socket.AF_UNIX: + # Set a fake client address to make log functions happy + self.client_address = ['127.0.0.1', 0] try: if not self.server.config: self.close_connection = 1 @@ -209,6 +235,7 @@ class LocalHTTPRequestHandler(BaseHTTPRequestHandler): self.wfile.flush() return request = {'creds': self.peer_creds, + 'client_id': self.peer_info, 'command': self.command, 'path': self.path, 'query': self.query, @@ -300,7 +327,7 @@ class LocalHTTPRequestHandler(BaseHTTPRequestHandler): valid_once = True if valid_once is not True: self.server._auditlog.svc_access(log.AUDIT_SVC_AUTH_FAIL, - request['creds']['pid'], "MAIN", + request['client_id'], "MAIN", 'No auth') raise HTTPError(403) @@ -314,7 +341,7 @@ class LocalHTTPRequestHandler(BaseHTTPRequestHandler): break if valid is not True: self.server._auditlog.svc_access(log.AUDIT_SVC_AUTHZ_FAIL, - request['creds']['pid'], "MAIN", + request['client_id'], "MAIN", request.get('path', '/')) raise HTTPError(403) @@ -340,15 +367,30 @@ class LocalHTTPRequestHandler(BaseHTTPRequestHandler): raise HTTPError(404) -class LocalHTTPServer(object): +class HTTPServer(object): + + def __init__(self, srvurl, config): + url = urlparse(srvurl) + address = unquote(url.netloc) + if url.scheme == 'http+unix': + # Unix socket + serverclass = ForkingUnixHTTPServer + if address[0] != '/': + raise ValueError('Must use absolute unix socket name') + if os.path.exists(address): + os.remove(address) + elif url.scheme == 'http': + host, port = address.split(":") + address = (host, int(port)) + serverclass = ForkingHTTPServer + elif url.scheme == 'https': + raise NotImplementedError + else: + raise ValueError('Unknown URL Scheme: %s' % url.scheme) - def __init__(self, address, config): - if address[0] != '/': - raise ValueError('Must use absolute unix socket name') - if os.path.exists(address): - os.remove(address) - self.httpd = ForkingLocalHTTPServer(address, LocalHTTPRequestHandler, - config) + self.httpd = serverclass(address, + HTTPRequestHandler, + config) def get_socket(self): return (self.httpd.socket, self.httpd.socket_file) diff --git a/custodia/secrets.py b/custodia/secrets.py index a009dcb..90dac4c 100644 --- a/custodia/secrets.py +++ b/custodia/secrets.py @@ -261,6 +261,7 @@ class SecretsTests(unittest.TestCase): pass def check_authz(self, req): + req['client_id'] = 'test' req['path'] = '/'.join([''] + req.get('trail', [])) if self.authz.handle(req) is False: raise HTTPError(403) -- cgit