HTTP API: secrets infrastructure #1166

Merged
itamarst merged 18 commits from 3848.http-api-start-immutables into master 2021-12-22 18:39:51 +00:00
3 changed files with 34 additions and 21 deletions
Showing only changes of commit 816dc0c73f - Show all commits

View File

@ -77,7 +77,7 @@ class StorageClient(object):
for key, value in secrets.items(): for key, value in secrets.items():
headers.addRawHeader( headers.addRawHeader(
"X-Tahoe-Authorization", "X-Tahoe-Authorization",
b"{} {}".format(key.value.encode("ascii"), b64encode(value).strip()) b"%s %s" % (key.value.encode("ascii"), b64encode(value).strip())
) )
return self._treq.request(method, url, headers=headers, **kwargs) return self._treq.request(method, url, headers=headers, **kwargs)

View File

@ -54,8 +54,10 @@ def _extract_secrets(header_values, required_secrets): # type: (List[str], Set[
try: try:
for header_value in header_values: for header_value in header_values:
key, value = header_value.strip().split(" ", 1) key, value = header_value.strip().split(" ", 1)
# TODO enforce secret is 32 bytes long for lease secrets. dunno
# about upload secret.
result[key_to_enum[key]] = b64decode(value) result[key_to_enum[key]] = b64decode(value)
except (ValueError, KeyError) as e: except (ValueError, KeyError):
raise ClientSecretsException("Bad header value(s): {}".format(header_values)) raise ClientSecretsException("Bad header value(s): {}".format(header_values))
if result.keys() != required_secrets: if result.keys() != required_secrets:
raise ClientSecretsException( raise ClientSecretsException(
@ -64,12 +66,12 @@ def _extract_secrets(header_values, required_secrets): # type: (List[str], Set[
return result return result
def _authorization_decorator(f): def _authorization_decorator(required_secrets):
""" """
Check the ``Authorization`` header, and (TODO: in later revision of code) Check the ``Authorization`` header, and (TODO: in later revision of code)
extract ``X-Tahoe-Authorization`` headers and pass them in. extract ``X-Tahoe-Authorization`` headers and pass them in.
""" """
def decorator(f):
@wraps(f) @wraps(f)
def route(self, request, *args, **kwargs): def route(self, request, *args, **kwargs):
if request.requestHeaders.getRawHeaders("Authorization", [None])[0] != str( if request.requestHeaders.getRawHeaders("Authorization", [None])[0] != str(
@ -77,25 +79,32 @@ def _authorization_decorator(f):
): ):
request.setResponseCode(http.UNAUTHORIZED) request.setResponseCode(http.UNAUTHORIZED)
return b"" return b""
# authorization = request.requestHeaders.getRawHeaders("X-Tahoe-Authorization", []) authorization = request.requestHeaders.getRawHeaders("X-Tahoe-Authorization", [])
# For now, just a placeholder: try:
authorization = None secrets = _extract_secrets(authorization, required_secrets)
return f(self, request, authorization, *args, **kwargs) except ClientSecretsException:
request.setResponseCode(400)
return b""
return f(self, request, secrets, *args, **kwargs)
return route return route
return decorator
def _authorized_route(app, *route_args, **route_kwargs):
def _authorized_route(app, required_secrets, *route_args, **route_kwargs):
""" """
Like Klein's @route, but with additional support for checking the Like Klein's @route, but with additional support for checking the
``Authorization`` header as well as ``X-Tahoe-Authorization`` headers. The ``Authorization`` header as well as ``X-Tahoe-Authorization`` headers. The
latter will (TODO: in later revision of code) get passed in as second latter will get passed in as second argument to wrapped functions, a
argument to wrapped functions. dictionary mapping a ``Secret`` value to the uploaded secret.
:param required_secrets: Set of required ``Secret`` types.
""" """
def decorator(f): def decorator(f):
@app.route(*route_args, **route_kwargs) @app.route(*route_args, **route_kwargs)
@_authorization_decorator @_authorization_decorator(required_secrets)
def handle_route(*args, **kwargs): def handle_route(*args, **kwargs):
return f(*args, **kwargs) return f(*args, **kwargs)
@ -127,6 +136,10 @@ class HTTPServer(object):
# TODO if data is big, maybe want to use a temporary file eventually... # TODO if data is big, maybe want to use a temporary file eventually...
return dumps(data) return dumps(data)
@_authorized_route(_app, "/v1/version", methods=["GET"])
##### Generic APIs #####
@_authorized_route(_app, set(), "/v1/version", methods=["GET"])
def version(self, request, authorization): def version(self, request, authorization):
"""Return version information."""
return self._cbor(request, self._storage_server.get_version()) return self._cbor(request, self._storage_server.get_version())

View File

@ -147,7 +147,7 @@ class RoutingTests(TestCase):
# With secret, we're good. # With secret, we're good.
response = yield self.client._request( response = yield self.client._request(
"GET", "http://127.0.0.1/upload_secret", {Secrets.UPLOAD, b"abc"} "GET", "http://127.0.0.1/upload_secret", {Secrets.UPLOAD: b"abc"}
) )
self.assertEqual(response.code, 200) self.assertEqual(response.code, 200)