feat: add secure agent enrollment
This commit is contained in:
1 parent
82309db79a
commit
d9f0da694b
26 files changed
+2942
-76
No files matched your search
@@ -0,0 +1,157 @@
|
||||
"""Focused tests for secure Agent enrollment."""
|
||||
|
||||
import re
|
||||
import unittest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from uuid import UUID
|
||||
|
||||
from app.agent_auth.service import hash_agent_secret
|
||||
from app.agent_enrollment.models import (
|
||||
AgentEnrollmentCreateRequest,
|
||||
AgentEnrollRequest,
|
||||
)
|
||||
from app.agent_enrollment.service import AgentEnrollmentService
|
||||
from app.identity import machine_agent_request, required_api_permission
|
||||
|
||||
|
||||
SITE_ID = UUID("00000000-0000-4000-8000-000000001700")
|
||||
ZONE_ID = UUID("00000000-0000-4000-8000-000000001701")
|
||||
USER_ID = UUID("00000000-0000-4000-8000-000000001702")
|
||||
ENROLLMENT_ID = UUID("00000000-0000-4000-8000-000000001703")
|
||||
NOW = datetime(2026, 8, 26, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
class FakeAgentEnrollmentRepository:
|
||||
def __init__(self):
|
||||
self.created = None
|
||||
self.redeemed = None
|
||||
|
||||
def create_enrollment(self, **kwargs):
|
||||
self.created = kwargs
|
||||
return {
|
||||
"id": ENROLLMENT_ID,
|
||||
"site_id": SITE_ID,
|
||||
"site_name": "Test Site",
|
||||
"agent_zone_id": ZONE_ID,
|
||||
"agent_zone_name": "Test Zone",
|
||||
"display_name": kwargs["display_name"],
|
||||
"created_at": NOW,
|
||||
"expires_at": NOW + timedelta(minutes=30),
|
||||
}
|
||||
|
||||
def list_enrollments(self, **kwargs):
|
||||
return []
|
||||
|
||||
def redeem_enrollment(self, **kwargs):
|
||||
self.redeemed = kwargs
|
||||
return {
|
||||
"agent_id": kwargs["agent_id"],
|
||||
"site_id": SITE_ID,
|
||||
"site_name": "Test Site",
|
||||
"agent_zone_id": ZONE_ID,
|
||||
"agent_zone_name": "Test Zone",
|
||||
"display_name": "Test Agent",
|
||||
"ip_ranges": ["10.30.0.0/22"],
|
||||
}
|
||||
|
||||
|
||||
class AgentEnrollmentServiceTests(unittest.TestCase):
|
||||
def test_create_returns_plaintext_token_once_but_repository_gets_only_hash(self):
|
||||
repository = FakeAgentEnrollmentRepository()
|
||||
service = AgentEnrollmentService(repository)
|
||||
|
||||
result = service.create_enrollment(
|
||||
AgentEnrollmentCreateRequest(
|
||||
site_id=SITE_ID,
|
||||
agent_zone_id=ZONE_ID,
|
||||
display_name=" Test Agent ",
|
||||
),
|
||||
actor_user_id=USER_ID,
|
||||
)
|
||||
|
||||
token = result["enrollment_token"]
|
||||
|
||||
self.assertGreaterEqual(len(token), 32)
|
||||
self.assertNotEqual(repository.created["token_hash"], token)
|
||||
self.assertEqual(
|
||||
repository.created["token_hash"],
|
||||
hash_agent_secret(token),
|
||||
)
|
||||
self.assertRegex(
|
||||
repository.created["token_hash"],
|
||||
r"^[0-9a-f]{64}$",
|
||||
)
|
||||
self.assertEqual(
|
||||
repository.created["display_name"],
|
||||
"Test Agent",
|
||||
)
|
||||
|
||||
serialized = repr(repository.created)
|
||||
self.assertNotIn(token, serialized)
|
||||
|
||||
def test_redeem_returns_plaintext_secret_but_repository_gets_only_hash(self):
|
||||
repository = FakeAgentEnrollmentRepository()
|
||||
service = AgentEnrollmentService(repository)
|
||||
|
||||
result = service.redeem(
|
||||
AgentEnrollRequest(
|
||||
token="A" * 43,
|
||||
)
|
||||
)
|
||||
|
||||
secret = result["agent_secret"]
|
||||
|
||||
self.assertGreaterEqual(len(secret), 32)
|
||||
self.assertNotEqual(
|
||||
repository.redeemed["agent_secret_hash"],
|
||||
secret,
|
||||
)
|
||||
self.assertEqual(
|
||||
repository.redeemed["agent_secret_hash"],
|
||||
hash_agent_secret(secret),
|
||||
)
|
||||
self.assertRegex(
|
||||
repository.redeemed["agent_secret_hash"],
|
||||
r"^[0-9a-f]{64}$",
|
||||
)
|
||||
|
||||
self.assertRegex(
|
||||
result["agent_id"],
|
||||
r"^agent-[0-9a-f]{32}$",
|
||||
)
|
||||
|
||||
serialized = repr(repository.redeemed)
|
||||
self.assertNotIn(secret, serialized)
|
||||
|
||||
def test_enrollment_admin_permission_mapping_is_explicit(self):
|
||||
self.assertEqual(
|
||||
required_api_permission("GET", "/api/agent-enrollments"),
|
||||
"agents.view",
|
||||
)
|
||||
self.assertEqual(
|
||||
required_api_permission("POST", "/api/agent-enrollments"),
|
||||
"agents.manage",
|
||||
)
|
||||
|
||||
def test_bootstrap_endpoint_is_machine_exempt_but_admin_endpoint_is_not(self):
|
||||
self.assertTrue(machine_agent_request("/api/agent/enroll"))
|
||||
self.assertFalse(
|
||||
machine_agent_request("/api/agent-enrollments")
|
||||
)
|
||||
|
||||
def test_generated_identifiers_are_bounded(self):
|
||||
repository = FakeAgentEnrollmentRepository()
|
||||
service = AgentEnrollmentService(repository)
|
||||
|
||||
result = service.redeem(
|
||||
AgentEnrollRequest(token="B" * 43)
|
||||
)
|
||||
|
||||
self.assertLessEqual(len(result["agent_id"]), 255)
|
||||
self.assertTrue(
|
||||
re.fullmatch(r"agent-[0-9a-f]{32}", result["agent_id"])
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user