mirror of
https://github.com/GSA/notifications-api.git
synced 2025-12-08 14:12:27 -05:00
139 lines
4.9 KiB
Python
139 lines
4.9 KiB
Python
from flask import abort, current_app, request
|
|
from flask.wrappers import Request
|
|
|
|
TRACE_ID_HEADER = "X-B3-TraceId"
|
|
SPAN_ID_HEADER = "X-B3-SpanId"
|
|
PARENT_SPAN_ID_HEADER = "X-B3-ParentSpanId"
|
|
|
|
|
|
class NotifyRequest(Request):
|
|
"""
|
|
A custom Request class, implementing extraction of zipkin headers used to trace request through cloudfoundry
|
|
as described here: https://docs.cloudfoundry.org/concepts/http-routing.html#zipkin-headers
|
|
"""
|
|
|
|
@property
|
|
def request_id(self):
|
|
return self.trace_id
|
|
|
|
@property
|
|
def trace_id(self):
|
|
"""
|
|
The "trace id" (in zipkin terms) assigned to this request, if present (None otherwise)
|
|
"""
|
|
if not hasattr(self, "_trace_id"):
|
|
self._trace_id = self._get_header_value(TRACE_ID_HEADER)
|
|
return self._trace_id
|
|
|
|
@property
|
|
def span_id(self):
|
|
"""
|
|
The "span id" (in zipkin terms) set in this request's header, if present (None otherwise)
|
|
"""
|
|
if not hasattr(self, "_span_id"):
|
|
# note how we don't generate an id of our own. not being supplied a span id implies that we are running in
|
|
# an environment with no span-id-aware request router, and thus would have no intermediary to prevent the
|
|
# propagation of our span id all the way through all our onwards requests much like trace id. and the point
|
|
# of span id is to assign identifiers to each individual request.
|
|
self._span_id = self._get_header_value(SPAN_ID_HEADER)
|
|
return self._span_id
|
|
|
|
@property
|
|
def parent_span_id(self):
|
|
"""
|
|
The "parent span id" (in zipkin terms) set in this request's header, if present (None otherwise)
|
|
"""
|
|
if not hasattr(self, "_parent_span_id"):
|
|
self._parent_span_id = self._get_header_value(PARENT_SPAN_ID_HEADER)
|
|
return self._parent_span_id
|
|
|
|
def _get_header_value(self, header_name):
|
|
"""
|
|
Returns value of the given header
|
|
"""
|
|
if header_name in self.headers and self.headers[header_name]:
|
|
return self.headers[header_name]
|
|
|
|
return None
|
|
|
|
|
|
class ResponseHeaderMiddleware(object):
|
|
def __init__(self, app):
|
|
self._app = app
|
|
|
|
def __call__(self, environ, start_response):
|
|
try:
|
|
req = NotifyRequest(environ)
|
|
|
|
def rewrite_response_headers(status, headers, exc_info=None):
|
|
lower_existing_header_names = frozenset(
|
|
name.lower() for name, value in headers
|
|
)
|
|
|
|
if TRACE_ID_HEADER.lower() not in lower_existing_header_names:
|
|
headers.append((TRACE_ID_HEADER, str(req.trace_id)))
|
|
|
|
if SPAN_ID_HEADER.lower() not in lower_existing_header_names:
|
|
headers.append((SPAN_ID_HEADER, str(req.span_id)))
|
|
|
|
headers = [
|
|
(key, value)
|
|
for key, value in headers
|
|
if key.lower() not in ["server", "last-modified"]
|
|
]
|
|
return start_response(status, headers, exc_info)
|
|
|
|
return self._app(environ, rewrite_response_headers)
|
|
except BaseException as be: # noqa
|
|
if "AuthError" in str(be): # notify-api-1135
|
|
current_app.logger.exception("AuthError")
|
|
elif "AttributeError" in str(be): # notify-api-1394
|
|
current_app.logger.exception("AttributeError")
|
|
elif "MethodNotAllowed" in str(be): # notify-admin-1392
|
|
current_app.logger.exception("MethodNotAllowed")
|
|
else:
|
|
raise be
|
|
|
|
|
|
def init_app(app):
|
|
app.request_class = NotifyRequest
|
|
app.wsgi_app = ResponseHeaderMiddleware(app.wsgi_app)
|
|
|
|
|
|
def check_proxy_header_before_request():
|
|
keys = [
|
|
current_app.config.get("ROUTE_SECRET_KEY_1"),
|
|
current_app.config.get("ROUTE_SECRET_KEY_2"),
|
|
]
|
|
result, msg = _check_proxy_header_secret(request, keys)
|
|
|
|
if not result:
|
|
if current_app.config.get("CHECK_PROXY_HEADER", False):
|
|
current_app.logger.warning(msg)
|
|
abort(403)
|
|
|
|
# We need to return None to continue processing the request
|
|
# http://flask.pocoo.org/docs/0.12/api/#flask.Flask.before_request
|
|
return None
|
|
|
|
|
|
def _check_proxy_header_secret(request, secrets, header="X-Custom-Forwarder"):
|
|
if header not in request.headers:
|
|
return False, "Header missing"
|
|
|
|
header_secret = request.headers.get(header)
|
|
if not header_secret:
|
|
return False, "Header exists but is empty"
|
|
|
|
# if there isn't any non-empty secret configured we fail closed
|
|
if not any(secrets):
|
|
return False, "Secrets are not configured"
|
|
|
|
for i, secret in enumerate(secrets):
|
|
if header_secret == secret:
|
|
return True, "Key used: {}".format(
|
|
i + 1
|
|
) # add 1 to make it human-compatible
|
|
|
|
return False, "Header didn't match any keys"
|