151 lines
5.4 KiB
Python
151 lines
5.4 KiB
Python
"""Adversarial tests for the shared Bearer-token principal fallback."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from uuid import UUID, uuid4
|
|
|
|
import pytest
|
|
from fastapi import Request
|
|
|
|
from ragcore.api.v1.control.bearer_auth import bearer_principal
|
|
from ragcore.api.v1.control.dependencies import ControlAPIProblem
|
|
from ragcore.application.identity.credentials import ServiceAccountCredentialService
|
|
from ragcore.domain.authorization import Principal, PrincipalType
|
|
from ragcore.domain.control import Role, Scope, ServiceAccount, Workspace
|
|
from ragcore.infrastructure.database.identity import InMemoryCredentialRepository
|
|
|
|
PEPPER = "test-credential-pepper-that-is-long-enough"
|
|
|
|
|
|
def _token_factory(credential_id: UUID) -> str:
|
|
return f"rc_sa_{credential_id.hex}_{'a' * 43}"
|
|
|
|
|
|
def _request(headers: dict[bytes, bytes]) -> Request:
|
|
return Request({"type": "http", "headers": list(headers.items())})
|
|
|
|
|
|
def _service_account(scopes: set[Scope]) -> tuple[Workspace, ServiceAccount]:
|
|
frozen_scopes = frozenset(scopes)
|
|
workspace = Workspace(uuid4(), "Test", "test")
|
|
application = workspace.create_application(
|
|
id=uuid4(), name="n8n", slug="n8n", scopes=frozen_scopes
|
|
)
|
|
account = ServiceAccount(
|
|
id=uuid4(),
|
|
workspace_id=workspace.id,
|
|
application_id=application.id,
|
|
name="n8n worker",
|
|
scopes=frozen_scopes,
|
|
active=True,
|
|
)
|
|
return workspace, account
|
|
|
|
|
|
async def _credential_service(
|
|
account: ServiceAccount | None,
|
|
) -> ServiceAccountCredentialService:
|
|
async def load(account_id: UUID) -> ServiceAccount | None:
|
|
return account if account is not None and account_id == account.id else None
|
|
|
|
return ServiceAccountCredentialService(
|
|
InMemoryCredentialRepository(),
|
|
PEPPER,
|
|
service_account_loader=load,
|
|
token_factory=_token_factory,
|
|
)
|
|
|
|
|
|
@pytest.mark.security
|
|
async def test_bearer_principal_accepts_a_correctly_scoped_token() -> None:
|
|
workspace, account = _service_account({Scope.SEARCH})
|
|
credential_service = await _credential_service(account)
|
|
issued = await credential_service.issue(account, scopes={Scope.SEARCH})
|
|
|
|
dependency = bearer_principal(Scope.SEARCH)
|
|
request = _request({b"authorization": f"Bearer {issued.token}".encode()})
|
|
|
|
result = await dependency(request, credential_service)
|
|
|
|
assert result.principal_type is PrincipalType.SERVICE_ACCOUNT
|
|
assert result.id == account.id
|
|
assert result.workspace_id == workspace.id
|
|
assert Role.EDITOR in result.roles
|
|
|
|
|
|
@pytest.mark.security
|
|
async def test_bearer_principal_prefers_an_existing_session_principal() -> None:
|
|
workspace, account = _service_account({Scope.CONTEXT})
|
|
credential_service = await _credential_service(account)
|
|
issued = await credential_service.issue(account, scopes={Scope.CONTEXT})
|
|
|
|
session_principal = Principal(
|
|
id=uuid4(), workspace_id=workspace.id, principal_type=PrincipalType.HUMAN
|
|
)
|
|
request = _request({b"authorization": f"Bearer {issued.token}".encode()})
|
|
request.state.principal = session_principal
|
|
|
|
result = await bearer_principal(Scope.CONTEXT)(request, credential_service)
|
|
|
|
assert result is session_principal
|
|
|
|
|
|
@pytest.mark.security
|
|
async def test_bearer_principal_rejects_wrong_scope_missing_header_and_malformed_token() -> None:
|
|
_workspace, account = _service_account({Scope.SEARCH})
|
|
credential_service = await _credential_service(account)
|
|
issued = await credential_service.issue(account, scopes={Scope.SEARCH})
|
|
dependency = bearer_principal(Scope.ANSWER)
|
|
scoped_request = _request({b"authorization": f"Bearer {issued.token}".encode()})
|
|
|
|
with pytest.raises(ControlAPIProblem) as wrong_scope:
|
|
await dependency(scoped_request, credential_service)
|
|
assert wrong_scope.value.status == 401
|
|
|
|
with pytest.raises(ControlAPIProblem) as no_header:
|
|
await dependency(_request({}), credential_service)
|
|
assert no_header.value.status == 401
|
|
|
|
with pytest.raises(ControlAPIProblem) as malformed:
|
|
await dependency(
|
|
_request({b"authorization": b"Bearer not-a-real-token"}), credential_service
|
|
)
|
|
assert malformed.value.status == 401
|
|
|
|
|
|
@pytest.mark.security
|
|
async def test_bearer_principal_rejects_deactivated_service_account() -> None:
|
|
_workspace, account = _service_account({Scope.ANSWER})
|
|
repository = InMemoryCredentialRepository()
|
|
|
|
async def load_active(account_id: UUID) -> ServiceAccount | None:
|
|
return account if account_id == account.id else None
|
|
|
|
issuing_service = ServiceAccountCredentialService(
|
|
repository, PEPPER, service_account_loader=load_active, token_factory=_token_factory
|
|
)
|
|
issued = await issuing_service.issue(account, scopes={Scope.ANSWER})
|
|
|
|
inactive_account = ServiceAccount(
|
|
id=account.id,
|
|
workspace_id=account.workspace_id,
|
|
application_id=account.application_id,
|
|
name=account.name,
|
|
scopes=account.scopes,
|
|
active=False,
|
|
)
|
|
|
|
async def load_inactive(account_id: UUID) -> ServiceAccount | None:
|
|
return inactive_account if account_id == account.id else None
|
|
|
|
authenticating_service = ServiceAccountCredentialService(
|
|
repository, PEPPER, service_account_loader=load_inactive, token_factory=_token_factory
|
|
)
|
|
|
|
with pytest.raises(ControlAPIProblem) as error:
|
|
await bearer_principal(Scope.ANSWER)(
|
|
_request({b"authorization": f"Bearer {issued.token}".encode()}),
|
|
authenticating_service,
|
|
)
|
|
assert error.value.status == 401
|