approved_by was client-supplied, making approval forgeable ahead of Phase 2 pushing real spend to Meta. Add a proportionate operator-token check (no login system) via a new require_operator FastAPI dependency: MASKANX_OPERATOR_TOKENS maps name:token pairs, and approve_campaign now derives its actor solely from the resolved operator, never from the client-supplied ActorPayload.actor. Unset env resolves to "unauthenticated" so local dev and existing tests are not blocked; set-but-unrecognised tokens 401. submit and reject remain unauthenticated since neither authorises spend. Updates test_submit_then_approve_moves_through_states to assert approved_by == "unauthenticated" (env var unset in tests) instead of the previously-trusted client actor, since a forged actor must now be ignored.
61 lines
2.1 KiB
Python
61 lines
2.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Approve and launch must be attributable to a known operator."""
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from adclaw.app.routers import _operator
|
|
|
|
|
|
def _app():
|
|
from adclaw.app.routers import campaigns as campaigns_router
|
|
|
|
app = FastAPI()
|
|
app.include_router(campaigns_router.router, prefix="/api")
|
|
return app
|
|
|
|
|
|
def test_unset_env_allows_and_reports_unauthenticated(monkeypatch):
|
|
monkeypatch.delenv("MASKANX_OPERATOR_TOKENS", raising=False)
|
|
assert _operator.resolve_operator(None) == "unauthenticated"
|
|
|
|
|
|
def test_known_token_resolves_to_its_name(monkeypatch):
|
|
monkeypatch.setenv("MASKANX_OPERATOR_TOKENS", "owner:s3cr3t,ops:t0ken")
|
|
assert _operator.resolve_operator("s3cr3t") == "owner"
|
|
assert _operator.resolve_operator("t0ken") == "ops"
|
|
|
|
|
|
def test_unknown_token_is_rejected(monkeypatch):
|
|
monkeypatch.setenv("MASKANX_OPERATOR_TOKENS", "owner:s3cr3t")
|
|
with pytest.raises(_operator.OperatorAuthError):
|
|
_operator.resolve_operator("wrong")
|
|
|
|
|
|
def test_missing_header_is_rejected_when_configured(monkeypatch):
|
|
monkeypatch.setenv("MASKANX_OPERATOR_TOKENS", "owner:s3cr3t")
|
|
with pytest.raises(_operator.OperatorAuthError):
|
|
_operator.resolve_operator(None)
|
|
|
|
|
|
def test_malformed_config_entries_are_ignored(monkeypatch):
|
|
monkeypatch.setenv("MASKANX_OPERATOR_TOKENS", "bad,owner:s3cr3t,:empty,x:")
|
|
assert _operator.resolve_operator("s3cr3t") == "owner"
|
|
with pytest.raises(_operator.OperatorAuthError):
|
|
_operator.resolve_operator("empty")
|
|
|
|
|
|
def test_approve_uses_operator_identity_not_client_actor(monkeypatch):
|
|
"""The body's `actor` must never override the authenticated operator."""
|
|
monkeypatch.setenv("MASKANX_OPERATOR_TOKENS", "owner:s3cr3t")
|
|
client = TestClient(_app())
|
|
# Full flow is covered in test_campaign_api.py; here we assert the
|
|
# dependency is wired so a forged actor cannot win.
|
|
from adclaw.app.routers import campaigns as campaigns_router
|
|
|
|
import inspect
|
|
|
|
source = inspect.getsource(campaigns_router.approve_campaign)
|
|
assert "require_operator" in source
|
|
assert "payload.actor" not in source
|