Import Upstream version 4.12.4
This commit is contained in:
0
ipaserver/custodia/httpd/__init__.py
Normal file
0
ipaserver/custodia/httpd/__init__.py
Normal file
52
ipaserver/custodia/httpd/authenticators.py
Normal file
52
ipaserver/custodia/httpd/authenticators.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# Copyright (C) 2015 Custodia Project Contributors - see LICENSE file
|
||||
from __future__ import absolute_import
|
||||
|
||||
from ipaserver.custodia import log
|
||||
from ipaserver.custodia.plugin import HTTPAuthenticator, PluginOption
|
||||
|
||||
|
||||
class SimpleCredsAuth(HTTPAuthenticator):
|
||||
uid = PluginOption('pwd_uid', -1, "User id or name, -1 ignores user")
|
||||
gid = PluginOption('grp_gid', -1, "Group id or name, -1 ignores group")
|
||||
|
||||
def handle(self, request):
|
||||
creds = request.get('creds')
|
||||
if creds is None:
|
||||
self.logger.debug('SCA: Missing "creds" from request')
|
||||
return False
|
||||
uid = int(creds['uid'])
|
||||
gid = int(creds['gid'])
|
||||
uid_match = self.uid != -1 and self.uid == uid
|
||||
gid_match = self.gid != -1 and self.gid == gid
|
||||
if uid_match or gid_match:
|
||||
self.audit_svc_access(log.AUDIT_SVC_AUTH_PASS,
|
||||
request['client_id'],
|
||||
"%d, %d" % (uid, gid))
|
||||
return True
|
||||
else:
|
||||
self.audit_svc_access(log.AUDIT_SVC_AUTH_FAIL,
|
||||
request['client_id'],
|
||||
"%d, %d" % (uid, gid))
|
||||
return False
|
||||
|
||||
|
||||
class SimpleHeaderAuth(HTTPAuthenticator):
|
||||
header = PluginOption(str, 'REMOTE_USER', "header name")
|
||||
value = PluginOption('str_set', None,
|
||||
"Comma-separated list of required values")
|
||||
|
||||
def handle(self, request):
|
||||
if self.header not in request['headers']:
|
||||
self.logger.debug('SHA: No "headers" in request')
|
||||
return None
|
||||
value = request['headers'][self.header]
|
||||
if self.value is not None:
|
||||
if value not in self.value:
|
||||
self.audit_svc_access(log.AUDIT_SVC_AUTH_FAIL,
|
||||
request['client_id'], value)
|
||||
return False
|
||||
|
||||
self.audit_svc_access(log.AUDIT_SVC_AUTH_PASS,
|
||||
request['client_id'], value)
|
||||
request['remote_user'] = value
|
||||
return True
|
||||
47
ipaserver/custodia/httpd/authorizers.py
Normal file
47
ipaserver/custodia/httpd/authorizers.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# Copyright (C) 2015 Custodia Project Contributors - see LICENSE file
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
|
||||
from ipaserver.custodia import log
|
||||
from ipaserver.custodia.plugin import HTTPAuthorizer
|
||||
|
||||
|
||||
class SimplePathAuthz(HTTPAuthorizer):
|
||||
# keep SimplePathAuthz an old-style plugin for now.
|
||||
# KEMKeysStore and IPAKEMKeys haven't been ported.
|
||||
|
||||
def __init__(self, config):
|
||||
super(SimplePathAuthz, self).__init__(config)
|
||||
self.paths = []
|
||||
if 'paths' in self.config:
|
||||
self.paths = self.config['paths'].split()
|
||||
|
||||
def handle(self, request):
|
||||
reqpath = path = request.get('path', '')
|
||||
|
||||
# if an authorized path does not end in /
|
||||
# check if it matches fullpath for strict match
|
||||
for authz in self.paths:
|
||||
if authz.endswith('/'):
|
||||
continue
|
||||
if authz.endswith('.'):
|
||||
# special case to match a path ending in /
|
||||
authz = authz[:-1]
|
||||
if authz == path:
|
||||
self.audit_svc_access(log.AUDIT_SVC_AUTHZ_PASS,
|
||||
request['client_id'], path)
|
||||
return True
|
||||
|
||||
while path != '':
|
||||
if path in self.paths:
|
||||
self.audit_svc_access(log.AUDIT_SVC_AUTHZ_PASS,
|
||||
request['client_id'], path)
|
||||
return True
|
||||
if path == '/':
|
||||
path = ''
|
||||
else:
|
||||
path, _head = os.path.split(path)
|
||||
|
||||
self.logger.debug('No path in %s matched %s', self.paths, reqpath)
|
||||
return None
|
||||
507
ipaserver/custodia/httpd/server.py
Normal file
507
ipaserver/custodia/httpd/server.py
Normal file
@@ -0,0 +1,507 @@
|
||||
# Copyright (C) 2015 Custodia Project Contributors - see LICENSE file
|
||||
from __future__ import absolute_import
|
||||
|
||||
import atexit
|
||||
import errno
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
from socketserver import ForkingTCPServer, BaseServer
|
||||
from urllib.parse import parse_qs, unquote, urlparse
|
||||
|
||||
import six
|
||||
|
||||
from ipaserver.custodia import log
|
||||
from ipaserver.custodia.plugin import HTTPError
|
||||
|
||||
try:
|
||||
from systemd import daemon as sd
|
||||
except ImportError:
|
||||
sd = None
|
||||
if 'NOTIFY_SOCKET' in os.environ:
|
||||
warnings.warn(
|
||||
"NOTIFY_SOCKET env var is set but python-systemd bindings are "
|
||||
"not available!",
|
||||
category=RuntimeWarning
|
||||
)
|
||||
if 'LISTEN_FDS' in os.environ:
|
||||
warnings.warn(
|
||||
"LISTEN_FDS env var is set, but python-systemd bindings are"
|
||||
"not available!",
|
||||
category=RuntimeWarning
|
||||
)
|
||||
|
||||
|
||||
logger = log.getLogger(__name__)
|
||||
|
||||
SO_PEERCRED = getattr(socket, 'SO_PEERCRED', 17)
|
||||
SO_PEERSEC = getattr(socket, 'SO_PEERSEC', 31)
|
||||
SELINUX_CONTEXT_LEN = 256
|
||||
MAX_REQUEST_SIZE = 10 * 1024 * 1024 # For now limit body to 10MiB
|
||||
|
||||
|
||||
class ForkingHTTPServer(ForkingTCPServer):
|
||||
"""
|
||||
A forking HTTP Server.
|
||||
Each request runs into a forked server so that the whole environment
|
||||
is clean and isolated, and parallel requests cannot unintentionally
|
||||
influence one another.
|
||||
|
||||
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,
|
||||
bind_and_activate=True):
|
||||
# pylint: disable=non-parent-init-called
|
||||
# Init just BaseServer, TCPServer creates a socket.
|
||||
BaseServer.__init__(self, server_address, handler_class)
|
||||
|
||||
if isinstance(server_address, socket.socket):
|
||||
# It's a bound and activates socket from systemd.
|
||||
self.socket = server_address
|
||||
bind_and_activate = False
|
||||
else:
|
||||
self.socket = socket.socket(self.address_family,
|
||||
self.socket_type)
|
||||
|
||||
# copied from TCPServer
|
||||
if bind_and_activate:
|
||||
try:
|
||||
self.server_bind()
|
||||
self.server_activate()
|
||||
except BaseException:
|
||||
self.server_close()
|
||||
raise
|
||||
|
||||
if self.socket.family == socket.AF_UNIX:
|
||||
self.socket_file = self.socket.getsockname()
|
||||
|
||||
if 'consumers' not in config:
|
||||
raise ValueError('Configuration does not provide any consumer')
|
||||
self.config = config
|
||||
if 'server_string' in self.config:
|
||||
self.server_string = self.config['server_string']
|
||||
self.auditlog = log.auditlog
|
||||
|
||||
|
||||
class ForkingUnixHTTPServer(ForkingHTTPServer):
|
||||
address_family = socket.AF_UNIX
|
||||
|
||||
def server_bind(self):
|
||||
self.unlink()
|
||||
# Remove on exit
|
||||
atexit.register(self.unlink)
|
||||
basedir = os.path.dirname(self.server_address)
|
||||
if not os.path.isdir(basedir):
|
||||
os.makedirs(basedir, mode=0o755)
|
||||
ForkingHTTPServer.server_bind(self)
|
||||
os.chmod(self.server_address, 0o666)
|
||||
|
||||
def unlink(self):
|
||||
try:
|
||||
os.unlink(self.server_address)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
class HTTPRequestHandler(BaseHTTPRequestHandler):
|
||||
|
||||
"""
|
||||
This request handler is a slight modification of BaseHTTPRequestHandler
|
||||
where the per-request handler is replaced.
|
||||
|
||||
When a request comes in it is parsed and the 'request' dictionary is
|
||||
populated accordingly. Additionally a 'creds' structure is added to the
|
||||
request.
|
||||
|
||||
The 'creds' structure contains the data retrieved via a call to
|
||||
getsockopt with the SO_PEERCRED option. This retrieves via kernel assist
|
||||
the uid,gid and pid of the process on the other side of the unix socket
|
||||
on which the request has been made. This can be used for authentication
|
||||
and/or authorization purposes.
|
||||
The 'creds' structure is further augmented with a 'context' option
|
||||
containing the Selinux Context string for the calling process, if
|
||||
available.
|
||||
|
||||
after the request is parsed the server's pipeline() function is invoked
|
||||
in order to handle it. The pipeline() should return a response object,
|
||||
where te return 'code', the 'output' and 'headers' may be found.
|
||||
|
||||
If no 'code' is present the request is assumed to be successful and a
|
||||
'200 OK' status code will be sent back to the client.
|
||||
|
||||
The 'output' parameter can be a string or a file like object.
|
||||
|
||||
The 'headers' objct must be a dictionary where keys are headers names.
|
||||
|
||||
By default we assume HTTP1.0
|
||||
"""
|
||||
|
||||
protocol_version = "HTTP/1.0"
|
||||
|
||||
def __init__(self, request, client_address, server):
|
||||
self.requestline = ''
|
||||
self.request_version = ''
|
||||
self.command = ''
|
||||
self.raw_requestline = None
|
||||
self.close_connection = 0
|
||||
self.path = None # quoted, raw path
|
||||
self.path_chain = None # tuple of unquoted segments
|
||||
self.query = None
|
||||
self.url = None
|
||||
self.body = None
|
||||
self.loginuid = None
|
||||
self._creds = False
|
||||
BaseHTTPRequestHandler.__init__(self, request, client_address, server)
|
||||
|
||||
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/%i/loginuid" % pid, "r") as f:
|
||||
loginuid = int(f.read())
|
||||
except IOError as e:
|
||||
if e.errno != errno.ENOENT:
|
||||
raise
|
||||
if loginuid == -1:
|
||||
loginuid = None
|
||||
return loginuid
|
||||
|
||||
@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
|
||||
# pid_t: signed int32, uid_t/gid_t: unsigned int32
|
||||
fmt = 'iII'
|
||||
creds = self.request.getsockopt(socket.SOL_SOCKET, SO_PEERCRED,
|
||||
struct.calcsize(fmt))
|
||||
pid, uid, gid = struct.unpack(fmt, creds)
|
||||
try:
|
||||
creds = self.request.getsockopt(socket.SOL_SOCKET, SO_PEERSEC,
|
||||
SELINUX_CONTEXT_LEN)
|
||||
context = creds.rstrip(b'\x00').decode('utf-8')
|
||||
except Exception: # pylint: disable=broad-except
|
||||
logger.debug("Couldn't retrieve SELinux Context", exc_info=True)
|
||||
context = None
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
def peer_cert(self):
|
||||
if not hasattr(self.request, 'getpeercert'):
|
||||
return None
|
||||
return self.request.getpeercert()
|
||||
|
||||
def parse_request(self):
|
||||
if not BaseHTTPRequestHandler.parse_request(self):
|
||||
return False
|
||||
|
||||
# grab the loginuid from `/proc` as soon as possible
|
||||
creds = self.peer_creds
|
||||
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.
|
||||
url = urlparse(self.path)
|
||||
|
||||
# Yes, override path with the path part only
|
||||
self.path = url.path
|
||||
self.path_chain = self._parse_path(url)
|
||||
|
||||
# Create dict out of query
|
||||
self.query = parse_qs(url.query)
|
||||
|
||||
# keep the rest into the 'url' element in case someone needs it
|
||||
self.url = url
|
||||
|
||||
return True
|
||||
|
||||
def _parse_path(self, url):
|
||||
path_chain = []
|
||||
for segment in url.path.split('/'):
|
||||
# unquote URL path encoding
|
||||
segment = unquote(segment)
|
||||
path_chain.append(segment)
|
||||
return tuple(path_chain)
|
||||
|
||||
def parse_body(self):
|
||||
length = int(self.headers.get('content-length', 0))
|
||||
if length > MAX_REQUEST_SIZE:
|
||||
raise HTTPError(413)
|
||||
if length == 0:
|
||||
self.body = None
|
||||
else:
|
||||
self.body = self.rfile.read(length)
|
||||
|
||||
def handle_one_request(self):
|
||||
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
|
||||
return
|
||||
self.raw_requestline = self.rfile.readline(65537)
|
||||
if not self.raw_requestline:
|
||||
self.close_connection = 1
|
||||
return
|
||||
if len(self.raw_requestline) > 65536:
|
||||
self.requestline = ''
|
||||
self.request_version = ''
|
||||
self.command = ''
|
||||
self.send_error(414)
|
||||
self.wfile.flush()
|
||||
return
|
||||
if not self.parse_request():
|
||||
self.close_connection = 1
|
||||
return
|
||||
try:
|
||||
self.parse_body()
|
||||
except HTTPError as e:
|
||||
self.send_error(e.code, e.mesg)
|
||||
self.wfile.flush()
|
||||
return
|
||||
request = {'creds': self.peer_creds,
|
||||
'client_cert': self.peer_cert,
|
||||
'client_id': self.peer_info,
|
||||
'command': self.command,
|
||||
'path': self.path,
|
||||
'path_chain': self.path_chain,
|
||||
'query': self.query,
|
||||
'url': self.url,
|
||||
'version': self.request_version,
|
||||
'headers': self.headers,
|
||||
'body': self.body}
|
||||
logger.debug(
|
||||
"REQUEST: %s %s, query: %r, cred: %r, client_id: %s, "
|
||||
"headers: %r, body: %r",
|
||||
request['command'], request['path_chain'], request['query'],
|
||||
request['creds'], request['client_id'],
|
||||
dict(request['headers']), request['body']
|
||||
)
|
||||
try:
|
||||
response = self.pipeline(self.server.config, request)
|
||||
if response is None:
|
||||
raise HTTPError(500)
|
||||
except HTTPError as e:
|
||||
self.send_error(e.code, e.mesg)
|
||||
self.wfile.flush()
|
||||
return
|
||||
except socket.timeout as e:
|
||||
self.log_error("Request timed out: %r", e)
|
||||
self.close_connection = 1
|
||||
return
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
self.log_error("Handler failed: %r", e, exc_info=True)
|
||||
self.send_error(500)
|
||||
self.wfile.flush()
|
||||
return
|
||||
|
||||
self.send_response(response.get('code', 200))
|
||||
for header, value in six.iteritems(response.get('headers', {})):
|
||||
self.send_header(header, value)
|
||||
self.end_headers()
|
||||
|
||||
output = response.get('output', None)
|
||||
if hasattr(output, 'read'):
|
||||
shutil.copyfileobj(output, self.wfile)
|
||||
output.close()
|
||||
elif output is not None:
|
||||
self.wfile.write(output)
|
||||
else:
|
||||
self.close_connection = 1
|
||||
self.wfile.flush()
|
||||
return
|
||||
except socket.timeout as e:
|
||||
self.log_error("Request timed out: %r", e)
|
||||
self.close_connection = 1
|
||||
return
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
def log_error(self, fmtstr, *args, **kwargs):
|
||||
logger.error(fmtstr, *args, **kwargs)
|
||||
|
||||
def pipeline(self, config, request):
|
||||
"""
|
||||
The pipeline() function handles authentication and invocation of
|
||||
the correct consumer based on the server configuration, that is
|
||||
provided at initialization time.
|
||||
|
||||
When authentication is performed all the authenticators are
|
||||
executed. If any returns False, authentication fails and a 403
|
||||
error is raised. If none of them positively succeeds and they all
|
||||
return None then also authentication fails and a 403 error is
|
||||
raised. Authentication plugins can add attributes to the request
|
||||
object for use of authorization or other plugins.
|
||||
|
||||
When authorization is performed and positive result will cause the
|
||||
operation to be accepted and any negative result will cause it to
|
||||
fail. If no authorization plugin returns a positive result a 403
|
||||
error is returned.
|
||||
|
||||
Once authentication and authorization are successful the pipeline
|
||||
will parse the path component and find the consumer plugin that
|
||||
handles the provided path walking up the path component by
|
||||
component until a consumer is found.
|
||||
|
||||
Paths are walked up from the leaf to the root, so if two consumers
|
||||
hang on the same tree, the one closer to the leaf will be used. If
|
||||
there is a trailing path when the conumer is selected then it will
|
||||
be stored in the request dicstionary named 'trail'. The 'trail' is
|
||||
an ordered list of the path components below the consumer entry
|
||||
point.
|
||||
"""
|
||||
path_chain = request['path_chain']
|
||||
if not path_chain or path_chain[0] != '':
|
||||
# no path or not an absolute path
|
||||
raise HTTPError(400)
|
||||
|
||||
# auth framework here
|
||||
authers = config.get('authenticators')
|
||||
if authers is None:
|
||||
raise HTTPError(403)
|
||||
valid_once = False
|
||||
for auth in authers:
|
||||
valid = authers[auth].handle(request)
|
||||
if valid is False:
|
||||
raise HTTPError(403)
|
||||
elif valid is True:
|
||||
valid_once = True
|
||||
if valid_once is not True:
|
||||
self.server.auditlog.svc_access(self.__class__.__name__,
|
||||
log.AUDIT_SVC_AUTH_FAIL,
|
||||
request['client_id'], 'No auth')
|
||||
raise HTTPError(403)
|
||||
|
||||
# auhz framework here
|
||||
authzers = config.get('authorizers')
|
||||
if authzers is None:
|
||||
raise HTTPError(403)
|
||||
authz_ok = None
|
||||
for authz in authzers:
|
||||
valid = authzers[authz].handle(request)
|
||||
if valid is True:
|
||||
authz_ok = True
|
||||
elif valid is False:
|
||||
authz_ok = False
|
||||
break
|
||||
if authz_ok is not True:
|
||||
self.server.auditlog.svc_access(self.__class__.__name__,
|
||||
log.AUDIT_SVC_AUTHZ_FAIL,
|
||||
request['client_id'],
|
||||
path_chain)
|
||||
raise HTTPError(403)
|
||||
|
||||
# Select consumer
|
||||
trail = []
|
||||
while path_chain:
|
||||
if path_chain in config['consumers']:
|
||||
con = config['consumers'][path_chain]
|
||||
if len(trail) != 0:
|
||||
request['trail'] = trail
|
||||
return con.handle(request)
|
||||
trail.insert(0, path_chain[-1])
|
||||
path_chain = path_chain[:-1]
|
||||
|
||||
raise HTTPError(404)
|
||||
|
||||
|
||||
class HTTPServer:
|
||||
handler = HTTPRequestHandler
|
||||
|
||||
def __init__(self, srvurl, config):
|
||||
url = urlparse(srvurl)
|
||||
serverclass, address = self._get_serverclass(url)
|
||||
if sd is not None:
|
||||
address = self._get_systemd_socket(address)
|
||||
self.httpd = serverclass(address, self.handler, config)
|
||||
|
||||
def _get_serverclass(self, url):
|
||||
if url.scheme == 'http+unix':
|
||||
# Unix socket
|
||||
address = unquote(url.netloc)
|
||||
if not address:
|
||||
raise ValueError('Empty address {}'.format(url))
|
||||
logger.info('Serving on Unix socket %s', address)
|
||||
serverclass = ForkingUnixHTTPServer
|
||||
elif url.scheme == 'http':
|
||||
host, port = url.netloc.split(":")
|
||||
address = (host, int(port))
|
||||
logger.info('Serving on %s (HTTP)', url.netloc)
|
||||
serverclass = ForkingHTTPServer
|
||||
else:
|
||||
raise ValueError('Unknown URL Scheme: %s' % url.scheme)
|
||||
return serverclass, address
|
||||
|
||||
def _get_systemd_socket(self, address):
|
||||
fds = sd.listen_fds()
|
||||
if not fds:
|
||||
return address
|
||||
elif len(fds) > 1:
|
||||
raise ValueError('Too many listening sockets', fds)
|
||||
|
||||
if isinstance(address, tuple):
|
||||
port = address[1]
|
||||
# systemd uses IPv6
|
||||
if not sd.is_socket_inet(fds[0], family=socket.AF_INET6,
|
||||
type=socket.SOCK_STREAM,
|
||||
listening=True, port=port):
|
||||
raise ValueError(
|
||||
"FD {} is not TCP IPv6 socket on port {}".format(
|
||||
fds[0], port
|
||||
)
|
||||
)
|
||||
logger.info('Using systemd socket activation on port %i', port)
|
||||
sock = socket.fromfd(fds[0], socket.AF_INET6, socket.SOCK_STREAM)
|
||||
else:
|
||||
if not sd.is_socket_unix(fds[0], socket.SOCK_STREAM,
|
||||
listening=True, path=address):
|
||||
raise ValueError(
|
||||
"FD {} is not Unix stream socket on path {}".format(
|
||||
fds[0], address
|
||||
)
|
||||
)
|
||||
logger.info('Using systemd socket activation on path %s', address)
|
||||
sock = socket.fromfd(fds[0], socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
|
||||
if sys.version_info[0] < 3:
|
||||
# Python 2.7's socket.fromfd() returns _socket.socket
|
||||
sock = socket.socket(_sock=sock)
|
||||
return sock
|
||||
|
||||
def get_socket(self):
|
||||
return (self.httpd.socket, self.httpd.socket_file)
|
||||
|
||||
def serve(self):
|
||||
if sd is not None and sd.booted():
|
||||
sd.notify("READY=1")
|
||||
return self.httpd.serve_forever()
|
||||
Reference in New Issue
Block a user