feat: RBAC for RTC write modes (Gerenciador ADMIN only, denials audited), central backpressure for heavy jobs (HEAVY_JOB_CONCURRENCY_LIMIT), concurrency simulation evidence
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
"""Backpressure: at most HEAVY_JOB_CONCURRENCY_LIMIT whole-database jobs run at once across all Agents; the rest queue as PENDING."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from central.app import jobs
|
||||
from central.app.models import Agent, Job, Tenant
|
||||
from central.tests.test_control_plane import setup_db
|
||||
|
||||
|
||||
def _agents(session, n):
|
||||
tenant = Tenant(name="Backpressure")
|
||||
session.add(tenant)
|
||||
session.flush()
|
||||
out = []
|
||||
for i in range(n):
|
||||
agent = Agent(tenant_id=tenant.id, name=f"a{i}", token_hash=f"t{i}".ljust(64, "x"), agent_version="1.2.1")
|
||||
session.add(agent)
|
||||
out.append(agent)
|
||||
session.flush()
|
||||
return out
|
||||
|
||||
|
||||
def _limit(monkeypatch, value):
|
||||
monkeypatch.setattr(jobs, "settings", SimpleNamespace(heavy_job_concurrency_limit=value))
|
||||
|
||||
|
||||
def test_only_the_configured_number_of_full_jobs_run_at_once_and_the_rest_wait(monkeypatch):
|
||||
_limit(monkeypatch, 2)
|
||||
with setup_db()() as session:
|
||||
agents = _agents(session, 4)
|
||||
queued = [jobs.create_job(session, a, "INITIAL_SYNC") for a in agents]
|
||||
claimed = [jobs.claim_next_job(session, a) for a in agents]
|
||||
assert [c is not None for c in claimed] == [True, True, False, False] # 2 running, 2 queued (backpressure)
|
||||
assert [session.get(Job, q.id).status for q in queued] == ["RUNNING", "RUNNING", "PENDING", "PENDING"]
|
||||
jobs.complete_job(session, session.get(Job, queued[0].id), {}) # a slot frees up
|
||||
assert jobs.claim_next_job(session, agents[2]) is not None and jobs.claim_next_job(session, agents[3]) is None
|
||||
|
||||
|
||||
def test_a_light_incremental_job_is_never_held_behind_a_queued_full_job(monkeypatch):
|
||||
_limit(monkeypatch, 1)
|
||||
with setup_db()() as session:
|
||||
busy, waiting = _agents(session, 2)
|
||||
jobs.create_job(session, busy, "REPROCESS_ALL")
|
||||
assert jobs.claim_next_job(session, busy) is not None
|
||||
full = jobs.create_job(session, waiting, "REPROCESS_ALL")
|
||||
light = jobs.create_job(session, waiting, "REPROCESS_PRODUCTS", {"codigos": ["1"]})
|
||||
claimed = jobs.claim_next_job(session, waiting)
|
||||
assert claimed is not None and claimed.id == light.id and session.get(Job, full.id).status == "PENDING"
|
||||
|
||||
|
||||
def test_limit_zero_disables_backpressure_and_a_stale_running_job_does_not_hold_a_slot(monkeypatch):
|
||||
from datetime import datetime, timedelta, timezone
|
||||
_limit(monkeypatch, 0)
|
||||
with setup_db()() as session:
|
||||
agents = _agents(session, 3)
|
||||
for a in agents:
|
||||
jobs.create_job(session, a, "INITIAL_SYNC")
|
||||
assert all(jobs.claim_next_job(session, a) is not None for a in agents)
|
||||
_limit(monkeypatch, 1)
|
||||
with setup_db()() as session:
|
||||
dead, fresh = _agents(session, 2)
|
||||
stale = jobs.create_job(session, dead, "INITIAL_SYNC")
|
||||
session.execute(Job.__table__.update().where(Job.id == stale.id).values(status="RUNNING", started_at=datetime.now(timezone.utc) - timedelta(hours=3)))
|
||||
session.commit()
|
||||
jobs.create_job(session, fresh, "INITIAL_SYNC")
|
||||
assert jobs.claim_next_job(session, fresh) is not None # the dead Agent's 3 h old RUNNING job does not block the queue
|
||||
@@ -97,3 +97,54 @@ def test_agent_can_reread_only_its_own_current_authorization_and_it_follows_the_
|
||||
s.commit()
|
||||
body = client.get("/v1/agents/me/authorizations", headers=H).json()
|
||||
assert body["rtc_write_mode"] == "ENABLED" and set(body) == {"rtc_write_mode", "rtc_approved_codes", "rtc_rollbacks"}
|
||||
|
||||
|
||||
def _become(client, role):
|
||||
"""Turn the logged-in test operator into a Gerenciador SSO collaborator with the given perfil (identity source of truth is the SSO)."""
|
||||
from central.app.models import AdminUser
|
||||
with client._session_factory() as s:
|
||||
admin = s.scalars(select(AdminUser)).first()
|
||||
admin.auth_source, admin.password_hash, admin.external_subject, admin.external_role = "GTI_GERENCIADOR", None, "sso-subject-1", role
|
||||
s.commit()
|
||||
|
||||
|
||||
def _post(client, path, **data):
|
||||
return client.post(path, data={"csrf_token": client.cookies.get("gti_csrf"), **data}, follow_redirects=False)
|
||||
|
||||
|
||||
def test_write_modes_are_restricted_to_gerenciador_admins_and_every_denial_is_audited():
|
||||
client, agent_id, _ = _panel_client()
|
||||
_become(client, "PADRAO")
|
||||
for mode, extra in (("ENABLED", {"confirm": "PREENCHER"}), ("APPROVAL_REQUIRED", {})):
|
||||
assert _post(client, f"/agents/{agent_id}/rtc-mode", mode=mode, **extra).status_code == 403
|
||||
assert _post(client, f"/agents/{agent_id}/rtc-candidates", action="approve", all="1").status_code == 403
|
||||
assert _post(client, f"/agents/{agent_id}/rtc-candidates", action="apply_approved").status_code == 403
|
||||
assert _post(client, f"/agents/{agent_id}/rtc-rollback", audit_id="1", confirm="REVERTER").status_code == 403
|
||||
with client._session_factory() as s:
|
||||
assert s.scalars(select(Installation)).one().rtc_write_mode == "DISABLED"
|
||||
assert len([e for e in s.scalars(select(AuditEvent)) if e.action == "RTC_DENIED"]) == 5
|
||||
# read-only modes stay available to a normal collaborator, and the page tells them why the rest is disabled
|
||||
assert _post(client, f"/agents/{agent_id}/rtc-mode", mode="DRY_RUN").status_code == 303
|
||||
page = client.get(f"/agents/{agent_id}/rtc").text
|
||||
assert "Somente leitura para o seu perfil" in page and "disabled" in page
|
||||
|
||||
|
||||
def test_a_normal_collaborator_cannot_switch_off_a_privileged_mode_either():
|
||||
client, agent_id, _ = _panel_client()
|
||||
with client._session_factory() as s:
|
||||
s.scalars(select(Installation)).one().rtc_write_mode = "ENABLED"
|
||||
s.commit()
|
||||
_become(client, "PADRAO")
|
||||
assert _post(client, f"/agents/{agent_id}/rtc-mode", mode="DISABLED").status_code == 403
|
||||
with client._session_factory() as s:
|
||||
assert s.scalars(select(Installation)).one().rtc_write_mode == "ENABLED"
|
||||
|
||||
|
||||
def test_gerenciador_admin_can_change_modes_and_the_operator_is_recorded():
|
||||
client, agent_id, _ = _panel_client()
|
||||
_become(client, "ADMIN")
|
||||
assert _post(client, f"/agents/{agent_id}/rtc-mode", mode="ENABLED", confirm="PREENCHER").status_code == 303
|
||||
with client._session_factory() as s:
|
||||
assert s.scalars(select(Installation)).one().rtc_write_mode == "ENABLED"
|
||||
event = [e for e in s.scalars(select(AuditEvent)) if e.action == "RTC_WRITE_MODE"][-1]
|
||||
assert event.context["to"] == "ENABLED" and event.context["role"] == "ADMIN" and event.context["operator"]
|
||||
|
||||
Reference in New Issue
Block a user