fix: allow CLI file tokens for RPC access
Build and Push Docker Container / build-and-push (push) Successful in 1m2s
Build and Push Docker Container / build-and-push (push) Successful in 1m2s
- Accept files-scoped bearer tokens on servicelink RPC calls. - Keep mesh shared-secret auth for trusted internal callers. - Validate CLI auth scopes and reject unsupported values early. - Stop CLI browser login waiting for the full timeout after callback. - Add tests for scope normalization, RPC access, and login callback timing.
This commit is contained in:
+17
-3
@@ -12,13 +12,24 @@ from quart import current_app
|
||||
cli_auth_bp = Blueprint('cli_auth', __name__)
|
||||
|
||||
CLI_CODE_TTL_SECONDS = 300
|
||||
CLI_AUTH_DEFAULT_SCOPE = 'files'
|
||||
CLI_AUTH_ALLOWED_SCOPES = {'all', 'files'}
|
||||
|
||||
def _normalize_cli_scope(scope: str | None) -> str:
|
||||
requested_scope = (scope or CLI_AUTH_DEFAULT_SCOPE).strip() or CLI_AUTH_DEFAULT_SCOPE
|
||||
if requested_scope not in CLI_AUTH_ALLOWED_SCOPES:
|
||||
raise ValueError(f'unsupported CLI scope: {requested_scope}')
|
||||
return requested_scope
|
||||
|
||||
@cli_auth_bp.get('/auth/cli/login')
|
||||
@LIMITER.limit('10 per minute;60 per hour;')
|
||||
async def cli_login():
|
||||
redirect_uri = request.args.get('redirect_uri', '').strip()
|
||||
state = request.args.get('state', '').strip()
|
||||
scope = request.args.get('scope', 'files').strip() or 'files'
|
||||
try:
|
||||
scope = _normalize_cli_scope(request.args.get('scope'))
|
||||
except ValueError as exc:
|
||||
return jsonify({'ok': False, 'error': str(exc)}), 400
|
||||
add_wide_event_context(auth={'method': 'cli_browser', 'operation': 'cli_login_start'})
|
||||
|
||||
if not redirect_uri or not state:
|
||||
@@ -44,7 +55,7 @@ async def cli_login_complete():
|
||||
user['sub'],
|
||||
login_request['redirect_uri'],
|
||||
login_request['state'],
|
||||
login_request.get('scope') or 'files',
|
||||
_normalize_cli_scope(login_request.get('scope')),
|
||||
)
|
||||
|
||||
async def _redirect_with_cli_code(user_id: str, redirect_uri: str, state: str, scope: str):
|
||||
@@ -72,7 +83,10 @@ async def cli_token_exchange():
|
||||
await cache.delete(f'cli:login:{code}')
|
||||
|
||||
user_id = login_data['user_id']
|
||||
scope = login_data.get('scope') or 'files'
|
||||
try:
|
||||
scope = _normalize_cli_scope(login_data.get('scope'))
|
||||
except ValueError as exc:
|
||||
return jsonify({'ok': False, 'error': str(exc)}), 400
|
||||
token_data = await current_app.convex.add_refresh_token(
|
||||
token_name='nanoshare-cli',
|
||||
user_id=user_id,
|
||||
|
||||
+8
-6
@@ -4,7 +4,7 @@ Lets other nodes (browser-cli, website) push files in and read file metadata
|
||||
over the shared servicelink envelope at POST /rpc, alongside the existing web
|
||||
UI and /api routes.
|
||||
|
||||
Every call needs a bearer token carrying the `mesh` scope; the endpoint is rate
|
||||
Every call needs a bearer token carrying the `files` or `mesh` scope; the endpoint is rate
|
||||
limited and body-size capped. Keep /rpc on the internal node network.
|
||||
'''
|
||||
from __future__ import annotations
|
||||
@@ -17,16 +17,18 @@ from quart import current_app
|
||||
from my_modules.app.setup import LIMITER
|
||||
from my_modules.expiry import ensure_utc, parse_expires
|
||||
from my_modules.file_meta import format_size, iso_stamp_filename
|
||||
from servicelink import InvalidParams, NotFound, Router, Unauthorized, any_verifier, bearer_verifier, create_link_blueprint, shared_secret_verifier
|
||||
from servicelink import Forbidden, InvalidParams, NotFound, Router, Unauthorized, any_verifier, bearer_verifier, create_link_blueprint, shared_secret_verifier
|
||||
|
||||
MAX_RPC_BODY = 16 * 1024 * 1024
|
||||
MESH_SCOPE = 'mesh'
|
||||
RPC_SCOPES = ('files', 'mesh')
|
||||
|
||||
router = Router('picoshare')
|
||||
|
||||
def _user_id(ctx):
|
||||
if ctx.principal is None:
|
||||
raise Unauthorized('authentication required')
|
||||
if not any(ctx.principal.has_scope(scope) for scope in RPC_SCOPES):
|
||||
raise Forbidden('missing required scope: files')
|
||||
return ctx.principal.subject
|
||||
|
||||
@router.method('files.upload')
|
||||
@@ -110,11 +112,11 @@ async def _decode_access_token(token):
|
||||
return payload
|
||||
|
||||
def _build_verify():
|
||||
# Accept a JWT access token with the mesh scope (public path) OR, on the
|
||||
# Accept a JWT access token with a file/RPC scope (public path) OR, on the
|
||||
# trusted Docker network, a static shared secret from SERVICELINK_MESH_SECRET.
|
||||
jwt = bearer_verifier(_decode_access_token, require_scope=MESH_SCOPE)
|
||||
jwt = bearer_verifier(_decode_access_token)
|
||||
secret = os.getenv('SERVICELINK_MESH_SECRET')
|
||||
return any_verifier(shared_secret_verifier(secret, scopes=(MESH_SCOPE,)), jwt) if secret else jwt
|
||||
return any_verifier(shared_secret_verifier(secret, scopes=('mesh',)), jwt) if secret else jwt
|
||||
|
||||
verify = _build_verify()
|
||||
link_bp = create_link_blueprint(router, verify=verify, limiter=LIMITER.limit('30 per minute'), max_body=MAX_RPC_BODY)
|
||||
|
||||
Reference in New Issue
Block a user