feat(agent): add durable existing-agent credential transitions
This commit is contained in:
1 parent
abf5654a25
commit
3cfb0d1abe
33 files changed
+3828
-54
No files matched your search
@@ -0,0 +1,245 @@
|
||||
"""Disposable PostgreSQL contract for durable legacy-auth retirement."""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from psycopg.errors import CheckViolation
|
||||
|
||||
from app.agent_auth.exceptions import (
|
||||
AgentAuthenticationFailed,
|
||||
AgentCredentialRevoked,
|
||||
)
|
||||
from app.agent_auth.repository import AgentAuthRepository, default_connection_factory
|
||||
from app.agent_auth.service import AgentAuthService
|
||||
from app.config import settings
|
||||
|
||||
|
||||
class AgentLegacyAuthRetirementPostgresTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if os.environ.get("MINEROPS_LEGACY_RETIREMENT_DISPOSABLE") != "1":
|
||||
raise unittest.SkipTest(
|
||||
"legacy-auth retirement requires its disposable PostgreSQL runner"
|
||||
)
|
||||
cls.case = os.environ["MINEROPS_LEGACY_RETIREMENT_CASE"]
|
||||
cls.agent_id = settings.minerops_agent_id
|
||||
cls.auth = AgentAuthService(AgentAuthRepository())
|
||||
|
||||
def row(self):
|
||||
connection = default_connection_factory()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT agent_secret_hash, credential_created_at,
|
||||
credential_revoked_at, legacy_auth_disabled_at
|
||||
FROM minerops_site_agents
|
||||
WHERE agent_id = %s
|
||||
""",
|
||||
(self.agent_id,),
|
||||
)
|
||||
return cur.fetchone()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def retirement_row(self, agent_id=None):
|
||||
connection = default_connection_factory()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT agent_id, retired_at
|
||||
FROM agent_legacy_auth_retirements
|
||||
WHERE agent_id = %s
|
||||
""",
|
||||
(agent_id or self.agent_id,),
|
||||
)
|
||||
return cur.fetchone()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def test_01_migration_backfill_is_selective(self):
|
||||
row = self.row()
|
||||
self.assertIsNotNone(row)
|
||||
if self.case == "legacy":
|
||||
self.assertEqual(row, (None, None, None, None))
|
||||
self.assertIsNone(self.retirement_row())
|
||||
else:
|
||||
self.assertIsNotNone(row[3])
|
||||
retirement = self.retirement_row()
|
||||
self.assertIsNotNone(retirement)
|
||||
self.assertEqual(retirement[1], row[3])
|
||||
|
||||
def test_02_real_repository_authentication_obeys_retirement(self):
|
||||
secret = settings.minerops_agent_shared_secret
|
||||
if self.case == "legacy":
|
||||
self.assertEqual(self.auth.authenticate(None, secret), self.agent_id)
|
||||
self.assertEqual(
|
||||
self.auth.authenticate(self.agent_id, secret), self.agent_id
|
||||
)
|
||||
return
|
||||
|
||||
for agent_id in (None, self.agent_id):
|
||||
with self.subTest(agent_id=agent_id), self.assertRaises(
|
||||
AgentCredentialRevoked
|
||||
):
|
||||
self.auth.authenticate(agent_id, secret)
|
||||
|
||||
def test_03_clearing_credentials_cannot_restore_global_fallback(self):
|
||||
if self.case == "legacy":
|
||||
self.skipTest("uncredentialed legacy case must remain eligible")
|
||||
|
||||
connection = default_connection_factory()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE minerops_site_agents
|
||||
SET agent_secret_hash = NULL,
|
||||
credential_created_at = NULL,
|
||||
credential_revoked_at = NULL
|
||||
WHERE agent_id = %s
|
||||
""",
|
||||
(self.agent_id,),
|
||||
)
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
row = self.row()
|
||||
self.assertEqual(row[:3], (None, None, None))
|
||||
self.assertIsNotNone(row[3])
|
||||
for agent_id in (None, self.agent_id):
|
||||
with self.subTest(agent_id=agent_id), self.assertRaises(
|
||||
AgentAuthenticationFailed
|
||||
):
|
||||
self.auth.authenticate(
|
||||
agent_id, settings.minerops_agent_shared_secret
|
||||
)
|
||||
|
||||
def test_04_deletion_and_recreation_cannot_restore_fallback(self):
|
||||
if self.case == "legacy":
|
||||
self.skipTest("uncredentialed legacy case has no retirement history")
|
||||
|
||||
retirement = self.retirement_row()
|
||||
self.assertIsNotNone(retirement)
|
||||
|
||||
connection = default_connection_factory()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"DELETE FROM minerops_site_agents WHERE agent_id = %s",
|
||||
(self.agent_id,),
|
||||
)
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
self.assertIsNone(self.row())
|
||||
self.assertEqual(self.retirement_row(), retirement)
|
||||
for agent_id in (None, self.agent_id):
|
||||
with self.subTest(state="deleted", agent_id=agent_id), self.assertRaises(
|
||||
AgentAuthenticationFailed
|
||||
):
|
||||
self.auth.authenticate(
|
||||
agent_id, settings.minerops_agent_shared_secret
|
||||
)
|
||||
|
||||
connection = default_connection_factory()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO minerops_site_agents (agent_id, site_name)
|
||||
VALUES (%s, 'Recreated')
|
||||
""",
|
||||
(self.agent_id,),
|
||||
)
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
recreated = self.row()
|
||||
self.assertEqual(recreated[:3], (None, None, None))
|
||||
self.assertEqual(recreated[3], retirement[1])
|
||||
for agent_id in (None, self.agent_id):
|
||||
with self.subTest(state="recreated", agent_id=agent_id), self.assertRaises(
|
||||
AgentAuthenticationFailed
|
||||
):
|
||||
self.auth.authenticate(
|
||||
agent_id, settings.minerops_agent_shared_secret
|
||||
)
|
||||
|
||||
def test_05_retirement_ledger_and_projection_are_one_way(self):
|
||||
if self.case == "legacy":
|
||||
self.skipTest("uncredentialed legacy case has no retirement history")
|
||||
|
||||
statements = (
|
||||
"""
|
||||
UPDATE agent_legacy_auth_retirements
|
||||
SET retired_at = retired_at + interval '1 second'
|
||||
WHERE agent_id = %s
|
||||
""",
|
||||
"""
|
||||
DELETE FROM agent_legacy_auth_retirements
|
||||
WHERE agent_id = %s
|
||||
""",
|
||||
"TRUNCATE agent_legacy_auth_retirements",
|
||||
"""
|
||||
UPDATE minerops_site_agents
|
||||
SET legacy_auth_disabled_at = NULL
|
||||
WHERE agent_id = %s
|
||||
""",
|
||||
)
|
||||
for statement in statements:
|
||||
connection = default_connection_factory()
|
||||
try:
|
||||
with self.subTest(statement=" ".join(statement.split())):
|
||||
with self.assertRaises(CheckViolation):
|
||||
with connection.cursor() as cur:
|
||||
if "%s" in statement:
|
||||
cur.execute(statement, (self.agent_id,))
|
||||
else:
|
||||
cur.execute(statement)
|
||||
connection.rollback()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
self.assertIsNotNone(self.retirement_row())
|
||||
self.assertIsNotNone(self.row()[3])
|
||||
|
||||
def test_06_other_legacy_identity_is_not_accidentally_retired(self):
|
||||
if self.case == "legacy":
|
||||
self.skipTest("cross-identity check runs in the retired case")
|
||||
|
||||
other_id = self.agent_id + "-untouched"
|
||||
connection = default_connection_factory()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO minerops_site_agents (agent_id, site_name)
|
||||
VALUES (%s, 'Untouched')
|
||||
""",
|
||||
(other_id,),
|
||||
)
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
self.assertIsNone(self.retirement_row(other_id))
|
||||
original_legacy_id = settings.minerops_agent_id
|
||||
settings.minerops_agent_id = other_id
|
||||
try:
|
||||
self.assertEqual(
|
||||
self.auth.authenticate(
|
||||
other_id, settings.minerops_agent_shared_secret
|
||||
),
|
||||
other_id,
|
||||
)
|
||||
finally:
|
||||
settings.minerops_agent_id = original_legacy_id
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user