feat(AUDIT-WP-0008): enforce temporary sender expiry
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a025c2-407a-7a32-b40a-f37a52f03f62
This commit is contained in:
parent
e916c957ea
commit
abd22fa0a6
6 changed files with 136 additions and 7 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import io
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -239,6 +240,7 @@ def bound_app(tmp_path, **kw):
|
|||
tenants=frozenset(kw.get("tenants", {"tenant:friendly:binky"})),
|
||||
may_read=kw.get("may_read", False),
|
||||
secret_policy=kw.get("secret_policy", "redact"),
|
||||
expires_at=kw.get("expires_at"),
|
||||
)
|
||||
# An unrestricted operator sits alongside the scoped sender. The
|
||||
# instance-wide read surfaces — stats, dead letters, secret findings,
|
||||
|
|
@ -281,6 +283,16 @@ def test_rotation_accepts_both_tokens(tmp_path):
|
|||
assert invoke(app, event(id="evt-3"), key="evt-3", token="retired")[0].startswith("401")
|
||||
|
||||
|
||||
def test_expired_sender_is_unauthorized_at_http_boundary(tmp_path):
|
||||
app, _ = bound_app(
|
||||
tmp_path,
|
||||
expires_at=datetime(2000, 1, 1, tzinfo=timezone.utc),
|
||||
)
|
||||
status, body = invoke(app, event())
|
||||
assert status.startswith("401")
|
||||
assert body["error"] == "unauthorized"
|
||||
|
||||
|
||||
def test_sender_credential_cannot_read_the_trail_back(tmp_path):
|
||||
app, _ = bound_app(tmp_path, may_read=False)
|
||||
assert invoke(app, event())[0].startswith("202")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import json
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
|
@ -87,6 +88,68 @@ def test_missing_overlay_leaves_secret_as_is():
|
|||
assert identity.tenants == frozenset({"tenant:friendly:binky"})
|
||||
|
||||
|
||||
def test_temporary_sender_is_rejected_at_and_after_expiry():
|
||||
registry = SenderRegistry.from_env(
|
||||
{
|
||||
"AUDIT_CORE_SENDERS": json.dumps(
|
||||
[
|
||||
{
|
||||
"name": "whitehat-a",
|
||||
"tokens": ["temporary"],
|
||||
"sources": ["whitehat-security"],
|
||||
"tenants": ["tenant:trial:whitehat-a"],
|
||||
"expires_at": "2026-08-22T18:15:00Z",
|
||||
}
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
before = datetime(2026, 8, 22, 18, 14, 59, tzinfo=timezone.utc)
|
||||
boundary = datetime(2026, 8, 22, 18, 15, 0, tzinfo=timezone.utc)
|
||||
assert registry.authenticate("Bearer temporary", now=before) is not None
|
||||
assert registry.authenticate("Bearer temporary", now=boundary) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expires_at", ["not-a-time", "2026-08-22T18:15:00"])
|
||||
def test_temporary_sender_expiry_must_be_valid_and_timezone_aware(expires_at):
|
||||
sender = json.dumps(
|
||||
[
|
||||
{
|
||||
"name": "whitehat-a",
|
||||
"tokens": ["temporary"],
|
||||
"sources": ["whitehat-security"],
|
||||
"expires_at": expires_at,
|
||||
}
|
||||
]
|
||||
)
|
||||
with pytest.raises(ValueError, match="expires_at"):
|
||||
SenderRegistry.from_env({"AUDIT_CORE_SENDERS": sender})
|
||||
|
||||
|
||||
def test_scope_overlay_cannot_remove_secret_backed_expiry():
|
||||
registry = SenderRegistry.from_env(
|
||||
{
|
||||
"AUDIT_CORE_SENDERS": json.dumps(
|
||||
[
|
||||
{
|
||||
"name": "user-engine",
|
||||
"tokens": ["temporary"],
|
||||
"sources": ["user-engine"],
|
||||
"expires_at": "2026-08-22T18:15:00Z",
|
||||
}
|
||||
]
|
||||
),
|
||||
"AUDIT_CORE_SENDERS_SCOPE": json.dumps(
|
||||
[{"name": "user-engine", "tenants": ["*"]}]
|
||||
),
|
||||
}
|
||||
)
|
||||
assert registry.identities[0].expires_at == datetime(
|
||||
2026, 8, 22, 18, 15, 0, tzinfo=timezone.utc
|
||||
)
|
||||
|
||||
|
||||
def test_invalid_scope_overlay_is_a_startup_error():
|
||||
secret = json.dumps(
|
||||
[{"name": "user-engine", "tokens": ["t"], "sources": ["user-engine"]}]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue