HTTP API: secrets infrastructure #1166
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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,38 +66,45 @@ 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)
|
||||||
|
def route(self, request, *args, **kwargs):
|
||||||
|
if request.requestHeaders.getRawHeaders("Authorization", [None])[0] != str(
|
||||||
|
swissnum_auth_header(self._swissnum), "ascii"
|
||||||
|
):
|
||||||
|
request.setResponseCode(http.UNAUTHORIZED)
|
||||||
|
return b""
|
||||||
|
authorization = request.requestHeaders.getRawHeaders("X-Tahoe-Authorization", [])
|
||||||
|
try:
|
||||||
|
secrets = _extract_secrets(authorization, required_secrets)
|
||||||
|
except ClientSecretsException:
|
||||||
|
request.setResponseCode(400)
|
||||||
|
return b""
|
||||||
|
return f(self, request, secrets, *args, **kwargs)
|
||||||
|
|
||||||
@wraps(f)
|
return route
|
||||||
def route(self, request, *args, **kwargs):
|
|
||||||
if request.requestHeaders.getRawHeaders("Authorization", [None])[0] != str(
|
|
||||||
swissnum_auth_header(self._swissnum), "ascii"
|
|
||||||
):
|
|
||||||
request.setResponseCode(http.UNAUTHORIZED)
|
|
||||||
return b""
|
|
||||||
# authorization = request.requestHeaders.getRawHeaders("X-Tahoe-Authorization", [])
|
|
||||||
# For now, just a placeholder:
|
|
||||||
authorization = None
|
|
||||||
return f(self, request, authorization, *args, **kwargs)
|
|
||||||
|
|
||||||
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())
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue