"""Install-code enrollment flow: POST /v1/agents/enroll with install_code. Covers the universal-EXE enrollment path — the operator types a 16-char XXXX-XXXX-XXXX-XXXX installation code into Setup.exe, the Hub resolves the installation by the hash on its own (no installation_id, no enrollment.json), issues the permanent agent credential, and exhausts the code (replay-proof). Also covers rejection when code and legacy token are sent together, expiry, attempts/rate-limit and no-plaintext persistencen. """ import dataclasses import json import sys import tempfile import zipfile from datetime import datetime, timedelta, timezone from pathlib import Path import pytest from fastapi.testclient import TestClient from sqlalchemy import select from central.app import config, enrollment, rate_limit, release_storage from central.app.main import create_app from central.app.models import Agent, AgentEnrollment, AgentRelease, AuditEvent, Installation, Tenant from central.scripts.publish_agent_release import publish from central.tests.test_control_plane import seed, setup_db VERSION = "1.1.1" @pytest.fixture(autouse=True) def clear_rate_limit(): rate_limit._attempts.clear() yield rate_limit._attempts.clear() @pytest.fixture def hub(tmp_path, monkeypatch): replaced = dataclasses.replace( config.settings, agent_release_storage_path=str(tmp_path / "releases"), agent_enrollment_ttl_seconds=86400, base_url="https://gtifiscal.example.gti", web_cookie_secure=False, ) monkeypatch.setattr(config, "settings", replaced) if "app.config" in sys.modules: monkeypatch.setattr(sys.modules["app.config"], "settings", replaced) factory = setup_db() seed(factory) client = TestClient(create_app(database_probe=lambda: True, session_factory=factory, bootstrap_token=None)) return client, factory def login(client): response = client.post("/login", data={"username": "operator", "password": "a-secure-password"}, follow_redirects=False) assert response.status_code == 303 def csrf(client): return client.cookies.get("gti_csrf", "") @pytest.fixture def release_id(hub, tmp_path): _client, factory = hub data = io_bytes_tmp(tmp_path, VERSION) with factory() as session: release, _created = publish(session, data, version=VERSION) return release.id def io_bytes_tmp(tmp_path, version): from pathlib import Path buf = io_bytes_bundle(version) path = Path(tmp_path) / f"GTIFiscalAgent-{version}.zip" path.write_bytes(buf) return path def io_bytes_bundle(version): import io buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as z: z.writestr(f"GTIFiscalAgent-{version}/gti-agent.ps1", "# fake\n") z.writestr(f"GTIFiscalAgent-{version}/VERSION", version) return buf.getvalue() def tenant_id(factory): with factory() as session: tenant = session.scalar(select(Tenant).where(Tenant.name == "Acme")) # /agents/new only offers active GTI_GERENCIADOR-sourced tenants # (central/app/client_sync.py) — flip the shared fixture tenant so # existing form-based tests keep working under that rule. if tenant.source != "GTI_GERENCIADOR" or not tenant.active: tenant.source, tenant.external_client_id, tenant.active = "GTI_GERENCIADOR", tenant.id, True session.commit() return tenant.id def make_installation(factory, release_id, name="Servidor E2E"): with factory() as session: enr = AgentEnrollment(enrollment_uuid=str(__import__("uuid").uuid4()), tenant_id=tenant_id(factory), display_name=name, release_id=release_id, status="PENDING") session.add(enr) session.commit() return enr.enrollment_uuid def set_code(factory, uuid, code, ttl_seconds=86400): with factory() as session: enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == uuid)) enr.install_code_hash = enrollment.install_code_digest(code) enr.install_code_expires_at = datetime.now(timezone.utc) + timedelta(seconds=ttl_seconds) enr.install_code_created_at = datetime.now(timezone.utc) session.commit() def audits(factory): with factory() as session: return session.scalars(select(AuditEvent)).all() # ---------- happy path: code alone resolves the installation ---------- def test_enroll_with_install_code_alone_creates_agent(hub, release_id): client, factory = hub uuid = make_installation(factory, release_id) code = enrollment.generate_install_code() set_code(factory, uuid, code) enrolled = client.post("/v1/agents/enroll", json={"install_code": code, "agent_version": "1.1.1", "hostname": "WIN-E2E", "platform": "windows"}) assert enrolled.status_code == 200 body = enrolled.json() assert body["agent_id"] and body["agent_token"] and body["hostname_mismatch"] is False assert body["installation_id"] == uuid with factory() as session: agent = session.get(Agent, body["agent_id"]) assert agent.enabled and agent.agent_version == "1.1.1" installation = session.scalar(select(Installation).where(Installation.agent_id == agent.id)) assert installation.installation_id == uuid and installation.hostname == "WIN-E2E" enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == uuid)) assert enr.status == "ENROLLED" and enr.agent_id == agent.id assert enr.install_code_hash is None and enr.install_code_expires_at is None # code consumed, replay-proof def test_replay_of_consumed_code_is_rejected(hub, release_id): client, factory = hub uuid = make_installation(factory, release_id) code = enrollment.generate_install_code() set_code(factory, uuid, code) first = client.post("/v1/agents/enroll", json={"install_code": code}) assert first.status_code == 200 second = client.post("/v1/agents/enroll", json={"install_code": code}) assert second.status_code == 401 events = {e.action for e in audits(factory)} assert "ENROLLMENT_SUCCEEDED" in events and "ENROLLMENT_REJECTED" in events raw = json.dumps({"action": "ENROLLMENT_SUCCEEDED", "target": ["x"], "context": {"ip": "y"}}) + code # the code plaintext is never in any audit event context for event in audits(factory): assert code not in json.dumps(event.context) assert enrollment.normalize_install_code(code) not in json.dumps(event.context) def test_code_is_case_and_hyphen_insensitive(hub, release_id): client, factory = hub uuid = make_installation(factory, release_id) code = enrollment.generate_install_code() set_code(factory, uuid, code) typed = code.lower().replace("-", " ") enrolled = client.post("/v1/agents/enroll", json={"install_code": typed}) assert enrolled.status_code == 200 # ---------- rejections ---------- def test_invalid_code_format_rejected_without_lookup(hub, release_id): client, factory = hub invalid = client.post("/v1/agents/enroll", json={"install_code": "NOT-A-CODE"}) assert invalid.status_code == 400 or invalid.status_code == 401 assert "INVALID" in str(invalid.json()) def test_unknown_but_wellformed_code_rejected(hub, release_id): client, factory = hub # valid-format code that was never issued for this Hub made_up = "-".join(["0123"] * 4) rejected = client.post("/v1/agents/enroll", json={"install_code": made_up}) assert rejected.status_code == 401 assert "INVALID" in str(rejected.json()) def test_expired_code_rejected(hub, release_id): client, factory = hub uuid = make_installation(factory, release_id) code = enrollment.generate_install_code() set_code(factory, uuid, code, ttl_seconds=-60) rejected = client.post("/v1/agents/enroll", json={"install_code": code}) assert rejected.status_code == 401 assert "EXPIRED" in str(rejected.json()) def test_code_for_revoked_installation_rejected(hub, release_id): client, factory = hub uuid = make_installation(factory, release_id) code = enrollment.generate_install_code() set_code(factory, uuid, code) with factory() as session: enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == uuid)) enr.status = "REVOKED" session.commit() rejected = client.post("/v1/agents/enroll", json={"install_code": code}) assert rejected.status_code == 401 assert "REVOKED" in str(rejected.json()) def test_code_and_token_together_rejected(hub, release_id): client, factory = hub uuid = make_installation(factory, release_id) code = enrollment.generate_install_code() set_code(factory, uuid, code) rejected = client.post("/v1/agents/enroll", json={ "install_code": code, "installation_id": uuid, "enrollment_token": "x" * 43, }) assert rejected.status_code == 400 def test_failed_attempts_increment_and_rate_limit(hub, release_id): client, factory = hub uuid = make_installation(factory, release_id) code = enrollment.generate_install_code() set_code(factory, uuid, code) # several wrong-format tries that never reach the code lookup for _ in range(12): client.post("/v1/agents/enroll", json={"install_code": "NOT-A-CODE"}) assert rate_limit.blocked("enroll", "testclient") with factory() as session: enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == uuid)) assert enr.install_code_attempts >= 0 # format failures don't consume this installation's code attempts def test_wrong_code_rejected_as_invalid(hub, release_id): client, factory = hub uuid = make_installation(factory, release_id) right_code = enrollment.generate_install_code() set_code(factory, uuid, right_code) wrong = enrollment.generate_install_code() # well-formed but never issued attempt = client.post("/v1/agents/enroll", json={"install_code": wrong}) assert attempt.status_code == 401 assert "INVALID" in str(attempt.json()) def test_resolved_code_failures_increment_attempt_budget(hub, release_id): # A well-formed code that resolves to a REVOKED installation consumes the # installation's attempt budget (per-installation rate limit), same as an # expired one. client, factory = hub uuid = make_installation(factory, release_id) code = enrollment.generate_install_code() set_code(factory, uuid, code) with factory() as session: enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == uuid)) enr.status = "REVOKED" session.commit() for _ in range(3): client.post("/v1/agents/enroll", json={"install_code": code}) with factory() as session: enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == uuid)) assert enr.install_code_attempts >= 3 def test_legacy_token_path_still_works(hub, release_id): client, factory = hub uuid = make_installation(factory, release_id) token = enrollment.new_token() with factory() as session: enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == uuid)) enr.enrollment_token_hash = enrollment.token_digest(token) enr.enrollment_token_expires_at = datetime.now(timezone.utc) + timedelta(seconds=86400) session.commit() enrolled = client.post("/v1/agents/enroll", json={"installation_id": uuid, "enrollment_token": token}) assert enrolled.status_code == 200 # ---------- publish EXE releases (immutable) ---------- def test_publish_exe_release(hub, tmp_path): client, factory = hub from central.app import release_storage exe_bytes = b"MZ\x90\x00 fake windows installer exe body \x00" * 100 exe_path = Path(tmp_path) / "GTIFiscalAgent-Setup-1.1.1.exe" exe_path.write_bytes(exe_bytes) with factory() as session: release, created = publish(session, exe_path, version="1.1.1", notes="universal installer") assert created and release.status == "PUBLISHED" and release.published_at is not None assert release.filename == "GTIFiscalAgent-Setup-1.1.1.exe" assert release_storage.safe_path_for(release.storage_path).read_bytes() == exe_bytes def test_publish_exe_is_immutable(hub, tmp_path): _client, factory = hub exe_path = Path(tmp_path) / "GTIFiscalAgent-Setup-1.1.1.exe" exe_path.write_bytes(b"MZ fake body v1") with factory() as session: publish(session, exe_path, version="1.1.1") exe_path.write_bytes(b"MZ DIFFERENT body v2") with factory() as session: with pytest.raises(SystemExit): publish(session, exe_path, version="1.1.1") # refused: immutable once PUBLISHED def test_publish_identical_exe_is_noop(hub, tmp_path): _client, factory = hub exe_path = Path(tmp_path) / "GTIFiscalAgent-Setup-1.1.1.exe" exe_path.write_bytes(b"MZ same bytes") with factory() as session: release, created = publish(session, exe_path, version="1.1.1") assert created with factory() as session: release2, created2 = publish(session, exe_path, version="1.1.1") assert release2.id == release.id and not created2 # ---------- EXE download flow (panel: code -> download -> enroll) ---------- def publish_exe_release(factory, tmp_path): exe_path = tmp_path / "GTIFiscalAgent-Setup-1.1.1.exe" exe_path.write_bytes(b"MZ\x90\x00 GTIFiscalAgent installer 1.1.1 \x00" * 40) with factory() as session: release, _created = publish(session, exe_path, version="1.1.1") return release.id def test_panel_flow_code_generate_download_and_enroll_with_exe(hub, tmp_path): client, factory = hub release_id = publish_exe_release(factory, tmp_path) login(client) created = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": tenant_id(factory), "display_name": "Servidor EXE", "release_id": release_id}, follow_redirects=False) assert created.status_code == 303 installation_uuid = created.headers["location"].rsplit("/", 1)[-1] # generate the install code (only hash stored; plaintext shown once via ?code=) code_page = client.post(f"/agents/pending/{installation_uuid}/code", data={"csrf_token": csrf(client)}, follow_redirects=True) assert code_page.status_code == 200 # the redirect carried ?code=XXXX-... ; the page must show it code_addr = code_page.history[0].headers.get("location") if code_page.history else "" assert "code=" in code_addr install_code = code_addr.rsplit("code=", 1)[-1].split("&")[0] assert len(install_code) == 19 with factory() as session: enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == installation_uuid)) assert enr.install_code_hash == enrollment.install_code_digest(install_code) assert enr.install_code_expires_at is not None assert enr.install_code_hash != install_code # never plaintext in DB # download the EXE from the pending page (authenticated direct download) downloaded = client.get(f"/agents/pending/{installation_uuid}/download") assert downloaded.status_code == 200 assert downloaded.content.startswith(b"MZ") assert "attachment" in downloaded.headers["content-disposition"] assert "GTIFiscalAgent-Setup-1.1.1.exe" in downloaded.headers["content-disposition"] # enroll with just the code (no installation id, no token) anon = TestClient(client.app) enrolled = anon.post("/v1/agents/enroll", json={"install_code": install_code, "agent_version": "1.1.1", "hostname": "WIN-EXE"}) assert enrolled.status_code == 200 body = enrolled.json() assert body["agent_id"] and body["agent_token"] with factory() as session: enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == installation_uuid)) assert enr.status == "ENROLLED" and enr.install_code_hash is None # consumed agent = session.get(Agent, body["agent_id"]) assert agent.enabled and agent.agent_version == "1.1.1" events = {e.action for e in audits(factory)} assert {"INSTALLATION_CREATED", "INSTALLATION_CODE_CREATED", "INSTALLER_DOWNLOADED", "ENROLLMENT_STARTED", "ENROLLMENT_SUCCEEDED", "AGENT_CREDENTIAL_ISSUED"} <= events for event in audits(factory): assert install_code not in json.dumps(event.context) def test_rotation_generates_a_code_that_invalidates_the_previous(hub, tmp_path): client, factory = hub release_id = publish_exe_release(factory, tmp_path) login(client) created = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": tenant_id(factory), "display_name": "Rotação", "release_id": release_id}, follow_redirects=False) installation_uuid = created.headers["location"].rsplit("/", 1)[-1] first = client.post(f"/agents/pending/{installation_uuid}/code", data={"csrf_token": csrf(client)}, follow_redirects=True) first_code = first.history[0].headers["location"].rsplit("code=", 1)[-1].split("&")[0] second = client.post(f"/agents/pending/{installation_uuid}/code", data={"csrf_token": csrf(client)}, follow_redirects=True) second_code = second.history[0].headers["location"].rsplit("code=", 1)[-1].split("&")[0] assert first_code != second_code # first code no longer resolves (rotated), second does with factory() as session: enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == installation_uuid)) assert enr.install_code_hash == enrollment.install_code_digest(second_code) anon = TestClient(client.app) assert anon.post("/v1/agents/enroll", json={"install_code": first_code}).status_code == 401 assert anon.post("/v1/agents/enroll", json={"install_code": second_code}).status_code == 200 events = [e.action for e in audits(factory)] assert events.count("INSTALLATION_CODE_CREATED") >= 2 def test_revoke_clears_code_and_is_audited(hub, tmp_path): client, factory = hub release_id = publish_exe_release(factory, tmp_path) login(client) created = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": tenant_id(factory), "display_name": "Revoga", "release_id": release_id}, follow_redirects=False) installation_uuid = created.headers["location"].rsplit("/", 1)[-1] client.post(f"/agents/pending/{installation_uuid}/code", data={"csrf_token": csrf(client)}) revoked = client.post(f"/agents/pending/{installation_uuid}/revoke", data={"csrf_token": csrf(client)}, follow_redirects=False) assert revoked.status_code == 303 with factory() as session: enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == installation_uuid)) assert enr.status == "REVOKED" and enr.install_code_hash is None events = {e.action for e in audits(factory)} assert "INSTALLATION_CODE_CREATED" in events and "INSTALLATION_CODE_REVOKED" in events and "INSTALLATION_REVOKED" in events