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,584 @@
|
||||
"""PostgreSQL security contract for existing-Agent credential transition."""
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import os
|
||||
import unittest
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from pydantic import ValidationError
|
||||
from psycopg.rows import dict_row
|
||||
|
||||
from app.agent_auth.exceptions import AgentAuthenticationFailed
|
||||
from app.agent_auth.repository import AgentAuthRepository
|
||||
from app.agent_auth.service import AgentAuthService, hash_agent_secret
|
||||
from app.agent_enrollment.exceptions import (
|
||||
AgentEnrollmentConflict,
|
||||
AgentEnrollmentCredentialCollision,
|
||||
AgentEnrollmentTokenInvalid,
|
||||
)
|
||||
from app.agent_enrollment.models import (
|
||||
AgentCredentialTransitionCreateRequest,
|
||||
AgentCredentialTransitionRedeemRequest,
|
||||
AgentEnrollmentCreateRequest,
|
||||
AgentEnrollRequest,
|
||||
)
|
||||
from app.agent_enrollment.repository import (
|
||||
AgentEnrollmentRepository,
|
||||
default_connection_factory,
|
||||
)
|
||||
from app.agent_enrollment.service import AgentEnrollmentService
|
||||
from app.agent_registry.repository import AgentRegistryRepository
|
||||
from app.config import settings
|
||||
|
||||
|
||||
PRODUCTION_AGENT_ID = "plexus-windows-agent"
|
||||
PRODUCTION_SITE_ID = UUID("00000000-0000-4000-8000-000000002001")
|
||||
PRODUCTION_ZONE_ID = UUID("00000000-0000-4000-8000-000000002002")
|
||||
PRODUCTION_MINER_ID = UUID("00000000-0000-4000-8000-000000002003")
|
||||
|
||||
|
||||
def connect():
|
||||
return default_connection_factory()
|
||||
|
||||
|
||||
class AgentCredentialTransitionPostgresTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if os.environ.get("MINEROPS_CREDENTIAL_TRANSITION_DISPOSABLE") != "1":
|
||||
raise unittest.SkipTest(
|
||||
"credential transition requires its disposable PostgreSQL runner"
|
||||
)
|
||||
if settings.minerops_agent_id != PRODUCTION_AGENT_ID:
|
||||
raise AssertionError(
|
||||
"runner must bind the production-shaped legacy Agent identity"
|
||||
)
|
||||
cls.service = AgentEnrollmentService(AgentEnrollmentRepository(connect))
|
||||
cls.auth = AgentAuthService(AgentAuthRepository(connect))
|
||||
|
||||
def create_legacy_agent(
|
||||
self,
|
||||
agent_id,
|
||||
*,
|
||||
site_id=None,
|
||||
zone_id=None,
|
||||
enabled=True,
|
||||
secret_hash=None,
|
||||
revoked=False,
|
||||
):
|
||||
site_id = site_id or uuid4()
|
||||
zone_id = zone_id or uuid4()
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO sites (id, name) VALUES (%s, %s)",
|
||||
(site_id, f"Transition Site {site_id.hex[:8]}"),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO agent_zones (id, site_id, name, is_enabled)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
""",
|
||||
(zone_id, site_id, "Default Zone", enabled),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO agent_zone_network_ranges (agent_zone_id, network)
|
||||
VALUES (%s, '10.220.0.0/24'::cidr)
|
||||
""",
|
||||
(zone_id,),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO minerops_site_agents (
|
||||
agent_id, site_id, agent_zone_id, site_name,
|
||||
display_name, agent_secret_hash,
|
||||
credential_created_at, credential_revoked_at
|
||||
)
|
||||
VALUES (
|
||||
%s, %s, %s, %s, %s, %s,
|
||||
CASE WHEN %s::boolean THEN now() ELSE NULL END,
|
||||
CASE WHEN %s::boolean THEN now() ELSE NULL END
|
||||
)
|
||||
""",
|
||||
(
|
||||
agent_id,
|
||||
site_id,
|
||||
zone_id,
|
||||
f"Transition Site {site_id.hex[:8]}",
|
||||
f"Agent {agent_id}"[:120],
|
||||
secret_hash,
|
||||
secret_hash is not None,
|
||||
revoked,
|
||||
),
|
||||
)
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
return site_id, zone_id
|
||||
|
||||
def create_transition(self, agent_id):
|
||||
return self.service.create_credential_transition(
|
||||
AgentCredentialTransitionCreateRequest(agent_id=agent_id),
|
||||
actor_user_id=None,
|
||||
)
|
||||
|
||||
def redeem_transition(self, token, secret):
|
||||
return AgentEnrollmentService(
|
||||
AgentEnrollmentRepository(connect)
|
||||
).redeem_credential_transition(
|
||||
AgentCredentialTransitionRedeemRequest(
|
||||
token=token,
|
||||
agent_secret_hash=hash_agent_secret(secret),
|
||||
)
|
||||
)
|
||||
|
||||
def agent_row(self, agent_id):
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor(row_factory=dict_row) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT agent_id, site_id, agent_zone_id, agent_secret_hash,
|
||||
credential_created_at, credential_revoked_at,
|
||||
legacy_auth_disabled_at
|
||||
FROM minerops_site_agents
|
||||
WHERE agent_id = %s
|
||||
""",
|
||||
(agent_id,),
|
||||
)
|
||||
return cur.fetchone()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def test_01_production_identity_transitions_without_routing_change(self):
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO sites (id, name)
|
||||
VALUES (%s, 'Plexus Mining Site')
|
||||
""",
|
||||
(PRODUCTION_SITE_ID,),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO agent_zones (id, site_id, name, is_enabled)
|
||||
VALUES (%s, %s, 'Default Zone', true)
|
||||
""",
|
||||
(PRODUCTION_ZONE_ID, PRODUCTION_SITE_ID),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO agent_zone_network_ranges (agent_zone_id, network)
|
||||
VALUES (%s, '192.0.2.0/24'::cidr)
|
||||
""",
|
||||
(PRODUCTION_ZONE_ID,),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO minerops_site_agents (
|
||||
agent_id, site_id, agent_zone_id, site_name, display_name
|
||||
)
|
||||
VALUES (%s, %s, %s, 'Plexus', 'Plexus Windows Agent')
|
||||
""",
|
||||
(
|
||||
PRODUCTION_AGENT_ID,
|
||||
PRODUCTION_SITE_ID,
|
||||
PRODUCTION_ZONE_ID,
|
||||
),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO miners (
|
||||
id, site_id, agent_zone_id, asset_tag,
|
||||
current_ip, current_mac, status
|
||||
)
|
||||
VALUES (
|
||||
%s, %s, %s, 'TRANSITION-PRODUCTION-MINER',
|
||||
'192.0.2.20', '02:00:00:00:20:03', 'online'
|
||||
)
|
||||
""",
|
||||
(
|
||||
PRODUCTION_MINER_ID,
|
||||
PRODUCTION_SITE_ID,
|
||||
PRODUCTION_ZONE_ID,
|
||||
),
|
||||
)
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
legacy_secret = settings.minerops_agent_shared_secret
|
||||
self.assertEqual(
|
||||
self.auth.authenticate(None, legacy_secret),
|
||||
PRODUCTION_AGENT_ID,
|
||||
)
|
||||
|
||||
transition = self.create_transition(PRODUCTION_AGENT_ID)
|
||||
token = transition["transition_token"]
|
||||
self.assertEqual(transition["target_agent_id"], PRODUCTION_AGENT_ID)
|
||||
self.assertEqual(transition["site_id"], PRODUCTION_SITE_ID)
|
||||
self.assertEqual(transition["agent_zone_id"], PRODUCTION_ZONE_ID)
|
||||
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor(row_factory=dict_row) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT token_hash, target_agent_id, used_at,
|
||||
enrolled_agent_id, site_id, agent_zone_id
|
||||
FROM agent_enrollments WHERE id = %s
|
||||
""",
|
||||
(transition["id"],),
|
||||
)
|
||||
authorization = cur.fetchone()
|
||||
finally:
|
||||
connection.close()
|
||||
self.assertEqual(authorization["token_hash"], hash_agent_secret(token))
|
||||
self.assertNotEqual(authorization["token_hash"], token)
|
||||
self.assertEqual(authorization["target_agent_id"], PRODUCTION_AGENT_ID)
|
||||
self.assertIsNone(authorization["used_at"])
|
||||
|
||||
new_secret = "production-shaped-per-agent-secret"
|
||||
result = self.redeem_transition(token, new_secret)
|
||||
self.assertEqual(result["agent_id"], PRODUCTION_AGENT_ID)
|
||||
self.assertNotIn("agent_secret", result)
|
||||
self.assertEqual(result["site_id"], PRODUCTION_SITE_ID)
|
||||
self.assertEqual(result["agent_zone_id"], PRODUCTION_ZONE_ID)
|
||||
|
||||
# An ambiguous response is safely retryable only with the same locally
|
||||
# generated credential; no second transition or audit event is created.
|
||||
retried = self.redeem_transition(token, new_secret)
|
||||
self.assertEqual(retried, result)
|
||||
with self.assertRaises(AgentEnrollmentTokenInvalid):
|
||||
self.redeem_transition(token, "different-secret-cannot-rotate")
|
||||
|
||||
self.assertEqual(
|
||||
self.auth.authenticate(PRODUCTION_AGENT_ID, new_secret),
|
||||
PRODUCTION_AGENT_ID,
|
||||
)
|
||||
for supplied_id in (None, PRODUCTION_AGENT_ID):
|
||||
with self.subTest(supplied_id=supplied_id), self.assertRaises(
|
||||
AgentAuthenticationFailed
|
||||
):
|
||||
self.auth.authenticate(supplied_id, legacy_secret)
|
||||
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor(row_factory=dict_row) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT site_id, agent_zone_id, agent_secret_hash,
|
||||
credential_created_at, legacy_auth_disabled_at
|
||||
FROM minerops_site_agents WHERE agent_id = %s
|
||||
""",
|
||||
(PRODUCTION_AGENT_ID,),
|
||||
)
|
||||
agent = cur.fetchone()
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT site_id, agent_zone_id, current_mac::text,
|
||||
host(current_ip) AS current_ip
|
||||
FROM miners WHERE id = %s
|
||||
""",
|
||||
(PRODUCTION_MINER_ID,),
|
||||
)
|
||||
miner = cur.fetchone()
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT count(*) AS retirement_count
|
||||
FROM agent_legacy_auth_retirements
|
||||
WHERE agent_id = %s
|
||||
""",
|
||||
(PRODUCTION_AGENT_ID,),
|
||||
)
|
||||
retirement_count = cur.fetchone()["retirement_count"]
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT count(*) AS redeemed_audits
|
||||
FROM audit_logs
|
||||
WHERE action = 'agent.credential_transition.redeemed'
|
||||
AND entity_id = %s
|
||||
""",
|
||||
(transition["id"],),
|
||||
)
|
||||
redeemed_audits = cur.fetchone()["redeemed_audits"]
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
self.assertEqual(agent["site_id"], PRODUCTION_SITE_ID)
|
||||
self.assertEqual(agent["agent_zone_id"], PRODUCTION_ZONE_ID)
|
||||
self.assertEqual(agent["agent_secret_hash"], hash_agent_secret(new_secret))
|
||||
self.assertIsNotNone(agent["credential_created_at"])
|
||||
self.assertIsNotNone(agent["legacy_auth_disabled_at"])
|
||||
self.assertEqual(miner["site_id"], PRODUCTION_SITE_ID)
|
||||
self.assertEqual(miner["agent_zone_id"], PRODUCTION_ZONE_ID)
|
||||
self.assertEqual(miner["current_mac"], "02:00:00:00:20:03")
|
||||
self.assertEqual(miner["current_ip"], "192.0.2.20")
|
||||
self.assertEqual(retirement_count, 1)
|
||||
self.assertEqual(redeemed_audits, 1)
|
||||
|
||||
# Remove deletable enrollment/current state to prove that the separate
|
||||
# append-only retirement identity still defeats fallback on recreation.
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"DELETE FROM agent_enrollments WHERE id = %s",
|
||||
(transition["id"],),
|
||||
)
|
||||
cur.execute(
|
||||
"DELETE FROM minerops_site_agents WHERE agent_id = %s",
|
||||
(PRODUCTION_AGENT_ID,),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO minerops_site_agents (
|
||||
agent_id, site_id, agent_zone_id, site_name
|
||||
) VALUES (%s, %s, %s, 'Recreated Plexus')
|
||||
""",
|
||||
(
|
||||
PRODUCTION_AGENT_ID,
|
||||
PRODUCTION_SITE_ID,
|
||||
PRODUCTION_ZONE_ID,
|
||||
),
|
||||
)
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
recreated = self.agent_row(PRODUCTION_AGENT_ID)
|
||||
self.assertIsNone(recreated["agent_secret_hash"])
|
||||
self.assertIsNotNone(recreated["legacy_auth_disabled_at"])
|
||||
with self.assertRaises(AgentAuthenticationFailed):
|
||||
self.auth.authenticate(PRODUCTION_AGENT_ID, legacy_secret)
|
||||
|
||||
def test_02_concurrent_redemption_has_one_credential_winner(self):
|
||||
agent_id = "transition-concurrent-agent"
|
||||
self.create_legacy_agent(agent_id)
|
||||
token = self.create_transition(agent_id)["transition_token"]
|
||||
secrets = ("concurrent-secret-a", "concurrent-secret-b")
|
||||
|
||||
def redeem(secret):
|
||||
try:
|
||||
return ("success", secret, self.redeem_transition(token, secret))
|
||||
except AgentEnrollmentTokenInvalid:
|
||||
return ("invalid", secret, None)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||
results = list(executor.map(redeem, secrets))
|
||||
|
||||
winners = [item for item in results if item[0] == "success"]
|
||||
losers = [item for item in results if item[0] == "invalid"]
|
||||
self.assertEqual(len(winners), 1)
|
||||
self.assertEqual(len(losers), 1)
|
||||
row = self.agent_row(agent_id)
|
||||
self.assertEqual(row["agent_secret_hash"], hash_agent_secret(winners[0][1]))
|
||||
|
||||
def test_03_authorization_cannot_substitute_identity_or_steal_zone(self):
|
||||
agent_a = "transition-bound-agent-a"
|
||||
agent_b = "transition-bound-agent-b"
|
||||
site_a, zone_a = self.create_legacy_agent(agent_a)
|
||||
site_b, zone_b = self.create_legacy_agent(agent_b)
|
||||
token = self.create_transition(agent_a)["transition_token"]
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
AgentCredentialTransitionRedeemRequest.model_validate(
|
||||
{
|
||||
"token": token,
|
||||
"agent_secret_hash": hash_agent_secret("bound-secret"),
|
||||
"agent_id": agent_b,
|
||||
}
|
||||
)
|
||||
|
||||
result = self.redeem_transition(token, "bound-secret")
|
||||
self.assertEqual(result["agent_id"], agent_a)
|
||||
self.assertEqual((result["site_id"], result["agent_zone_id"]), (site_a, zone_a))
|
||||
other = self.agent_row(agent_b)
|
||||
self.assertEqual((other["site_id"], other["agent_zone_id"]), (site_b, zone_b))
|
||||
self.assertIsNone(other["agent_secret_hash"])
|
||||
|
||||
def test_04_revoked_retired_and_disabled_targets_fail_closed(self):
|
||||
revoked = "transition-revoked-agent"
|
||||
self.create_legacy_agent(
|
||||
revoked,
|
||||
secret_hash=hash_agent_secret("revoked-secret"),
|
||||
revoked=True,
|
||||
)
|
||||
with self.assertRaises(AgentEnrollmentConflict):
|
||||
self.create_transition(revoked)
|
||||
|
||||
retired = "transition-retired-agent"
|
||||
self.create_legacy_agent(retired)
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO agent_legacy_auth_retirements (agent_id) VALUES (%s)",
|
||||
(retired,),
|
||||
)
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
with self.assertRaises(AgentEnrollmentConflict):
|
||||
self.create_transition(retired)
|
||||
|
||||
disabled = "transition-disabled-agent"
|
||||
self.create_legacy_agent(disabled, enabled=False)
|
||||
with self.assertRaises(AgentEnrollmentConflict):
|
||||
self.create_transition(disabled)
|
||||
|
||||
changed = "transition-zone-disabled-after-authorization"
|
||||
_, changed_zone = self.create_legacy_agent(changed)
|
||||
authorization = self.create_transition(changed)
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE agent_zones SET is_enabled = false WHERE id = %s",
|
||||
(changed_zone,),
|
||||
)
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
with self.assertRaises(AgentEnrollmentConflict):
|
||||
self.redeem_transition(
|
||||
authorization["transition_token"], "disabled-zone-secret"
|
||||
)
|
||||
self.assertIsNone(self.agent_row(changed)["agent_secret_hash"])
|
||||
|
||||
def test_05_credential_collision_rolls_back_retirement_and_token_use(self):
|
||||
collision_hash = hash_agent_secret("already-issued-secret")
|
||||
self.create_legacy_agent(
|
||||
"transition-existing-credential-holder",
|
||||
secret_hash=collision_hash,
|
||||
)
|
||||
target = "transition-rollback-target"
|
||||
self.create_legacy_agent(target)
|
||||
transition = self.create_transition(target)
|
||||
|
||||
with self.assertRaises(AgentEnrollmentCredentialCollision):
|
||||
self.service.redeem_credential_transition(
|
||||
AgentCredentialTransitionRedeemRequest(
|
||||
token=transition["transition_token"],
|
||||
agent_secret_hash=collision_hash,
|
||||
)
|
||||
)
|
||||
|
||||
row = self.agent_row(target)
|
||||
self.assertIsNone(row["agent_secret_hash"])
|
||||
self.assertIsNone(row["credential_created_at"])
|
||||
self.assertIsNone(row["legacy_auth_disabled_at"])
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT count(*) FROM agent_legacy_auth_retirements
|
||||
WHERE agent_id = %s
|
||||
""",
|
||||
(target,),
|
||||
)
|
||||
self.assertEqual(cur.fetchone()[0], 0)
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT used_at, enrolled_agent_id
|
||||
FROM agent_enrollments WHERE id = %s
|
||||
""",
|
||||
(transition["id"],),
|
||||
)
|
||||
self.assertEqual(cur.fetchone(), (None, None))
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def test_06_normal_enrollment_still_works_and_occupied_zone_is_protected(self):
|
||||
site_id, occupied_zone = self.create_legacy_agent(
|
||||
"transition-ordinary-occupied"
|
||||
)
|
||||
with self.assertRaises(AgentEnrollmentConflict):
|
||||
self.service.create_enrollment(
|
||||
AgentEnrollmentCreateRequest(
|
||||
site_id=site_id,
|
||||
agent_zone_id=occupied_zone,
|
||||
display_name="Must Not Steal Occupied Zone",
|
||||
),
|
||||
actor_user_id=None,
|
||||
)
|
||||
|
||||
free_zone = uuid4()
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO agent_zones (id, site_id, name, is_enabled)
|
||||
VALUES (%s, %s, 'Free Enrollment Zone', true)
|
||||
""",
|
||||
(free_zone, site_id),
|
||||
)
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
enrollment = self.service.create_enrollment(
|
||||
AgentEnrollmentCreateRequest(
|
||||
site_id=site_id,
|
||||
agent_zone_id=free_zone,
|
||||
display_name="Ordinary New Agent",
|
||||
),
|
||||
actor_user_id=None,
|
||||
)
|
||||
enrolled = self.service.redeem(
|
||||
AgentEnrollRequest(token=enrollment["enrollment_token"])
|
||||
)
|
||||
self.assertRegex(enrolled["agent_id"], r"^agent-[0-9a-f]{32}$")
|
||||
self.assertEqual(enrolled["agent_zone_id"], free_zone)
|
||||
|
||||
def test_07_registry_history_and_audit_never_expose_plaintext(self):
|
||||
agent_id = "transition-privacy-agent"
|
||||
self.create_legacy_agent(agent_id)
|
||||
transition = self.create_transition(agent_id)
|
||||
token = transition["transition_token"]
|
||||
|
||||
listed = self.service.list_enrollments()
|
||||
item = next(row for row in listed if row["id"] == transition["id"])
|
||||
self.assertEqual(item["target_agent_id"], agent_id)
|
||||
self.assertEqual(item["purpose"], "existing_agent_credential_transition")
|
||||
serialized_item = repr(item)
|
||||
self.assertNotIn(token, serialized_item)
|
||||
for forbidden in (
|
||||
"token_hash",
|
||||
"transition_token",
|
||||
"agent_secret",
|
||||
"agent_secret_hash",
|
||||
):
|
||||
self.assertNotIn(forbidden, item)
|
||||
|
||||
registry = AgentRegistryRepository().list_registry()
|
||||
registry_agent = next(
|
||||
row for row in registry["agents"] if row["agent_id"] == agent_id
|
||||
)
|
||||
for forbidden in ("agent_secret", "agent_secret_hash", "token_hash"):
|
||||
self.assertNotIn(forbidden, registry_agent)
|
||||
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT new_value_json::text
|
||||
FROM audit_logs
|
||||
WHERE entity_id = %s
|
||||
""",
|
||||
(transition["id"],),
|
||||
)
|
||||
audit_text = "\n".join(row[0] for row in cur.fetchall())
|
||||
finally:
|
||||
connection.close()
|
||||
self.assertNotIn(token, audit_text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user