"""Adversarial tests for service-account bearer credential lifecycle.""" from __future__ import annotations import asyncio from datetime import UTC, datetime, timedelta from uuid import UUID, uuid4 import pytest from ragcore.application.identity import ServiceAccountCredentialService from ragcore.domain.control import Scope, ServiceAccount, Workspace from ragcore.domain.identity import CredentialLimitExceeded, CredentialScopeViolation from ragcore.infrastructure.database.identity import InMemoryCredentialRepository NOW = datetime(2026, 1, 1, tzinfo=UTC) PEPPER = "test-credential-pepper-that-is-long-enough" def service_account(*, active: bool = True) -> ServiceAccount: workspace = Workspace(uuid4(), "Test", "test") application = workspace.create_application( id=uuid4(), name="Client", slug="client", scopes={Scope.SEARCH, Scope.CONTEXT} ) return ServiceAccount( id=uuid4(), workspace_id=workspace.id, application_id=application.id, name="Worker", scopes={Scope.SEARCH, Scope.CONTEXT}, active=active, ) def token_factory(credential_id: UUID) -> str: return f"rc_sa_{credential_id.hex}_{'a' * 43}" def build_service( repository: InMemoryCredentialRepository, accounts: tuple[ServiceAccount, ...] ) -> tuple[ServiceAccountCredentialService, dict[UUID, ServiceAccount]]: state = {account.id: account for account in accounts} async def load(account_id: UUID) -> ServiceAccount | None: return state.get(account_id) return ( ServiceAccountCredentialService( repository, PEPPER, service_account_loader=load, clock=lambda: NOW, token_factory=token_factory, ), state, ) @pytest.mark.security async def test_plaintext_is_returned_once_and_never_stored() -> None: repository = InMemoryCredentialRepository() account = service_account() service, _ = build_service(repository, (account,)) issued = await service.issue(account, scopes={Scope.SEARCH}) assert issued.token.startswith("rc_sa_") record = await repository.get(issued.credential.id, account.id, account.workspace_id) assert record is not None assert issued.token not in repr(record) assert issued.token.encode() != record.token_digest assert issued.token not in repr(repository.snapshot()) @pytest.mark.security async def test_wrong_scope_wrong_token_expired_and_revoked_fail_closed() -> None: repository = InMemoryCredentialRepository() account = service_account() service, _ = build_service(repository, (account,)) issued = await service.issue( account, scopes={Scope.SEARCH}, expires_at=NOW + timedelta(hours=1) ) assert await service.verify( issued.token, service_account=account, required_scopes={Scope.SEARCH} ) assert ( await service.verify(issued.token, service_account=account, required_scopes={Scope.CONTEXT}) is None ) assert await service.verify(issued.token[:-1] + "b", service_account=account) is None assert ( await service.verify(issued.token, service_account=account, now=NOW + timedelta(hours=1)) is None ) assert await service.revoke(issued.credential.id, account) assert await service.verify(issued.token, service_account=account) is None @pytest.mark.security async def test_authoritative_deactivation_and_scope_reduction_fail_closed() -> None: repository = InMemoryCredentialRepository() account = service_account() service, state = build_service(repository, (account,)) issued = await service.issue(account, scopes={Scope.SEARCH, Scope.CONTEXT}) state[account.id] = ServiceAccount( id=account.id, workspace_id=account.workspace_id, application_id=account.application_id, name=account.name, scopes={Scope.SEARCH}, active=True, ) assert ( await service.verify(issued.token, service_account=account, required_scopes={Scope.CONTEXT}) is None ) state[account.id] = ServiceAccount( id=account.id, workspace_id=account.workspace_id, application_id=account.application_id, name=account.name, scopes={Scope.SEARCH}, active=False, ) assert await service.verify(issued.token, service_account=account) is None @pytest.mark.security async def test_inactive_service_accounts_fail_closed() -> None: repository = InMemoryCredentialRepository() account = service_account() service, state = build_service(repository, (account,)) inactive_account = ServiceAccount( id=account.id, workspace_id=account.workspace_id, application_id=account.application_id, name=account.name, scopes=account.scopes, active=False, ) state[account.id] = inactive_account with pytest.raises(ValueError, match="authoritative"): await service.issue(inactive_account, scopes={Scope.SEARCH}) @pytest.mark.security async def test_scope_escalation_is_rejected() -> None: repository = InMemoryCredentialRepository() account = service_account() service, _ = build_service(repository, (account,)) with pytest.raises(CredentialScopeViolation): await service.issue(account, scopes={Scope.ANSWER}) @pytest.mark.security async def test_rotation_keeps_at_most_two_active_credentials_with_overlap() -> None: repository = InMemoryCredentialRepository() account = service_account() service, _ = build_service(repository, (account,)) first = await service.issue(account, scopes={Scope.SEARCH}) second = await service.issue(account, scopes={Scope.SEARCH}) with pytest.raises(CredentialLimitExceeded): await service.issue(account, scopes={Scope.SEARCH}) third = await service.issue(account, scopes={Scope.SEARCH}, rotate=True) assert await service.verify(first.token, service_account=account) is None assert await service.verify(second.token, service_account=account) assert await service.verify(third.token, service_account=account) assert len(await repository.list_active(account.id, account.workspace_id, NOW)) == 2 @pytest.mark.security async def test_authenticate_accepts_valid_token_without_a_caller_supplied_account() -> None: repository = InMemoryCredentialRepository() account = service_account() service, _ = build_service(repository, (account,)) issued = await service.issue(account, scopes={Scope.SEARCH}) authenticated = await service.authenticate(issued.token, required_scopes={Scope.SEARCH}) assert authenticated is not None assert authenticated.service_account_id == account.id assert authenticated.workspace_id == account.workspace_id @pytest.mark.security async def test_authenticate_rejects_malformed_unknown_wrong_scope_expired_and_revoked() -> None: repository = InMemoryCredentialRepository() account = service_account() service, _ = build_service(repository, (account,)) issued = await service.issue( account, scopes={Scope.SEARCH}, expires_at=NOW + timedelta(hours=1) ) assert await service.authenticate("not-a-real-token") is None assert await service.authenticate(f"rc_sa_{uuid4().hex}_{'a' * 43}") is None assert await service.authenticate(issued.token[:-1] + "b") is None assert await service.authenticate(issued.token, required_scopes={Scope.CONTEXT}) is None assert ( await service.authenticate(issued.token, now=NOW + timedelta(hours=1)) is None ) assert await service.revoke(issued.credential.id, account) assert await service.authenticate(issued.token) is None @pytest.mark.security async def test_authenticate_rejects_deactivated_service_account() -> None: repository = InMemoryCredentialRepository() account = service_account() service, state = build_service(repository, (account,)) issued = await service.issue(account, scopes={Scope.SEARCH}) state[account.id] = ServiceAccount( id=account.id, workspace_id=account.workspace_id, application_id=account.application_id, name=account.name, scopes=account.scopes, active=False, ) assert await service.authenticate(issued.token) is None @pytest.mark.security async def test_concurrent_issuance_stays_within_rotation_limit() -> None: repository = InMemoryCredentialRepository() account = service_account() service, _ = build_service(repository, (account,)) results = await asyncio.gather( *(service.issue(account, scopes={Scope.SEARCH}) for _ in range(3)), return_exceptions=True, ) assert sum(not isinstance(result, CredentialLimitExceeded) for result in results) == 2 assert len(await repository.list_active(account.id, account.workspace_id, NOW)) == 2