Files
windmill/backend/tests/job_s3_viewer_data_access.rs
T
b7b3306195 feat: read data as the job's identity, viewer is opt-in per runnable (#11744)
* feat: connect a data table as the job's own identity unless the runnable opts into its viewer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: key cached results by the data table mode and keep the flag across ai edits

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: hash the cache key's mode and viewer as fixed fields so no argument name can forge them

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: update ee-repo-ref to 2f74548ad1ec50c8e1faf17e42d5648160e833f1

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

Previous ee-repo-ref: 4ad768a777fff9f1ecc86a2803bc27d80f8242d6

New ee-repo-ref: 2f74548ad1ec50c8e1faf17e42d5648160e833f1

Automated by sync-ee-ref workflow.

* feat: rename the flag to viewer_data_access and apply it to object storage permission rules

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: describe viewer_data_access as covering object storage rules too

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: update ee-repo-ref to 0ffb633473c5465513576afc6f54d18f93c7f1e1

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

Previous ee-repo-ref: e9e06a9eb94168f46b378276b01fa0117dc7d8ed

New ee-repo-ref: 0ffb633473c5465513576afc6f54d18f93c7f1e1

Automated by sync-ee-ref workflow.

* feat: show the viewer data access control as an icon button

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-10-14 00:05:52 +02:00

113 lines
3.9 KiB
Rust

//! A job whose `job_perms.viewer_data_access` is set reads workspace object storage as its viewer:
//! the advanced permission rules are checked against the viewer, not the job's own identity, and
//! such a job with no viewer is refused rather than checked as itself.
//!
//! Advanced S3 permissions are an enterprise feature.
#![cfg(all(feature = "enterprise", feature = "private", feature = "parquet"))]
use serde_json::json;
use sqlx::{Pool, Postgres};
use uuid::Uuid;
use windmill_test_utils::*;
/// A job of `test-user-3`, run for `viewer`, reading data as that viewer or not.
async fn job_token(
db: &Pool<Postgres>,
viewer: Option<&str>,
viewer_data_access: bool,
) -> anyhow::Result<String> {
let id = Uuid::new_v4();
sqlx::query(
"INSERT INTO v2_job (id, workspace_id, created_by, permissioned_as, permissioned_as_email,
kind, script_lang, runnable_path, tag)
VALUES ($1, 'test-workspace', 'test-user-3', 'u/test-user-3', 'test3@windmill.dev',
'script', 'deno', 'u/test-user-3/app_runnable', 'deno')",
)
.bind(id)
.execute(db)
.await?;
sqlx::query(
"INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups,
workspace_id, end_user_email, viewer_data_access)
VALUES ($1, 'test3@windmill.dev', 'test-user-3', false, false, '{}', '{}',
'test-workspace', $2, $3)",
)
.bind(id)
.bind(viewer)
.bind(viewer_data_access)
.execute(db)
.await?;
Ok(windmill_common::auth::create_token_for_owner(
db,
"test-workspace",
"u/test-user-3",
"ephemeral-script",
300,
"test3@windmill.dev",
&id,
None,
None,
)
.await?)
}
#[sqlx::test(fixtures("base"))]
async fn a_job_reading_data_as_its_viewer_gets_the_viewers_storage_paths(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
initialize_tracing().await;
set_jwt_secret().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let storage_dir = tempfile::tempdir()?;
for user in ["test-user-2", "test-user-3"] {
let dir = storage_dir.path().join("u").join(user);
std::fs::create_dir_all(&dir)?;
std::fs::write(dir.join("file.txt"), user)?;
}
sqlx::query("UPDATE workspace_settings SET large_file_storage = $1 WHERE workspace_id = $2")
.bind(json!({
"type": "FilesystemStorage",
"root_path": storage_dir.path().to_string_lossy(),
"public_resource": null,
"advanced_permissions": [{ "pattern": "u/{username}/**", "allow": "read" }]
}))
.bind("test-workspace")
.execute(&db)
.await?;
let read = |token: String, user: &'static str| async move {
let resp = reqwest::Client::new()
.get(format!(
"http://localhost:{port}/api/w/test-workspace/job_helpers/download_s3_file"
))
.query(&[("file_key", format!("u/{user}/file.txt"))])
.bearer_auth(token)
.send()
.await?;
Ok::<_, anyhow::Error>(resp.status().is_success())
};
let own = job_token(&db, Some("test2@windmill.dev"), false).await?;
assert!(read(own.clone(), "test-user-3").await?);
assert!(
!read(own, "test-user-2").await?,
"a job not reading as its viewer reached the viewer's paths"
);
let as_viewer = job_token(&db, Some("test2@windmill.dev"), true).await?;
assert!(read(as_viewer.clone(), "test-user-2").await?);
assert!(
!read(as_viewer, "test-user-3").await?,
"a job reading as its viewer kept its own identity's paths"
);
let no_viewer = job_token(&db, None, true).await?;
assert!(
!read(no_viewer, "test-user-3").await?,
"a job reading as a viewer it does not have was checked as itself"
);
Ok(())
}