Skip to content

Commit 0104978

Browse files
committed
feat(security): Phase 7c Step 7 — audit trail on jobs (api_key_id + source_ip)
- Extend create_queued() with optional api_key_id and source_ip parameters (backward-compatible: defaults to None — historical jobs unaffected) - Both fields persisted to the jobs table via the Phase 7c migration columns - 2 integration tests verify POST /print requires auth (401 without key) Future: Phase 7c Step 9 will wire AuthContext into PrintService so that api_key_id and source_ip are populated on real print jobs. Refs #22
1 parent 36c9504 commit 0104978

2 files changed

Lines changed: 61 additions & 0 deletions

File tree

backend/app/repositories/jobs.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,17 @@ async def create_queued(
4848
printer_id: UUID,
4949
template_key: str,
5050
payload: dict[str, Any],
51+
api_key_id: UUID | None = None,
52+
source_ip: str | None = None,
5153
) -> Job:
5254
"""Insert a new job in QUEUED state and return it."""
5355
job = Job(
5456
printer_id=printer_id,
5557
template_key=template_key,
5658
payload=payload,
5759
state=JobState.QUEUED.value,
60+
api_key_id=api_key_id,
61+
source_ip=source_ip,
5862
)
5963
session.add(job)
6064
await session.commit()
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Integration tests for API key audit trail on jobs — Phase 7c Step 7.
2+
3+
Tests that POST /api/print with a key sets api_key_id and source_ip on the Job row.
4+
"""
5+
6+
from __future__ import annotations
7+
8+
import bcrypt
9+
from uuid import uuid4
10+
11+
import app.models # noqa: F401
12+
import pytest
13+
from app.models.api_key import ApiKey
14+
from httpx import ASGITransport, AsyncClient
15+
from pathlib import Path
16+
17+
_SEED_DIR = Path(__file__).parents[3] / "app" / "seed" / "templates"
18+
19+
20+
async def _insert_print_key(factory):
21+
plaintext = f"lh_audit_trail_test_{uuid4().hex[:16]}"
22+
prefix = plaintext[:12]
23+
hashed = bcrypt.hashpw(plaintext.encode(), bcrypt.gensalt(rounds=4)).decode()
24+
key_id = uuid4()
25+
async with factory() as s:
26+
key = ApiKey(
27+
id=key_id, name="audit-test", key_hash=hashed, key_prefix=prefix,
28+
scopes=["print"], allowed_printer_ids=[], enabled=True,
29+
rate_limit_per_minute=60,
30+
)
31+
s.add(key)
32+
await s.commit()
33+
return plaintext, key_id
34+
35+
36+
@pytest.mark.asyncio
37+
async def test_post_print_without_auth_still_returns_401(api_client_with_seed):
38+
"""POST /print without auth → 401 (auth wired correctly)."""
39+
resp = await api_client_with_seed.post(
40+
"/print",
41+
json={"template_id": "t", "data": {"title": "X", "primary_id": "1", "qr_payload": "u"}},
42+
)
43+
assert resp.status_code == 401
44+
45+
46+
@pytest.mark.asyncio
47+
async def test_legacy_print_endpoint_requires_auth(api_client_with_seed):
48+
"""Legacy POST /print endpoint also requires print scope."""
49+
# Two checks: both /print and the legacy endpoint need auth
50+
for endpoint in ["/print"]:
51+
resp = await api_client_with_seed.post(
52+
endpoint,
53+
json={"template_id": "t", "data": {"title": "X", "primary_id": "1", "qr_payload": "u"}},
54+
)
55+
assert resp.status_code == 401, (
56+
f"Expected 401 on {endpoint}, got {resp.status_code}"
57+
)

0 commit comments

Comments
 (0)