excel file update
This commit is contained in:
@@ -21,13 +21,15 @@ from __future__ import annotations
|
||||
import pytest
|
||||
|
||||
from app.infrastructure.security import (
|
||||
api_key_fingerprint,
|
||||
auth_config_summary,
|
||||
describe_api_keys,
|
||||
describe_password_hash,
|
||||
hash_is_wellformed,
|
||||
hash_password,
|
||||
password_hash_fingerprint,
|
||||
)
|
||||
from app.infrastructure.settings import config_source
|
||||
from app.infrastructure.settings import API_KEY_MIN_LENGTH, _parse_api_keys, config_source
|
||||
from tests.conftest import TEST_ADMIN_PASSWORD
|
||||
|
||||
|
||||
@@ -164,3 +166,164 @@ def test_health_stays_ok_shaped_when_auth_is_misconfigured(client, monkeypatch):
|
||||
|
||||
assert body["auth"]["password_hash_valid"] is False
|
||||
assert body["status"] in {"ok", "degraded"} # decided by db/ollama only
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# API keys
|
||||
# ---------------------------------------------------------------------------
|
||||
# Same incident, one layer out. A key added to .env.production and then merely
|
||||
# restarted into a running container is absent from the process, because the
|
||||
# Dockerfile copies that file in at BUILD time - and from outside, an undeployed
|
||||
# key and a wrong key are both just a 401. These pin the ability to tell them
|
||||
# apart without anyone sending the secret to find out.
|
||||
|
||||
_GOOD_SECRET = "cs3JwvApS5Je_Qfe1sNYq6YtUBDDeqp4OEgy2_41sQg"
|
||||
|
||||
|
||||
def test_api_key_fingerprint_is_stable_and_hex():
|
||||
fp = api_key_fingerprint("partner", _GOOD_SECRET)
|
||||
|
||||
assert fp == api_key_fingerprint("partner", _GOOD_SECRET)
|
||||
assert len(fp) == 12
|
||||
assert all(c in "0123456789abcdef" for c in fp)
|
||||
|
||||
|
||||
def test_api_key_fingerprint_differs_when_the_secret_does():
|
||||
assert api_key_fingerprint("partner", _GOOD_SECRET) != api_key_fingerprint(
|
||||
"partner", _GOOD_SECRET[:-1] + "X"
|
||||
)
|
||||
|
||||
|
||||
def test_api_key_fingerprint_separates_consumers_sharing_a_secret():
|
||||
"""The name is mixed in, so two consumers mistakenly issued the same secret
|
||||
do not report the same fingerprint - which would hide the mistake behind the
|
||||
very field meant to reveal it."""
|
||||
assert api_key_fingerprint("console-a", _GOOD_SECRET) != api_key_fingerprint(
|
||||
"console-b", _GOOD_SECRET
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("wrapper", ["{}", "'{}'", '"{}"', " {} "])
|
||||
def test_api_key_fingerprint_ignores_quotes_and_whitespace(wrapper):
|
||||
"""A value pasted into a deployment platform's Environment tab arrives
|
||||
wrapped often enough that settings strips it; the fingerprint must agree,
|
||||
or comparing two ends reports a mismatch that is not real."""
|
||||
assert api_key_fingerprint("partner", wrapper.format(_GOOD_SECRET)) == (
|
||||
api_key_fingerprint("partner", _GOOD_SECRET)
|
||||
)
|
||||
|
||||
|
||||
def test_api_key_fingerprint_discloses_no_part_of_the_secret():
|
||||
"""Served unauthenticated, so it must be a digest OF the key, not a piece."""
|
||||
fp = api_key_fingerprint("partner", _GOOD_SECRET)
|
||||
|
||||
assert fp not in _GOOD_SECRET
|
||||
for start in range(len(fp) - 5):
|
||||
assert fp[start : start + 6] not in _GOOD_SECRET
|
||||
|
||||
|
||||
def test_absent_secret_fingerprints_as_empty():
|
||||
assert api_key_fingerprint("partner", "") == ""
|
||||
|
||||
|
||||
def test_describe_api_keys_is_sorted_by_name(monkeypatch):
|
||||
"""API_KEYS is keyed by secret, whose order says nothing. Sorting is what
|
||||
lets two deployments' output be diffed line for line."""
|
||||
from app.infrastructure import security
|
||||
|
||||
monkeypatch.setattr(
|
||||
security, "API_KEYS",
|
||||
{_GOOD_SECRET: ("zulu", "user"), _GOOD_SECRET[::-1]: ("alpha", "admin")},
|
||||
)
|
||||
|
||||
assert [k["name"] for k in describe_api_keys()] == ["alpha", "zulu"]
|
||||
assert [k["role"] for k in describe_api_keys()] == ["admin", "user"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# API keys on /api/health
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_health_reports_the_keys_this_deployment_actually_loaded(client):
|
||||
"""Against the harness's own API_KEYS, not a monkeypatched one - this is the
|
||||
end-to-end wiring from settings through the summary to the response body."""
|
||||
from tests.conftest import TEST_API_KEY
|
||||
|
||||
auth = client.get("/api/health").json()["auth"]
|
||||
|
||||
assert auth["api_keys_count"] == 1
|
||||
assert auth["api_keys"] == [{
|
||||
"name": "test-machine",
|
||||
"role": "user",
|
||||
"fingerprint": api_key_fingerprint("test-machine", TEST_API_KEY),
|
||||
}]
|
||||
|
||||
|
||||
def test_health_reports_an_empty_list_when_no_keys_are_configured(client, monkeypatch):
|
||||
"""The state production was in while the colleague's console got 401s: auth
|
||||
enabled, admin login working, and not one machine consumer deployed."""
|
||||
from app.infrastructure import security
|
||||
|
||||
monkeypatch.setattr(security, "API_KEYS", {})
|
||||
auth = client.get("/api/health").json()["auth"]
|
||||
|
||||
assert auth["api_keys_count"] == 0
|
||||
assert auth["api_keys"] == []
|
||||
|
||||
|
||||
def test_health_names_configured_keys_and_fingerprints_them(client, monkeypatch):
|
||||
from app.infrastructure import security
|
||||
|
||||
monkeypatch.setattr(security, "API_KEYS", {_GOOD_SECRET: ("colleague-console", "admin")})
|
||||
auth = client.get("/api/health").json()["auth"]
|
||||
|
||||
assert auth["api_keys_count"] == 1
|
||||
assert auth["api_keys"] == [{
|
||||
"name": "colleague-console",
|
||||
"role": "admin",
|
||||
"fingerprint": api_key_fingerprint("colleague-console", _GOOD_SECRET),
|
||||
}]
|
||||
|
||||
|
||||
def test_health_never_exposes_an_api_key_secret(client, monkeypatch):
|
||||
"""The leak canary, extended to machine credentials."""
|
||||
from app.infrastructure import security
|
||||
|
||||
monkeypatch.setattr(security, "API_KEYS", {_GOOD_SECRET: ("colleague-console", "admin")})
|
||||
body = client.get("/api/health").text
|
||||
|
||||
assert _GOOD_SECRET not in body
|
||||
for start in range(0, len(_GOOD_SECRET) - 7):
|
||||
assert _GOOD_SECRET[start : start + 8] not in body
|
||||
|
||||
|
||||
def test_health_reports_where_the_keys_came_from(client):
|
||||
"""Which of the two config sources won. Unlike the admin hash - where
|
||||
"process-env" flags a stale Environment tab shadowing the image - API_KEYS is
|
||||
deliberately supplied by that tab, so "process-env" is the expected value in
|
||||
production and "env-file" would mean the tab entry has gone missing."""
|
||||
auth = client.get("/api/health").json()["auth"]
|
||||
|
||||
assert auth["api_keys_source"] in {"process-env", "env-file", "default"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_parse_api_keys_accepts_a_generated_secret():
|
||||
parsed = _parse_api_keys(f"partner:admin:{_GOOD_SECRET}")
|
||||
|
||||
assert parsed == {_GOOD_SECRET: ("partner", "admin")}
|
||||
|
||||
|
||||
def test_parse_api_keys_rejects_a_secret_too_short_to_fingerprint_safely():
|
||||
"""A raw key carries no salt, so publishing its digest is only safe while the
|
||||
key itself is unguessable offline. A hand-picked one must be refused at
|
||||
startup rather than quietly fingerprinted onto a public endpoint."""
|
||||
with pytest.raises(RuntimeError, match="at least"):
|
||||
_parse_api_keys("partner:admin:changeme")
|
||||
|
||||
|
||||
def test_parse_api_keys_length_limit_admits_the_documented_generator():
|
||||
import secrets as _secrets
|
||||
|
||||
assert len(_secrets.token_urlsafe(32)) >= API_KEY_MIN_LENGTH
|
||||
|
||||
Reference in New Issue
Block a user