summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristian Heimes <cheimes@redhat.com>2015-10-06 15:44:13 +0200
committerSimo Sorce <simo@redhat.com>2015-10-19 12:18:30 -0400
commit92e35e55d82e7cbb125da0c32eacec080eea2a54 (patch)
tree9c030734543e3d618fc12999dd2648286e363ee4
parentb20b47b100b2716273a5abfe2850e994c1d3e69d (diff)
downloadcustodia-92e35e55d82e7cbb125da0c32eacec080eea2a54.tar.gz
custodia-92e35e55d82e7cbb125da0c32eacec080eea2a54.tar.xz
custodia-92e35e55d82e7cbb125da0c32eacec080eea2a54.zip
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 <cheimes@redhat.com> Signed-off-by: Simo Sorce <simo@redhat.com>
-rw-r--r--.gitignore5
-rwxr-xr-xcustodia/custodia16
-rw-r--r--custodia/httpd/authenticators.py25
-rw-r--r--custodia/httpd/authorizers.py10
-rw-r--r--custodia/httpd/server.py94
-rw-r--r--custodia/secrets.py1
-rw-r--r--tests/custodia.py5
7 files changed, 107 insertions, 49 deletions
diff --git a/.gitignore b/.gitignore
index 9df8310..5861d81 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,8 @@ cscope.out
.tox
.coverage
MANIFEST
+
+/custodia.audit.log
+/secrets.db
+/testlog.txt
+
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)
diff --git a/tests/custodia.py b/tests/custodia.py
index ed80010..dc4a662 100644
--- a/tests/custodia.py
+++ b/tests/custodia.py
@@ -26,8 +26,11 @@ class CustodiaTests(unittest.TestCase):
with (open('testlog.txt', 'a')) as logfile:
p = subprocess.Popen([pexec, 'custodia/custodia'], env=env,
stdout=logfile, stderr=logfile)
- cls.custodia_process = p
time.sleep(1)
+ if p.poll() is not None:
+ raise AssertionError(
+ "Premature termination of Custodia server, see testlog.txt")
+ cls.custodia_process = p
cls.client = CustodiaClient('http+unix://%2E%2Fserver_socket/secrets')
cls.client.headers['REMOTE_USER'] = 'test'
cls.fwd = CustodiaClient('http+unix://%2E%2Fserver_socket/forwarder')