feat(v2): initialize allowlisted repositories

This commit is contained in:
2026-07-27 20:19:51 +02:00
parent e529ad60fa
commit df62536773
2 changed files with 137 additions and 1 deletions
+77 -1
View File
@@ -16,7 +16,8 @@ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from backup_tool.cli import build_alembic_config
from backup_tool.config import Settings
from backup_tool.db.engine import SchemaNotCurrentError, assert_schema_current, create_engine
from backup_tool.db.models import ApiToken, AuditEvent, IdempotencyRecord, Secret, User
from backup_tool.db.models import ApiToken, AuditEvent, IdempotencyRecord, Repository, Secret, User
from backup_tool.repository import RepositoryError, initialize
from backup_tool.security.auth import (
hash_password,
hash_token,
@@ -76,6 +77,18 @@ class UserPatch(BaseModel):
state: str
class RepositoryInput(BaseModel):
name: str = Field(min_length=1, max_length=255)
relative_path: str = Field(min_length=1, max_length=1024)
compression: str = "none"
encryption: str = "none"
class RepositoryPatch(BaseModel):
compression: str | None = None
encryption: str | None = None
def _etag(user: User) -> str:
return f'"{user.id}:{user.updated_at.isoformat()}"'
@@ -442,6 +455,69 @@ def create_app(settings: Settings) -> FastAPI:
response.headers["ETag"] = _etag(user)
return {"id": user.id, "username": user.username, "state": user.state}
@app.post("/api/v2/repositories", status_code=201)
async def create_repository(
input_: RepositoryInput,
request: Request,
db: Annotated[AsyncSession, Depends(session)],
identity: Annotated[tuple[User, set[str], bool], Depends(require)],
) -> dict[str, Any]:
user, scopes, _ = identity
if "*" not in scopes and "admin:write" not in scopes:
raise Problem(403, "insufficient_scope", "Required scope is missing.")
try:
initialized = initialize(
settings, input_.relative_path, input_.compression, input_.encryption
)
except RepositoryError as error:
raise Problem(409, "repository_invalid", str(error)) from error
repository = Repository(
name=input_.name,
root=str(initialized.root),
format_version=initialized.format_version,
compression=initialized.compression,
encryption=initialized.encryption,
)
db.add(repository)
await db.flush()
await audit(db, request, "create", "repository", repository.id, "success", user.id)
await db.commit()
return {
"id": repository.id,
"name": repository.name,
"format_version": repository.format_version,
"compression": repository.compression,
"encryption": repository.encryption,
}
@app.get("/api/v2/repositories/{repository_id}")
async def get_repository(
repository_id: str,
db: Annotated[AsyncSession, Depends(session)],
_: Annotated[tuple[User, set[str], bool], Depends(actor)],
) -> dict[str, Any]:
repository = await db.get(Repository, repository_id)
if repository is None:
raise Problem(404, "resource_not_found", "Repository was not found.")
return {
"id": repository.id,
"name": repository.name,
"format_version": repository.format_version,
"compression": repository.compression,
"encryption": repository.encryption,
"state": repository.state,
}
@app.patch("/api/v2/repositories/{repository_id}")
async def patch_repository(
repository_id: str,
input_: RepositoryPatch,
_: Annotated[tuple[User, set[str], bool], Depends(require)],
) -> None:
if input_.compression is not None or input_.encryption is not None:
raise Problem(409, "repository_policy_immutable", "Repository policy is immutable.")
raise Problem(422, "validation_failed", "No mutable fields supplied.")
@app.get("/api/v2/audit")
async def list_audit(
db: Annotated[AsyncSession, Depends(session)],