Initial curated RAGcore source from c16667f0c9397e2bfc8dd0255b114fba23c8d05e
This commit is contained in:
commit
7ca726caf2
659 files changed
+98171
No files matched your search
@@ -0,0 +1,241 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user