feat(storage): add readonly during storage migration

This commit is contained in:
Edouard Vanbelle
2026-08-01 13:27:49 +02:00
parent 6b7bb67500
commit 2de71b6d9a
10 changed files with 400 additions and 25 deletions
@@ -253,6 +253,18 @@ pub struct PgAclEngine {
/// Total parent-resolution queries actually issued (point + batches) —
/// exposed via [`Self::parent_query_count`] for benches/operators.
parent_queries: Arc<AtomicU64>,
/// Global "server is in migration read-only mode" flag. When
/// `true`, `check_inner` short-circuits every write-adjacent
/// permission (`Create`/`Update`/`Delete`/`Share`/`Comment`/`Manage`)
/// with a `Denied` decision — same reason as the per-drive
/// `read_only` gate below, but scoped to the whole process rather
/// than a specific drive. Backed by
/// `admin_settings.storage.migration_readonly` so it survives
/// restart (see `docs/plan/storage-multi-entry.md` §"Read-only
/// mode"). Shared as `Arc<AtomicBool>` with `AppState` so the
/// cutover state machine (slice 5) can flip it without needing
/// to reach into the engine.
migration_readonly: Arc<std::sync::atomic::AtomicBool>,
}
/// One parked parent-resolution request: file id + reply slot. A dropped
@@ -297,12 +309,14 @@ impl PgAclEngine {
folder_repo: Arc<FolderDbRepository>,
file_repo: Arc<FileBlobReadRepository>,
group_repo: Arc<SubjectGroupPgRepository>,
migration_readonly: Arc<std::sync::atomic::AtomicBool>,
) -> Self {
Self {
pool,
folder_repo,
file_repo,
group_repo: Some(group_repo),
migration_readonly,
user_groups_cache: Cache::builder()
.max_capacity(50_000)
.time_to_live(Duration::from_secs(30))
@@ -424,6 +438,7 @@ impl PgAclEngine {
.build(),
parent_batch: Arc::new(std::sync::Mutex::new(None)),
parent_queries: Arc::new(AtomicU64::new(0)),
migration_readonly: Arc::new(std::sync::atomic::AtomicBool::new(false)),
}
}
@@ -1461,6 +1476,38 @@ impl PgAclEngine {
resource: Resource,
counters: &QueryCounters,
) -> Result<bool, DomainError> {
// Global migration-readonly short-circuit. Applies to every
// resource type — no drive lookup, no per-resource state. When
// the server is in migration read-only mode, every mutating
// permission is refused with an audit line naming the specific
// `migration_readonly` reason so operators filtering the audit
// stream can distinguish it from per-drive freezes. Reads pass
// (browsers, downloads, PROPFIND all keep working — same as the
// per-drive gate). Admin operations don't reach `check_inner`
// — they go through `admin_guard` middleware which bypasses
// authz entirely, so the admin can still exit the mode, cancel
// the migration, restart the server, etc.
//
// See `docs/plan/storage-multi-entry.md` §"Read-only mode".
if Self::read_only_gate_applies(permission)
&& self
.migration_readonly
.load(std::sync::atomic::Ordering::Relaxed)
{
tracing::info!(
target: "audit",
event = "authz.denied",
reason = "migration_readonly",
subject_type = subject.type_str(),
subject_id = %subject.id(),
permission = permission.as_str(),
resource_type = resource.type_str(),
resource_id = %resource.id(),
"🚧 mutation refused: server is in storage-migration read-only mode",
);
return Ok(false);
}
// Drive-membership precheck for File/Folder. A role on the resource's
// drive is the baseline floor (`drive.md §5`): the caller passes any
// permission check the role bundle covers. Replaces the legacy