2016-03-21 21:42:39 +00:00
|
|
|
|
2017-07-05 15:22:03 +00:00
|
|
|
import falcon
|
2016-03-21 21:42:39 +00:00
|
|
|
import logging
|
2018-01-03 22:12:02 +00:00
|
|
|
from asn1crypto import pem, x509
|
2016-03-21 21:42:39 +00:00
|
|
|
|
|
|
|
logger = logging.getLogger("api")
|
|
|
|
|
|
|
|
def whitelist_subnets(subnets):
|
|
|
|
"""
|
|
|
|
Validate source IP address of API call against subnet list
|
|
|
|
"""
|
|
|
|
def wrapper(func):
|
|
|
|
def wrapped(self, req, resp, *args, **kwargs):
|
|
|
|
# Check for administration subnet whitelist
|
|
|
|
for subnet in subnets:
|
|
|
|
if req.context.get("remote_addr") in subnet:
|
|
|
|
break
|
|
|
|
else:
|
2017-12-30 13:57:48 +00:00
|
|
|
logger.info("Rejected access to administrative call %s by %s from %s, source address not whitelisted",
|
2016-03-21 21:42:39 +00:00
|
|
|
req.env["PATH_INFO"],
|
|
|
|
req.context.get("user", "unauthenticated user"),
|
|
|
|
req.context.get("remote_addr"))
|
2017-07-05 15:22:03 +00:00
|
|
|
raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % req.context.get("remote_addr"))
|
2016-03-21 21:42:39 +00:00
|
|
|
|
|
|
|
return func(self, req, resp, *args, **kwargs)
|
|
|
|
return wrapped
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
def whitelist_content_types(*content_types):
|
|
|
|
def wrapper(func):
|
|
|
|
def wrapped(self, req, resp, *args, **kwargs):
|
|
|
|
for content_type in content_types:
|
|
|
|
if req.get_header("Content-Type") == content_type:
|
|
|
|
return func(self, req, resp, *args, **kwargs)
|
|
|
|
raise falcon.HTTPUnsupportedMediaType(
|
|
|
|
"This API call accepts only %s content type" % ", ".join(content_types))
|
|
|
|
return wrapped
|
|
|
|
return wrapper
|
|
|
|
|
2017-07-05 15:22:03 +00:00
|
|
|
def whitelist_subject(func):
|
|
|
|
def wrapped(self, req, resp, cn, *args, **kwargs):
|
|
|
|
from ipaddress import ip_address
|
|
|
|
from certidude import authority
|
|
|
|
from xattr import getxattr
|
|
|
|
try:
|
2017-12-30 13:57:48 +00:00
|
|
|
path, buf, cert, signed, expires = authority.get_signed(cn)
|
2017-07-05 15:22:03 +00:00
|
|
|
except IOError:
|
|
|
|
raise falcon.HTTPNotFound()
|
|
|
|
else:
|
2018-01-03 22:12:02 +00:00
|
|
|
# First attempt to authenticate client with certificate
|
|
|
|
buf = req.get_header("X-SSL-CERT")
|
|
|
|
if buf:
|
|
|
|
header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii"))
|
|
|
|
origin_cert = x509.Certificate.load(der_bytes)
|
|
|
|
if origin_cert.native == cert.native:
|
2018-02-03 12:33:45 +00:00
|
|
|
logger.debug("Subject authenticated using certificates")
|
2018-01-03 22:12:02 +00:00
|
|
|
return func(self, req, resp, cn, *args, **kwargs)
|
|
|
|
|
|
|
|
# For backwards compatibility check source IP address
|
|
|
|
# TODO: make it disableable
|
2017-07-05 21:22:02 +00:00
|
|
|
try:
|
|
|
|
inner_address = getxattr(path, "user.lease.inner_address").decode("ascii")
|
|
|
|
except IOError:
|
2017-07-05 15:22:03 +00:00
|
|
|
raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % req.context.get("remote_addr"))
|
2017-07-05 21:22:02 +00:00
|
|
|
else:
|
|
|
|
if req.context.get("remote_addr") != ip_address(inner_address):
|
|
|
|
raise falcon.HTTPForbidden("Forbidden", "Remote address %s mismatch" % req.context.get("remote_addr"))
|
|
|
|
else:
|
2018-01-03 22:12:02 +00:00
|
|
|
return func(self, req, resp, cn, *args, **kwargs)
|
2017-07-05 15:22:03 +00:00
|
|
|
return wrapped
|