mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 20:15:10 +00:00
feat(remote): accept serve.users keys in api-only mode and add client auth-header fallback
api-only/remote-only serve now authenticates every serve.users key (str-coerced), not just api_secret, so per-user service allowlists apply. RemoteClient tries X-Secret-Key then X-Api-Key on 401 (sticky winner); remote_services.auth_headers prepends extra header names to the defaults.
This commit is contained in:
@@ -69,6 +69,61 @@ def test_serve_no_key_without_api_secret_does_not_require_secret(
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
|
||||
async def test_api_only_accepts_serve_users_keys(
|
||||
runner: CliRunner, monkeypatch: pytest.MonkeyPatch, aiohttp_client
|
||||
) -> None:
|
||||
"""In api-only keyed mode the accepted keys are api_secret plus every `serve.users` key."""
|
||||
from aiohttp import web
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
def capture_app(app, **kwargs):
|
||||
captured["app"] = app
|
||||
|
||||
monkeypatch.setattr(web, "run_app", capture_app)
|
||||
from unshackle.core.config import config as cfg
|
||||
|
||||
monkeypatch.setattr(
|
||||
cfg,
|
||||
"serve",
|
||||
{"api_secret": "master-key", "users": {"user-key": {"username": "alice", "services": ["EXAMPLE"]}}},
|
||||
)
|
||||
|
||||
result = runner.invoke(serve, ["--api-only", "--remote-only"])
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
app = captured["app"]
|
||||
assert set(app["config"]["users"]) == {"master-key", "user-key"}
|
||||
assert app["config"]["users"]["user-key"]["username"] == "alice"
|
||||
|
||||
client = await aiohttp_client(app)
|
||||
# an unrouted path still passes through the auth middleware first
|
||||
assert (await client.get("/api/unknown", headers={"X-Secret-Key": "user-key"})).status == 404
|
||||
assert (await client.get("/api/unknown", headers={"X-Secret-Key": "master-key"})).status == 404
|
||||
assert (await client.get("/api/unknown", headers={"X-Secret-Key": "nope"})).status == 401
|
||||
|
||||
|
||||
async def test_api_only_coerces_non_str_user_keys(
|
||||
runner: CliRunner, monkeypatch: pytest.MonkeyPatch, aiohttp_client
|
||||
) -> None:
|
||||
"""A yaml users key that parses as int must still authenticate (compare_digest needs str)."""
|
||||
from aiohttp import web
|
||||
|
||||
captured: dict = {}
|
||||
monkeypatch.setattr(web, "run_app", lambda app, **kwargs: captured.update(app=app))
|
||||
from unshackle.core.config import config as cfg
|
||||
|
||||
monkeypatch.setattr(cfg, "serve", {"api_secret": "master-key", "users": {123456: {"username": "bob"}}})
|
||||
|
||||
result = runner.invoke(serve, ["--api-only", "--remote-only"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "123456" in captured["app"]["config"]["users"]
|
||||
|
||||
client = await aiohttp_client(captured["app"])
|
||||
assert (await client.get("/api/unknown", headers={"X-Secret-Key": "123456"})).status == 404
|
||||
assert (await client.get("/api/unknown", headers={"X-Secret-Key": "master-key"})).status == 404
|
||||
|
||||
|
||||
def test_serve_without_no_key_requires_api_secret(runner: CliRunner, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from unshackle.core.config import config as cfg
|
||||
|
||||
|
||||
Reference in New Issue
Block a user