Publish curated Unraid-AppOps-Gateway source from ad0a390e431166c3198a2ac3bec010c2b44f2dea
This commit is contained in:
1 parent
29c1e3d5a1
commit
98d31477f9
35 files changed
+7264
-185
No files matched your search
@@ -2,20 +2,37 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import insert as sql_insert
|
||||
from sqlalchemy import update as sql_update
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlmodel import Session
|
||||
|
||||
from ..core.security import require_operator_request
|
||||
from ..core.time import utc_now
|
||||
from ..db.models import AppSetting
|
||||
from ..db.session import get_session
|
||||
from ..schemas.enrollment import EnrollmentPlanV1, EnrollmentTrustConfigV1
|
||||
from ..services.enrollment_plane import EnrollmentError, EnrollmentPlane
|
||||
from ..schemas.enrollment import (
|
||||
LEGACY_APPDATA_ROOT,
|
||||
EnrollmentPlanV1,
|
||||
EnrollmentStorageRootSelectionV1,
|
||||
EnrollmentTrustConfigV1,
|
||||
)
|
||||
from ..services.audit_logger import log_event
|
||||
from ..services.enrollment_plane import (
|
||||
TRUST_SETTING_KEY,
|
||||
EnrollmentError,
|
||||
EnrollmentPlane,
|
||||
)
|
||||
from ..services.repopulse_adoption_journal import (
|
||||
RepoPulseAdoptionJournalError,
|
||||
get_repopulse_adoption_status,
|
||||
)
|
||||
from ..services.user_access import current_user
|
||||
|
||||
|
||||
def _require_operator(request: Request) -> str:
|
||||
@@ -58,10 +75,156 @@ def enrollment_trust_status(session: EnrollmentSession):
|
||||
|
||||
|
||||
@router.put("/trust")
|
||||
def update_enrollment_trust(payload: EnrollmentTrustConfigV1, session: EnrollmentSession):
|
||||
def update_enrollment_trust(
|
||||
payload: EnrollmentTrustConfigV1,
|
||||
session: EnrollmentSession,
|
||||
request: Request,
|
||||
):
|
||||
actor = current_user(request)
|
||||
if actor.role != "admin":
|
||||
row = session.get(AppSetting, TRUST_SETTING_KEY)
|
||||
original_value = deepcopy(row.value_json) if row is not None else None
|
||||
original_updated_at = row.updated_at if row is not None else None
|
||||
if row is None or row.is_secret:
|
||||
existing = None
|
||||
else:
|
||||
try:
|
||||
existing = EnrollmentTrustConfigV1.model_validate(original_value)
|
||||
except ValueError:
|
||||
existing = None
|
||||
if row is None:
|
||||
# Preserve initial legacy setup for settings.manage users, but the
|
||||
# fixed managed-root opt-in must be made by an administrator.
|
||||
selector_changed = (
|
||||
payload.trusted_appdata_root != LEGACY_APPDATA_ROOT
|
||||
or payload.managed_appdata_backup_acknowledged is not False
|
||||
)
|
||||
elif existing is None:
|
||||
# A corrupt stored trust document has no safe selector baseline.
|
||||
selector_changed = True
|
||||
else:
|
||||
selector_changed = (
|
||||
payload.trusted_appdata_root != existing.trusted_appdata_root
|
||||
or payload.managed_appdata_backup_acknowledged
|
||||
!= existing.managed_appdata_backup_acknowledged
|
||||
)
|
||||
if selector_changed:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Administrator role is required to change the appdata root.",
|
||||
)
|
||||
try:
|
||||
if row is None:
|
||||
# A concurrent administrator may create the trust row after the
|
||||
# absence check. The unique key makes this insert a safe CAS:
|
||||
# losing the race is a conflict, never an overwrite.
|
||||
session.execute(
|
||||
sql_insert(AppSetting).values(
|
||||
key=TRUST_SETTING_KEY,
|
||||
value_json=payload.model_dump(mode="json"),
|
||||
is_secret=False,
|
||||
updated_at=utc_now(),
|
||||
)
|
||||
)
|
||||
else:
|
||||
result = session.execute(
|
||||
sql_update(AppSetting)
|
||||
.where(AppSetting.key == TRUST_SETTING_KEY)
|
||||
.where(AppSetting.updated_at == original_updated_at)
|
||||
.where(AppSetting.value_json == original_value)
|
||||
.values(
|
||||
value_json=payload.model_dump(mode="json"),
|
||||
is_secret=False,
|
||||
updated_at=utc_now(),
|
||||
)
|
||||
.execution_options(synchronize_session=False)
|
||||
)
|
||||
if result.rowcount != 1:
|
||||
session.rollback()
|
||||
raise HTTPException(status_code=409, detail="enrollment_trust_changed")
|
||||
session.commit()
|
||||
except HTTPException:
|
||||
raise
|
||||
except SQLAlchemyError:
|
||||
session.rollback()
|
||||
raise HTTPException(status_code=409, detail="enrollment_trust_changed") from None
|
||||
session.expire_all()
|
||||
return EnrollmentPlane.trust_status(session)
|
||||
return EnrollmentPlane.set_trust_config(session, payload)
|
||||
|
||||
|
||||
@router.patch("/trust/storage-root")
|
||||
def update_enrollment_storage_root(
|
||||
payload: EnrollmentStorageRootSelectionV1,
|
||||
session: EnrollmentSession,
|
||||
request: Request,
|
||||
):
|
||||
"""Change only the fixed storage selector and persist its backup acknowledgement."""
|
||||
if current_user(request).role != "admin":
|
||||
raise HTTPException(status_code=403, detail="Administrator role is required.")
|
||||
trust = EnrollmentPlane.get_trust_config(session)
|
||||
row = session.get(AppSetting, TRUST_SETTING_KEY)
|
||||
if trust is None or row is None:
|
||||
raise HTTPException(status_code=409, detail="enrollment_trust_not_configured")
|
||||
|
||||
original_value = deepcopy(row.value_json)
|
||||
original_updated_at = row.updated_at
|
||||
try:
|
||||
stored_trust = EnrollmentTrustConfigV1.model_validate(original_value)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=409, detail="enrollment_trust_changed") from None
|
||||
if stored_trust.model_dump(mode="json") != trust.model_dump(mode="json"):
|
||||
raise HTTPException(status_code=409, detail="enrollment_trust_changed")
|
||||
updated = EnrollmentTrustConfigV1.model_validate({
|
||||
**original_value,
|
||||
**payload.model_dump(mode="json"),
|
||||
})
|
||||
before = {
|
||||
"trustedAppdataRoot": trust.trusted_appdata_root,
|
||||
"managedAppdataBackupAcknowledged": trust.managed_appdata_backup_acknowledged,
|
||||
}
|
||||
after = {
|
||||
"trustedAppdataRoot": updated.trusted_appdata_root,
|
||||
"managedAppdataBackupAcknowledged": updated.managed_appdata_backup_acknowledged,
|
||||
}
|
||||
try:
|
||||
result = session.execute(
|
||||
sql_update(AppSetting)
|
||||
.where(AppSetting.key == TRUST_SETTING_KEY)
|
||||
.where(AppSetting.updated_at == original_updated_at)
|
||||
.where(AppSetting.value_json == original_value)
|
||||
.values(
|
||||
value_json=updated.model_dump(mode="json"),
|
||||
is_secret=False,
|
||||
updated_at=utc_now(),
|
||||
)
|
||||
.execution_options(synchronize_session=False)
|
||||
)
|
||||
except SQLAlchemyError:
|
||||
session.rollback()
|
||||
raise HTTPException(status_code=409, detail="enrollment_trust_changed") from None
|
||||
if result.rowcount != 1:
|
||||
session.rollback()
|
||||
raise HTTPException(status_code=409, detail="enrollment_trust_changed")
|
||||
session.expire(row)
|
||||
log_event(
|
||||
session,
|
||||
source="settings",
|
||||
identity="manual",
|
||||
action_id="enrollment.trust.storage_root.update",
|
||||
target="tower/deployment-controller",
|
||||
risk="high",
|
||||
policy_decision="allowed",
|
||||
reason="Authenticated settings.manage operator selected a fixed appdata root and recorded its backup acknowledgement.",
|
||||
result="updated",
|
||||
target_type="controller",
|
||||
before_state=before,
|
||||
after_state=after,
|
||||
redacted_metadata={"backupLocation": "dockerRootDir" if updated.managed_appdata_backup_acknowledged else None},
|
||||
)
|
||||
return EnrollmentPlane.trust_status(session)
|
||||
|
||||
|
||||
@router.post("")
|
||||
def request_enrollment(
|
||||
payload: EnrollmentRequestV1,
|
||||
|
||||
Reference in new issue
Block a user