fix: allow CLI file tokens for RPC access
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:
2026-07-27 17:08:23 +02:00
parent 6e4e5b837b
commit 9db36c2d5b
6 changed files with 110 additions and 17 deletions
+43
View File
@@ -0,0 +1,43 @@
import importlib.util
import sys
import types
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
class _NoOpLimiter:
def limit(self, *args, **kwargs):
return lambda fn: fn
def _load_cli_auth_module():
setup_stub = types.ModuleType('my_modules.app.setup')
setup_stub.LIMITER = _NoOpLimiter()
setup_stub.cache = object()
sys.modules['my_modules.app.setup'] = setup_stub
module_path = Path(__file__).resolve().parents[1] / 'routes' / 'api' / 'cli_auth.py'
spec = importlib.util.spec_from_file_location('api_cli_auth_under_test', module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_normalize_cli_scope_keeps_files_scope():
module = _load_cli_auth_module()
assert module._normalize_cli_scope('files') == 'files'
def test_normalize_cli_scope_defaults_to_files():
module = _load_cli_auth_module()
assert module._normalize_cli_scope(None) == 'files'
assert module._normalize_cli_scope('') == 'files'
def test_normalize_cli_scope_rejects_unsupported_scope():
module = _load_cli_auth_module()
try:
module._normalize_cli_scope('printer')
except ValueError as exc:
assert 'unsupported CLI scope' in str(exc)
else:
raise AssertionError('expected unsupported scope to raise')
+20
View File
@@ -1,10 +1,13 @@
import json
import sys
import threading
import urllib.request
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'cli'))
from nanoshare_client import cli
from nanoshare_client.auth import _CallbackServer
class FakeClient:
def __init__(self):
@@ -52,6 +55,23 @@ def test_sync_uploads_new_local_file_and_writes_state(tmp_path, monkeypatch):
state = json.loads((tmp_path / '.nanoshare-sync' / 'state.json').read_text())
assert state['files']['hello.txt']['file_id'] == 'file_1'
def test_login_callback_returns_without_waiting_for_timeout():
server = _CallbackServer('127.0.0.1', 0, 'expected-state')
result = {}
def wait_for_code():
result['code'] = server.wait_for_code(30)
thread = threading.Thread(target=wait_for_code)
thread.start()
with urllib.request.urlopen(f'{server.url}?code=login-code&state=expected-state', timeout=5) as response:
assert response.status == 200
assert b'login complete' in response.read()
thread.join(5)
assert not thread.is_alive()
assert result['code'] == 'login-code'
def test_sync_deletes_old_remote_when_local_file_changes(tmp_path, monkeypatch):
fake = FakeClient()
fake.remote_files = [{'file_id': 'old_file', 'file_name': 'hello.txt', 'file_size': '5 B'}]
+6 -3
View File
@@ -56,6 +56,9 @@ class FakeConvex:
async def get_file(self, file_id):
return self.files.get(file_id)
async def get_files(self, user_id):
return list(self.files.values())
def _call(envelope, token=None):
# Call the handler the /rpc route delegates to, bypassing the LIMITER/size-cap
# wrapper; exercises the real verify (mesh scope) + dispatch + handlers.
@@ -100,7 +103,7 @@ def test_missing_token_is_unauthorized():
assert status == 401
assert body['error']['code'] == 'unauthorized'
def test_token_without_mesh_scope_is_forbidden():
def test_token_with_files_scope_is_allowed():
status, body = _call(_request('files.list'), token='nomesh')
assert status == 403
assert body['error']['code'] == 'forbidden'
assert status == 200
assert body['ok'] is True