fix: restrict filesystem workspace storage to debug builds (#10864)

* fix: restrict filesystem workspace storage to debug builds

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7p2VbtYqaXHGaAskgwVk5

* chore: update ee-repo-ref to b58ad414b098d3d7787001a352bfbb13e43a335f

This commit updates the EE repository reference after PR #747 was merged in windmill-ee-private.

Previous ee-repo-ref: 1b4dada77a8fe2224579c643550c63b1ac2616de

New ee-repo-ref: b58ad414b098d3d7787001a352bfbb13e43a335f

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
This commit is contained in:
Ruben Fiszel
2026-08-26 23:35:44 +02:00
committed by GitHub
co-authored by Claude Opus 5 windmill-internal-app[bot]
parent f131c3920f
commit 8b80b09f33
7 changed files with 87 additions and 25 deletions
+5 -3
View File
@@ -131,9 +131,11 @@ minimal explicit set for dev.
## Workspace object storage in dev — use the local filesystem
For a dev workspace you don't need MinIO/S3: use the built-in **`FilesystemStorage`** large-file
storage (a root path on local disk). It is intentionally hidden from the settings-UI storage
dropdown (dev-only), so set it via the API. Requires the backend built with `parquet` (+ `private`
for the real S3 helpers, + `enterprise` if you want advanced permission rules enforced):
storage (a root path on local disk). It is a **debug-build affordance only** — every site that
builds a filesystem object store calls `ensure_filesystem_storage_allowed`, so release builds
refuse it, and the settings UI never offers it — so set it via the API on a `cargo run`/`cargo
test` binary. Requires the backend built with `parquet` (+ `private` for the real S3 helpers,
+ `enterprise` if you want advanced permission rules enforced):
```bash
curl -X POST "$BASE/api/w/<ws>/workspaces/edit_large_file_storage_config" \
+1 -1
View File
@@ -1 +1 @@
212cc7d61ec38580d4a70d9ac38d7a2cc9daf409
b58ad414b098d3d7787001a352bfbb13e43a335f
@@ -1970,6 +1970,23 @@ async fn edit_large_file_storage_config(
)));
}
if !windmill_common::workspaces::filesystem_storage_allowed() {
let named = std::iter::once(("primary storage", &lfs_config.large_file_storage)).chain(
lfs_config
.secondary_storage
.iter()
.map(|(name, storage)| (name.as_str(), storage)),
);
for (name, storage) in named {
if matches!(storage, LargeFileStorage::FilesystemStorage(_)) {
return Err(Error::BadRequest(format!(
"{name}: {}",
windmill_common::workspaces::FILESYSTEM_STORAGE_DEV_ONLY_MSG
)));
}
}
}
let serialized_lfs_config =
serde_json::to_value::<LargeFileStorageWithSecondary>(lfs_config)
.map_err(|err| Error::internal_err(err.to_string()))?;
+26
View File
@@ -2193,6 +2193,32 @@ pub fn lfs_entry_storage_ref(entry: &serde_json::Value) -> Option<String> {
Some(format!("{typ}:{path}"))
}
pub const FILESYSTEM_STORAGE_DEV_ONLY_MSG: &str =
"Filesystem storage is only available in development builds of Windmill: it points the \
workspace at a directory on the server's own disk rather than at a resource. Use an S3, \
Azure Blob or Google Cloud Storage backend instead.";
/// A filesystem workspace storage names a directory on the server's own disk, so it hands whoever
/// configures it — a workspace admin, or any member who can write a `filesystem` resource —
/// whatever the server process can reach, and it only resolves when server and workers share that
/// disk. It is there so local development can skip MinIO, hence debug builds only. Instance object
/// storage on local disk is a separate, superadmin-only setting and stays allowed everywhere.
pub fn filesystem_storage_allowed() -> bool {
cfg!(debug_assertions)
}
/// Guards every site that builds an `ObjectStoreResource::Filesystem`, so nothing downstream can
/// reach a local-disk store: a stored config outlives the build that accepted it, and the resource
/// route never passes through the workspace-storage settings at all.
pub fn ensure_filesystem_storage_allowed() -> Result<()> {
if !filesystem_storage_allowed() {
return Err(Error::BadRequest(
FILESYSTEM_STORAGE_DEV_ONLY_MSG.to_string(),
));
}
Ok(())
}
/// Resolve a `$res:`/`$var:` reference tree to its concrete value (recursively, secrets
/// decrypted). No permission checks — trusted server-side callers only; never echo the result
/// to a user.
+1
View File
@@ -1171,6 +1171,7 @@ pub fn lfs_to_object_store_resource(
Ok(ObjectStoreResource::Gcs(gcs_resource))
}
LargeFileStorage::FilesystemStorage(fs) => {
windmill_common::workspaces::ensure_filesystem_storage_allowed()?;
Ok(ObjectStoreResource::Filesystem(FilesystemSettings {
root_path: fs.root_path.clone(),
}))
+1
View File
@@ -1640,6 +1640,7 @@ pub(crate) async fn get_workspace_s3_resource_path(
)
}
Some(LargeFileStorage::FilesystemStorage(fs)) => {
windmill_common::workspaces::ensure_filesystem_storage_allowed()?;
return Ok(Some(
windmill_object_store::ObjectStoreResource::Filesystem(
windmill_object_store::FilesystemSettings { root_path: fs.root_path.clone() },
@@ -44,6 +44,14 @@
onDiscard?: () => void
} = $props()
const creatableStorageTypes = [
{ value: 's3', label: 'S3' },
{ value: 'azure_blob', label: 'Azure Blob' },
{ value: 's3_aws_oidc', label: 'AWS OIDC' },
{ value: 'azure_workload_identity', label: 'Azure Workload Identity' },
{ value: 'gcloud_storage', label: 'Google Cloud Storage' }
]
let advancedPermissionModalState:
| { open: false }
| { open: true; storage: S3ResourceSettingsItem } = $state({ open: false })
@@ -294,31 +302,38 @@
<Cell>
<div class="flex gap-2">
<div class="relative">
{#if tableRow[1].resourceType === 'filesystem'}
<!-- Filesystem storage is deliberately absent from the creatable
types below: it is dev-only (set via the API), so the UI only
renders it read-only when already configured. -->
<div class="flex items-center gap-1">
<!-- `filesystem` is offered only to a row that already is one, so it can be
converted away but never chosen: the backend accepts it in development
builds alone. -->
<Select
items={[{ value: 'filesystem', label: 'Filesystem' }]}
value={'filesystem'}
disabled
items={tableRow[1].resourceType === 'filesystem'
? [{ value: 'filesystem', label: 'Filesystem' }, ...creatableStorageTypes]
: creatableStorageTypes}
bind:value={
() => tableRow[1].resourceType,
(resourceType) => {
if (
tableRow[1].resourceType === 'filesystem' &&
resourceType !== 'filesystem'
) {
// A filesystem row holds a server path, not a resource path.
tableRow[1].resourcePath = undefined
}
tableRow[1].resourceType = resourceType
}
}
id="storage-resource-type-select"
class="w-40"
/>
{:else}
<Select
items={[
{ value: 's3', label: 'S3' },
{ value: 'azure_blob', label: 'Azure Blob' },
{ value: 's3_aws_oidc', label: 'AWS OIDC' },
{ value: 'azure_workload_identity', label: 'Azure Workload Identity' },
{ value: 'gcloud_storage', label: 'Google Cloud Storage' }
]}
bind:value={tableRow[1].resourceType}
id="storage-resource-type-select"
class="w-40"
/>
{/if}
{#if tableRow[1].resourceType === 'filesystem'}
<Tooltip>
Filesystem storage points the workspace at a directory on the server's own
disk. Only development builds of Windmill accept it switch this storage to
S3, Azure Blob or Google Cloud Storage to configure it here.
</Tooltip>
{/if}
</div>
</div>
<div class="flex flex-1">
{#if tableRow[1].resourceType === 'filesystem'}