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,388 @@
|
||||
"""A workspace-scoped operator can neither observe nor mutate another
|
||||
workspace's jobs -- and a job with no owner belongs to nobody.
|
||||
|
||||
These tests drive the real Control Plane routes against a job store that
|
||||
records every query it is asked to run, so they check two different things at
|
||||
once:
|
||||
|
||||
* what the *screen* shows (no foreign subject, source name or failure code);
|
||||
* what the *repository* was asked for (an ownership predicate, not a global
|
||||
query whose rows were filtered afterwards).
|
||||
|
||||
The second matters as much as the first. A listing that reads every tenant's
|
||||
jobs and hides some in the template has still pulled them into this process;
|
||||
the next template change, log line or timing measurement leaks them. So the
|
||||
fake asserts on the scope it received, and a global read by a workspace-scoped
|
||||
principal fails the test even when nothing foreign is rendered.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from ragcore.api.v1.control.dependencies import get_principal
|
||||
from ragcore.application.jobs import Job, JobAttempt, JobListFilter, JobListPage, JobStatus
|
||||
from ragcore.domain.authorization import Principal, PrincipalType
|
||||
from ragcore.domain.control import Role
|
||||
from ragcore.main import create_app
|
||||
|
||||
WORKSPACE_A = uuid4()
|
||||
WORKSPACE_B = uuid4()
|
||||
SPACE_A = uuid4()
|
||||
SPACE_B = uuid4()
|
||||
SOURCE_A = uuid4()
|
||||
SOURCE_B = uuid4()
|
||||
|
||||
JOB_A1 = uuid4()
|
||||
JOB_A2 = uuid4()
|
||||
JOB_B1 = uuid4()
|
||||
JOB_LEGACY = uuid4()
|
||||
|
||||
|
||||
def _principal(workspace_id: UUID, role: Role) -> Principal:
|
||||
return Principal(uuid4(), workspace_id, PrincipalType.HUMAN, frozenset({role}))
|
||||
|
||||
|
||||
def _job(
|
||||
job_id: UUID,
|
||||
*,
|
||||
workspace_id: UUID | None,
|
||||
space_id: UUID | None,
|
||||
source_id: UUID | None,
|
||||
name: str,
|
||||
key: str,
|
||||
) -> Job:
|
||||
now = datetime.now(UTC)
|
||||
return Job(
|
||||
id=job_id,
|
||||
workspace_id=workspace_id,
|
||||
job_key=key,
|
||||
operation="ingest_upload",
|
||||
target_key=f"digest-{key}",
|
||||
profile_version="upload-v1",
|
||||
payload={
|
||||
"original_name": name,
|
||||
"source_id": str(source_id) if source_id else None,
|
||||
# Payload ownership is deliberately still present: it is what the
|
||||
# old fail-open check read, so a regression that starts trusting
|
||||
# it again would pass without this and fail with it.
|
||||
"workspace_id": str(workspace_id) if workspace_id else None,
|
||||
"space_id": str(space_id) if space_id else None,
|
||||
"media_type": "text/markdown",
|
||||
},
|
||||
status=JobStatus.TERMINAL,
|
||||
available_at=now - timedelta(minutes=3),
|
||||
attempt_count=3,
|
||||
last_error_code="retryable_error",
|
||||
last_error_details={"failure_code": f"failed_{key}"},
|
||||
)
|
||||
|
||||
|
||||
class RecordingJobRepository:
|
||||
"""A job store that enforces ownership in the query, like the real one."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.jobs: dict[UUID, Job] = {
|
||||
JOB_A1: _job(
|
||||
JOB_A1,
|
||||
workspace_id=WORKSPACE_A,
|
||||
space_id=SPACE_A,
|
||||
source_id=SOURCE_A,
|
||||
name="alpha-one.md",
|
||||
key="a1",
|
||||
),
|
||||
JOB_A2: _job(
|
||||
JOB_A2,
|
||||
workspace_id=WORKSPACE_A,
|
||||
space_id=SPACE_A,
|
||||
source_id=SOURCE_A,
|
||||
name="alpha-two.md",
|
||||
key="a2",
|
||||
),
|
||||
JOB_B1: _job(
|
||||
JOB_B1,
|
||||
workspace_id=WORKSPACE_B,
|
||||
space_id=SPACE_B,
|
||||
source_id=SOURCE_B,
|
||||
name="bravo-secret.md",
|
||||
key="b1",
|
||||
),
|
||||
JOB_LEGACY: _job(
|
||||
JOB_LEGACY,
|
||||
workspace_id=None,
|
||||
space_id=None,
|
||||
source_id=None,
|
||||
name="legacy-unowned.md",
|
||||
key="legacy",
|
||||
),
|
||||
}
|
||||
self.requeued: list[UUID] = []
|
||||
self.scopes: list[tuple[str, UUID | None]] = []
|
||||
self.filters: list[JobListFilter] = []
|
||||
|
||||
def _owned(self, workspace_id: UUID | None) -> list[Job]:
|
||||
if workspace_id is None:
|
||||
return list(self.jobs.values())
|
||||
return [job for job in self.jobs.values() if job.workspace_id == workspace_id]
|
||||
|
||||
async def list_jobs(
|
||||
self, *, workspace_id: UUID | None, filters: JobListFilter
|
||||
) -> JobListPage:
|
||||
self.scopes.append(("list_jobs", workspace_id))
|
||||
self.filters.append(filters)
|
||||
matching = [
|
||||
job
|
||||
for job in self._owned(workspace_id)
|
||||
if (filters.status is None or job.status is filters.status)
|
||||
and (filters.operation is None or job.operation == filters.operation)
|
||||
]
|
||||
window = matching[filters.offset : filters.offset + filters.limit]
|
||||
return JobListPage(
|
||||
items=tuple(window),
|
||||
total=len(matching),
|
||||
limit=filters.limit,
|
||||
offset=filters.offset,
|
||||
)
|
||||
|
||||
async def status_counts(self, *, workspace_id: UUID | None) -> dict[str, int]:
|
||||
self.scopes.append(("status_counts", workspace_id))
|
||||
counts: dict[str, int] = {}
|
||||
for job in self._owned(workspace_id):
|
||||
counts[job.status.value] = counts.get(job.status.value, 0) + 1
|
||||
return counts
|
||||
|
||||
async def get(self, job_id: UUID) -> Job | None:
|
||||
raise AssertionError("principal-facing code must use get_owned, never get")
|
||||
|
||||
async def get_owned(self, job_id: UUID, *, workspace_id: UUID | None) -> Job | None:
|
||||
self.scopes.append(("get_owned", workspace_id))
|
||||
job = self.jobs.get(job_id)
|
||||
if job is None:
|
||||
return None
|
||||
if workspace_id is not None and job.workspace_id != workspace_id:
|
||||
return None
|
||||
return job
|
||||
|
||||
async def list_attempts(self, job_id: UUID) -> tuple[JobAttempt, ...]:
|
||||
started = datetime(2026, 8, 20, 9, 0, tzinfo=UTC)
|
||||
return (
|
||||
JobAttempt(
|
||||
attempt_number=1,
|
||||
worker_id="worker-7",
|
||||
status="terminal",
|
||||
leased_at=started,
|
||||
heartbeat_at=started,
|
||||
finished_at=started + timedelta(seconds=9),
|
||||
error_code="retryable_error",
|
||||
),
|
||||
)
|
||||
|
||||
async def requeue(
|
||||
self, job_id: UUID, *, workspace_id: UUID | None, now: datetime
|
||||
) -> Job | None:
|
||||
self.scopes.append(("requeue", workspace_id))
|
||||
job = self.jobs.get(job_id)
|
||||
if job is None or job.status is not JobStatus.TERMINAL:
|
||||
return None
|
||||
if workspace_id is not None and job.workspace_id != workspace_id:
|
||||
return None
|
||||
self.jobs[job_id] = replace(job, status=JobStatus.READY, attempt_count=0)
|
||||
self.requeued.append(job_id)
|
||||
return self.jobs[job_id]
|
||||
|
||||
|
||||
def _client(principal: Principal) -> tuple[TestClient, RecordingJobRepository]:
|
||||
app = create_app()
|
||||
repository = RecordingJobRepository()
|
||||
app.dependency_overrides[get_principal] = lambda: principal
|
||||
app.state.job_repository = repository
|
||||
# No database: audit writes and source-name enrichment degrade quietly,
|
||||
# which is what the tests want -- the subject under test is the scope.
|
||||
app.state.session_factory = None
|
||||
return TestClient(app), repository
|
||||
|
||||
|
||||
def _workspace_scopes(repository: RecordingJobRepository) -> set[UUID | None]:
|
||||
return {scope for _, scope in repository.scopes}
|
||||
|
||||
|
||||
# -- listing ---------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
def test_workspace_admin_job_list_shows_only_its_own_workspace() -> None:
|
||||
client, repository = _client(_principal(WORKSPACE_A, Role.WORKSPACE_ADMIN))
|
||||
|
||||
response = client.get("/admin/jobs")
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.text
|
||||
assert "alpha-one.md" in body and "alpha-two.md" in body
|
||||
assert "bravo-secret.md" not in body
|
||||
assert str(JOB_B1) not in body
|
||||
assert str(SOURCE_B) not in body
|
||||
assert "failed_b1" not in body
|
||||
# Not merely filtered afterwards: the repository was never asked for a
|
||||
# global result set in the first place.
|
||||
assert _workspace_scopes(repository) == {WORKSPACE_A}
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
def test_status_counters_do_not_count_foreign_jobs() -> None:
|
||||
client, repository = _client(_principal(WORKSPACE_A, Role.WORKSPACE_ADMIN))
|
||||
|
||||
response = client.get("/admin/jobs")
|
||||
|
||||
assert response.status_code == 200
|
||||
# Two terminal jobs in A; B's and the legacy one must not be counted. The
|
||||
# count is rendered in the "Failed" card, so a leak would show as 3 or 4.
|
||||
assert ">2</p>" in response.text
|
||||
assert ("status_counts", WORKSPACE_A) in repository.scopes
|
||||
assert ("status_counts", None) not in repository.scopes
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
def test_status_filter_cannot_widen_the_scope() -> None:
|
||||
client, repository = _client(_principal(WORKSPACE_A, Role.WORKSPACE_ADMIN))
|
||||
|
||||
response = client.get("/admin/jobs?status=terminal&operation=ingest_upload")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "bravo-secret.md" not in response.text
|
||||
assert _workspace_scopes(repository) == {WORKSPACE_A}
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
def test_job_screens_state_their_scope() -> None:
|
||||
scoped, _ = _client(_principal(WORKSPACE_A, Role.WORKSPACE_ADMIN))
|
||||
assert "This workspace" in scoped.get("/admin/jobs").text
|
||||
|
||||
platform, _ = _client(_principal(WORKSPACE_A, Role.PLATFORM_ADMIN))
|
||||
assert "All workspaces" in platform.get("/admin/jobs").text
|
||||
|
||||
|
||||
# -- detail ----------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
def test_foreign_job_detail_is_denied_without_leaking_metadata() -> None:
|
||||
client, _ = _client(_principal(WORKSPACE_A, Role.WORKSPACE_ADMIN))
|
||||
|
||||
response = client.get(f"/admin/jobs/{JOB_B1}")
|
||||
|
||||
assert response.status_code == 404
|
||||
assert "bravo-secret.md" not in response.text
|
||||
assert "failed_b1" not in response.text
|
||||
assert str(SOURCE_B) not in response.text
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
def test_own_job_detail_still_renders() -> None:
|
||||
client, _ = _client(_principal(WORKSPACE_A, Role.WORKSPACE_ADMIN))
|
||||
|
||||
response = client.get(f"/admin/jobs/{JOB_A1}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "alpha-one.md" in response.text
|
||||
assert "worker-7" in response.text
|
||||
|
||||
|
||||
# -- mutation --------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
def test_foreign_job_retry_is_denied_and_leaves_the_job_untouched() -> None:
|
||||
client, repository = _client(_principal(WORKSPACE_A, Role.WORKSPACE_ADMIN))
|
||||
before = repository.jobs[JOB_B1]
|
||||
|
||||
response = client.post(f"/admin/jobs/{JOB_B1}/retry", follow_redirects=False)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert repository.requeued == []
|
||||
after = repository.jobs[JOB_B1]
|
||||
assert after.status is JobStatus.TERMINAL
|
||||
assert after.status == before.status
|
||||
assert after.attempt_count == before.attempt_count
|
||||
assert after.available_at == before.available_at
|
||||
# And if the mutation were ever attempted, it would have carried A's scope.
|
||||
assert ("requeue", None) not in repository.scopes
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
def test_own_job_retry_still_works() -> None:
|
||||
client, repository = _client(_principal(WORKSPACE_A, Role.WORKSPACE_ADMIN))
|
||||
|
||||
response = client.post(f"/admin/jobs/{JOB_A1}/retry", follow_redirects=False)
|
||||
|
||||
assert response.status_code == 303
|
||||
assert repository.requeued == [JOB_A1]
|
||||
assert ("requeue", WORKSPACE_A) in repository.scopes
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
def test_auditor_reads_own_workspace_but_cannot_retry_anything() -> None:
|
||||
client, repository = _client(_principal(WORKSPACE_A, Role.AUDITOR))
|
||||
|
||||
listing = client.get("/admin/jobs")
|
||||
assert listing.status_code == 200
|
||||
assert "alpha-one.md" in listing.text
|
||||
assert "bravo-secret.md" not in listing.text
|
||||
|
||||
# Read-only: refused for its own workspace's job as much as for a foreign
|
||||
# one, and the refusal is the permission gate, not the ownership gate.
|
||||
assert client.post(f"/admin/jobs/{JOB_A1}/retry").status_code == 403
|
||||
assert client.post(f"/admin/jobs/{JOB_B1}/retry").status_code == 403
|
||||
assert repository.requeued == []
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
def test_viewer_and_editor_reach_no_job_screen_at_all() -> None:
|
||||
for role in (Role.VIEWER, Role.EDITOR, Role.SPACE_MANAGER):
|
||||
client, repository = _client(_principal(WORKSPACE_A, role))
|
||||
assert client.get("/admin/jobs").status_code == 403
|
||||
assert client.get(f"/admin/jobs/{JOB_A1}").status_code == 403
|
||||
assert client.post(f"/admin/jobs/{JOB_A1}/retry").status_code == 403
|
||||
assert repository.requeued == []
|
||||
|
||||
|
||||
# -- legacy / unowned jobs -------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
def test_legacy_job_without_ownership_fails_closed_for_workspace_roles() -> None:
|
||||
client, repository = _client(_principal(WORKSPACE_A, Role.WORKSPACE_ADMIN))
|
||||
|
||||
listing = client.get("/admin/jobs")
|
||||
assert "legacy-unowned.md" not in listing.text
|
||||
|
||||
assert client.get(f"/admin/jobs/{JOB_LEGACY}").status_code == 404
|
||||
assert client.post(f"/admin/jobs/{JOB_LEGACY}/retry", follow_redirects=False).status_code == 404
|
||||
assert repository.requeued == []
|
||||
assert repository.jobs[JOB_LEGACY].status is JobStatus.TERMINAL
|
||||
|
||||
|
||||
# -- platform admin --------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
def test_platform_admin_keeps_global_operations() -> None:
|
||||
client, repository = _client(_principal(WORKSPACE_A, Role.PLATFORM_ADMIN))
|
||||
|
||||
listing = client.get("/admin/jobs")
|
||||
assert listing.status_code == 200
|
||||
assert "alpha-one.md" in listing.text
|
||||
assert "bravo-secret.md" in listing.text
|
||||
assert "legacy-unowned.md" in listing.text
|
||||
|
||||
assert client.get(f"/admin/jobs/{JOB_B1}").status_code == 200
|
||||
assert client.get(f"/admin/jobs/{JOB_LEGACY}").status_code == 200
|
||||
|
||||
retried = client.post(f"/admin/jobs/{JOB_B1}/retry", follow_redirects=False)
|
||||
assert retried.status_code == 303
|
||||
assert repository.requeued == [JOB_B1]
|
||||
assert _workspace_scopes(repository) == {None}
|
||||
Reference in new issue
Block a user