refactor: use shared ServiceLink blueprint
- Replace the local /rpc Quart route with the shared ServiceLink blueprint helper. - Move mesh scope enforcement into bearer_verifier via require_scope. - Update ServiceLink tests to call the public handle_envelope and verify exports. - Advance the servicelink submodule to include the shared CLI and RPC discovery work.
This commit is contained in:
+10
-38
@@ -1,26 +1,23 @@
|
|||||||
'''ServiceLink mesh endpoint for the picoshare/NanoShare node.
|
'''ServiceLink mesh endpoint for the picoshare/NanoShare node.
|
||||||
|
|
||||||
Lets other nodes (browser-cli, website) push files in and read file metadata
|
Lets other nodes (browser-cli, website) push files in and read file metadata
|
||||||
over the shared servicelink envelope at POST /rpc. Sits alongside the existing
|
over the shared servicelink envelope at POST /rpc, alongside the existing web
|
||||||
web UI and /api routes.
|
UI and /api routes.
|
||||||
|
|
||||||
Security: every call needs a bearer token that decodes to the `mesh` scope
|
Every call needs a bearer token carrying the `mesh` scope; the endpoint is rate
|
||||||
(or `all`); the endpoint is rate limited and body-size capped. Intended to be
|
limited and body-size capped. Keep /rpc on the internal node network.
|
||||||
reachable only from the internal node network (lock it down at the reverse
|
|
||||||
proxy), with the token as defence-in-depth.
|
|
||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from quart import Blueprint, Response, current_app, request
|
from quart import current_app
|
||||||
|
|
||||||
from my_modules.app.setup import LIMITER
|
from my_modules.app.setup import LIMITER
|
||||||
from my_modules.expiry import ensure_utc, parse_expires
|
from my_modules.expiry import ensure_utc, parse_expires
|
||||||
from my_modules.file_meta import format_size, iso_stamp_filename
|
from my_modules.file_meta import format_size, iso_stamp_filename
|
||||||
from servicelink import Forbidden, InvalidParams, NotFound, Router, Unauthorized, bearer_verifier, handle_envelope
|
from servicelink import InvalidParams, NotFound, Router, Unauthorized, bearer_verifier, create_link_blueprint
|
||||||
|
|
||||||
# Cap the /rpc body. base64 inflates ~33%, so this bounds upload size too.
|
|
||||||
MAX_RPC_BODY = 16 * 1024 * 1024
|
MAX_RPC_BODY = 16 * 1024 * 1024
|
||||||
MESH_SCOPE = 'mesh'
|
MESH_SCOPE = 'mesh'
|
||||||
|
|
||||||
@@ -36,9 +33,6 @@ async def files_upload(params, ctx):
|
|||||||
user_id = _user_id(ctx)
|
user_id = _user_id(ctx)
|
||||||
text = params.get('text')
|
text = params.get('text')
|
||||||
content_b64 = params.get('content_b64')
|
content_b64 = params.get('content_b64')
|
||||||
note = params.get('note', '')
|
|
||||||
expires_at = ensure_utc(parse_expires(params.get('expires', '')))
|
|
||||||
|
|
||||||
if content_b64:
|
if content_b64:
|
||||||
data = base64.b64decode(content_b64)
|
data = base64.b64decode(content_b64)
|
||||||
content_type = params.get('content_type') or 'application/octet-stream'
|
content_type = params.get('content_type') or 'application/octet-stream'
|
||||||
@@ -55,9 +49,9 @@ async def files_upload(params, ctx):
|
|||||||
await current_app.convex.add_file(
|
await current_app.convex.add_file(
|
||||||
file_name=file_name,
|
file_name=file_name,
|
||||||
file_size=format_size(len(data)),
|
file_size=format_size(len(data)),
|
||||||
note=note,
|
note=params.get('note', ''),
|
||||||
content_type=content_type,
|
content_type=content_type,
|
||||||
expires_at=expires_at,
|
expires_at=ensure_utc(parse_expires(params.get('expires', ''))),
|
||||||
storage_id=storage_id,
|
storage_id=storage_id,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
)
|
)
|
||||||
@@ -113,27 +107,5 @@ async def _decode_access_token(token):
|
|||||||
raise ValueError((payload or {}).get('error', 'invalid token'))
|
raise ValueError((payload or {}).get('error', 'invalid token'))
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
_base_verify = bearer_verifier(_decode_access_token)
|
verify = bearer_verifier(_decode_access_token, require_scope=MESH_SCOPE)
|
||||||
|
link_bp = create_link_blueprint(router, verify=verify, limiter=LIMITER.limit('30 per minute'), max_body=MAX_RPC_BODY)
|
||||||
async def _verify(authorization, req):
|
|
||||||
principal = await _base_verify(authorization, req)
|
|
||||||
if principal is None or not principal.has_scope(MESH_SCOPE):
|
|
||||||
raise Forbidden(f'token lacks the {MESH_SCOPE!r} scope')
|
|
||||||
return principal
|
|
||||||
|
|
||||||
link_bp = Blueprint('servicelink_picoshare', __name__)
|
|
||||||
|
|
||||||
@link_bp.post('/rpc')
|
|
||||||
@LIMITER.limit('30 per minute')
|
|
||||||
async def rpc_endpoint():
|
|
||||||
if (request.content_length or 0) > MAX_RPC_BODY:
|
|
||||||
return Response('{"error":"payload too large"}', status=413, content_type='application/json')
|
|
||||||
raw = await request.get_data()
|
|
||||||
status, body, content_type = await handle_envelope(
|
|
||||||
router,
|
|
||||||
raw,
|
|
||||||
authorization=request.headers.get('Authorization'),
|
|
||||||
verify=_verify,
|
|
||||||
content_type=request.headers.get('Content-Type', 'application/json'),
|
|
||||||
)
|
|
||||||
return Response(body, status=status, content_type=content_type)
|
|
||||||
|
|||||||
+1
-1
Submodule servicelink updated: 946d79c95d...094bdc8c56
@@ -9,7 +9,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|||||||
|
|
||||||
from quart import Quart
|
from quart import Quart
|
||||||
|
|
||||||
from servicelink import Request
|
from servicelink import Request, handle_envelope
|
||||||
|
|
||||||
# Load routes/api/link.py in isolation. Importing it as `routes.api.link` would
|
# Load routes/api/link.py in isolation. Importing it as `routes.api.link` would
|
||||||
# run routes/__init__.py -> my_modules.app.setup -> constens (asyncio.run at
|
# run routes/__init__.py -> my_modules.app.setup -> constens (asyncio.run at
|
||||||
@@ -63,11 +63,11 @@ def _call(envelope, token=None):
|
|||||||
app = Quart(__name__)
|
app = Quart(__name__)
|
||||||
app.convex = FakeConvex()
|
app.convex = FakeConvex()
|
||||||
async with app.test_request_context('/rpc', method='POST'):
|
async with app.test_request_context('/rpc', method='POST'):
|
||||||
status, body, _ = await link.handle_envelope(
|
status, body, _ = await handle_envelope(
|
||||||
link.router,
|
link.router,
|
||||||
json.dumps(envelope).encode('utf-8'),
|
json.dumps(envelope).encode('utf-8'),
|
||||||
authorization=f'Bearer {token}' if token else None,
|
authorization=f'Bearer {token}' if token else None,
|
||||||
verify=link._verify,
|
verify=link.verify,
|
||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
)
|
)
|
||||||
return status, json.loads(body)
|
return status, json.loads(body)
|
||||||
|
|||||||
Reference in New Issue
Block a user