From de340eed6f5520414b59166f8dc16938f7ac6c36 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Fri, 16 Oct 2026 21:03:57 +0200 Subject: [PATCH] feat: route python S3 client settings through the S3 proxy (#11819) * feat: route the python SDK's S3 client settings through the S3 proxy Co-Authored-By: Claude Opus 5.5 * fix: drop the boto3 config key and expose the proxied bucket to polars Co-Authored-By: Claude Opus 5.5 * chore: update ee-repo-ref Co-Authored-By: Claude Opus 5.5 * fix: proxy S3 settings only for server-signed tokens and keep tls verify Co-Authored-By: Claude Opus 5.5 * chore: update ee-repo-ref Co-Authored-By: Claude Opus 5.5 * chore: update ee-repo-ref to cd3395013d8484cd3917cdb6ee7837b6ac91e577 This commit updates the EE repository reference after PR #889 was merged in windmill-ee-private. Previous ee-repo-ref: e90873bba9dde1e899741d111137f94dde2e5ff8 New ee-repo-ref: cd3395013d8484cd3917cdb6ee7837b6ac91e577 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: windmill-internal-app[bot] --- backend/ee-repo-ref.txt | 2 +- .../windmill-api-workspaces/src/workspaces.rs | 9 ++ backend/windmill-api/openapi.yaml | 25 ++++ backend/windmill-types/src/s3.rs | 6 + cli/src/guidance/skills.gen.ts | 22 +++- .../wmill/tests/test_s3_proxy_settings.py | 68 ++++++++++ python-client/wmill/wmill/client.py | 124 +++++++++++++++++- python-client/wmill/wmill/s3_types.py | 2 + system_prompts/auto-generated/prompts.ts | 22 +++- system_prompts/auto-generated/script.md | 22 +++- system_prompts/auto-generated/sdks/python.md | 22 +++- .../skills/write-script-python3/SKILL.md | 22 +++- 12 files changed, 327 insertions(+), 19 deletions(-) create mode 100644 python-client/wmill/tests/test_s3_proxy_settings.py diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 029c272236..6f07da341c 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -c8e450396ff4210c3bc253b1fa83b913c1a24c45 +cd3395013d8484cd3917cdb6ee7837b6ac91e577 diff --git a/backend/windmill-api-workspaces/src/workspaces.rs b/backend/windmill-api-workspaces/src/workspaces.rs index 002b11ed2b..d76f5c9cc2 100644 --- a/backend/windmill-api-workspaces/src/workspaces.rs +++ b/backend/windmill-api-workspaces/src/workspaces.rs @@ -2168,6 +2168,15 @@ async fn edit_large_file_storage_config( windmill_types::s3::DEFAULT_STORAGE ))); } + if lfs_config + .secondary_storage + .contains_key(windmill_types::s3::BUCKET_ADDRESSED_SEGMENT) + { + return Err(Error::BadRequest(format!( + "`{}` is reserved by the S3 proxy and cannot name a secondary storage", + windmill_types::s3::BUCKET_ADDRESSED_SEGMENT + ))); + } if !windmill_common::workspaces::filesystem_storage_allowed() { let named = std::iter::once(("primary storage", &lfs_config.large_file_storage)).chain( diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 1fb5cb400e..81a4e3274a 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -25248,6 +25248,31 @@ paths: schema: $ref: "#/components/schemas/S3Resource" + /w/{workspace}/job_helpers/v2/s3_proxy_bucket: + get: + summary: Returns the bucket S3 clients name to reach a workspace storage + through the S3 proxy, or null when that storage is not S3 + operationId: s3ProxyBucket + tags: + - helpers + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: storage + in: query + schema: + type: string + responses: + "200": + description: The storage's bucket + content: + application/json: + schema: + type: object + properties: + bucket: + type: string + nullable: true + /w/{workspace}/job_helpers/test_connection: get: summary: Test connection to the workspace object storage diff --git a/backend/windmill-types/src/s3.rs b/backend/windmill-types/src/s3.rs index 3af4b7f99a..e6a219a370 100644 --- a/backend/windmill-types/src/s3.rs +++ b/backend/windmill-types/src/s3.rs @@ -9,6 +9,12 @@ use std::fmt; /// it as the primary one. pub const DEFAULT_STORAGE: &str = "_default_"; +/// The s3-proxy path segment under which a request names the storage's real bucket +/// (`s3_proxy/_wm_bucket_/{storage}/{bucket}/{key}`), for S3 clients that address +/// `s3:///`. Reserved like `DEFAULT_STORAGE`: a secondary storage of that name +/// would be shadowed by the bucket-addressed routes. +pub const BUCKET_ADDRESSED_SEGMENT: &str = "_wm_bucket_"; + #[derive(Debug, Deserialize, Serialize, Clone)] pub struct FilesystemSettings { pub root_path: String, diff --git a/cli/src/guidance/skills.gen.ts b/cli/src/guidance/skills.gen.ts index bcef0d5d3e..d1cf9672a8 100644 --- a/cli/src/guidance/skills.gen.ts +++ b/cli/src/guidance/skills.gen.ts @@ -4731,15 +4731,31 @@ def get_flow_user_state(key: str) -> Any def version() # Convenient helpers that takes an S3 resource as input and returns the settings necessary to -# initiate an S3 connection from DuckDB +# initiate an S3 connection from DuckDB. +# +# Without a resource path, the connection goes to the workspace storage through Windmill's +# S3 proxy when that storage is S3: address it as \`\`s3:///\`\` with the +# storage's own bucket, returned as \`\`s3_bucket\`\`. Pass the resource path to connect with +# the storage's own credentials instead. def get_duckdb_connection_settings(s3_resource_path: str = '') -> DuckDbConnectionSettings | None # Convenient helpers that takes an S3 resource as input and returns the settings necessary to -# initiate an S3 connection from Polars +# initiate an S3 connection from Polars. +# +# Without a resource path, the connection goes to the workspace storage through Windmill's +# S3 proxy when that storage is S3: address it as \`\`s3:///\`\` with the +# storage's own bucket, returned as \`\`s3_bucket\`\`. Pass the resource path to connect with +# the storage's own credentials instead. def get_polars_connection_settings(s3_resource_path: str = '') -> PolarsConnectionSettings # Convenient helpers that takes an S3 resource as input and returns the settings necessary to -# initiate an S3 connection using boto3 +# initiate an S3 connection using boto3. +# +# Without a resource path, the connection goes to the workspace storage through Windmill's +# S3 proxy when that storage is S3: pass the storage's own bucket as \`\`Bucket\`\`. The proxy +# serves object reads, writes, deletes, listing and multipart uploads of that bucket only; +# presigned URLs, other buckets and bucket-level APIs need the storage's own credentials, +# which passing the resource path returns. def get_boto3_connection_settings(s3_resource_path: str = '') -> Boto3ConnectionSettings # Load a file from the workspace s3 bucket and returns its content as bytes. diff --git a/python-client/wmill/tests/test_s3_proxy_settings.py b/python-client/wmill/tests/test_s3_proxy_settings.py new file mode 100644 index 0000000000..52db4fc8a9 --- /dev/null +++ b/python-client/wmill/tests/test_s3_proxy_settings.py @@ -0,0 +1,68 @@ +"""The default S3 connection settings go through the S3 proxy only when the server can verify +the token's SigV4 signature and serves the bucket-addressed routes; otherwise they keep the +storage's own credentials.""" + +import pytest + +from wmill.client import Windmill + +JOB_TOKEN = "jwt_header.payload.signature" + + +class _Response: + def __init__(self, status_code, body): + self.status_code = status_code + self._body = body + + def json(self): + return self._body + + +def _client(token, status_code=200, body=None): + client = Windmill(base_url="https://wm.example", token=token, workspace="ws") + calls = [] + + def get(endpoint, raise_for_status=True, **kwargs): + calls.append(endpoint) + return _Response(status_code, body if body is not None else {"bucket": "data"}) + + def post(endpoint, raise_for_status=True, **kwargs): + calls.append(endpoint) + resource = {"endPoint": "s3.example", "useSSL": True, "region": "eu-west-1"} + return _Response(200, {**resource, "accessKey": "raw", "secretKey": "raw-secret"}) + + client.get = get + client.post = post + return client, calls + + +def test_job_token_goes_through_the_proxy(): + client, _ = _client(JOB_TOKEN) + settings = client.get_boto3_connection_settings() + assert settings["endpoint_url"] == "https://wm.example/api/w/ws/s3_proxy/_wm_bucket_/_default_" + assert settings["aws_access_key_id"] == "jwt_header.payload" + assert settings["aws_secret_access_key"] == "signature" + assert client.get_polars_connection_settings()["s3_bucket"] == "data" + + +@pytest.mark.parametrize( + "token,status_code,body", + [ + ("personaltokenwithoutdots", 200, None), + ("jwt_ext_header.payload.signature", 200, None), + (JOB_TOKEN, 404, None), + (JOB_TOKEN, 200, {"bucket": None}), + ], + ids=["personal token", "external jwt", "older server", "non-S3 storage"], +) +def test_falls_back_to_the_storage_credentials(token, status_code, body): + client, calls = _client(token, status_code, body) + settings = client.get_boto3_connection_settings() + assert settings["aws_access_key_id"] == "raw" + assert calls[-1].endswith("/job_helpers/v2/s3_resource_info") + + +def test_explicit_resource_path_never_asks_for_the_proxy(): + client, calls = _client(JOB_TOKEN) + client.get_boto3_connection_settings("u/admin/s3") + assert calls == ["/w/ws/job_helpers/v2/s3_resource_info"] diff --git a/python-client/wmill/wmill/client.py b/python-client/wmill/wmill/client.py index 824db9741f..f0d0134fa7 100644 --- a/python-client/wmill/wmill/client.py +++ b/python-client/wmill/wmill/client.py @@ -872,15 +872,75 @@ class Windmill: """ return self.get("version").text + def _s3_proxy_connection(self) -> dict | None: + """Where an S3 client reaches the workspace storage through Windmill's S3 proxy, so the + storage's permission rules apply and its credentials stay on the server. + + None when the workspace storage is not S3, when the token is not one the server signs + itself (the proxy re-derives the SigV4 secret from a ``jwt_`` token's header and payload, + which an external ``jwt_ext_`` token's signature does not match), or when the server + predates the proxy's bucket-addressed routes: the caller then connects with the + storage's own credentials. + """ + if ( + not self.token + or not self.token.startswith("jwt_") + or self.token.startswith(("jwt_ext_", "jwt_agent_")) + ): + return None + resp = self.get( + f"/w/{self.workspace}/job_helpers/v2/s3_proxy_bucket", + raise_for_status=False, + ) + if resp.status_code != 200: + return None + bucket = resp.json().get("bucket") + if bucket is None: + return None + access_key, secret_key = self.token.rsplit(".", 1) + return { + "endpoint_url": f"{self.base_url}/w/{self.workspace}/s3_proxy/_wm_bucket_/_default_", + "use_ssl": self.base_url.startswith("https://"), + "access_key": access_key, + "secret_key": secret_key, + # The proxy checks the signature with whatever region the client signed for. + "region": "us-east-1", + "bucket": bucket, + } + def get_duckdb_connection_settings( self, s3_resource_path: str = "", ) -> DuckDbConnectionSettings | None: """ Convenient helpers that takes an S3 resource as input and returns the settings necessary to - initiate an S3 connection from DuckDB + initiate an S3 connection from DuckDB. + + Without a resource path, the connection goes to the workspace storage through Windmill's + S3 proxy when that storage is S3: address it as ``s3:///`` with the + storage's own bucket, returned as ``s3_bucket``. Pass the resource path to connect with + the storage's own credentials instead. """ s3_resource_path = parse_resource_syntax(s3_resource_path) or s3_resource_path + proxy = self._s3_proxy_connection() if s3_resource_path == "" else None + if proxy is not None: + endpoint = proxy["endpoint_url"].split("://", 1)[1] + settings = ( + "SET home_directory='./';\n" + "INSTALL 'httpfs';\n" + "SET s3_url_style='path';\n" + f"SET s3_region='{proxy['region']}';\n" + f"SET s3_endpoint='{endpoint}';\n" + f"SET s3_access_key_id='{proxy['access_key']}';\n" + f"SET s3_secret_access_key='{proxy['secret_key']}';\n" + ) + if not proxy["use_ssl"]: + settings += "SET s3_use_ssl=0;\n" + if self.verify is False: + settings += "SET enable_server_cert_verification=false;\n" + return DuckDbConnectionSettings( + {"connection_settings_str": settings, "s3_bucket": proxy["bucket"]} + ) try: raw_obj = self.post( f"/w/{self.workspace}/job_helpers/v2/duckdb_connection_settings", @@ -888,6 +948,7 @@ class Windmill: if s3_resource_path == "" else {"s3_resource_path": s3_resource_path}, ).json() + raw_obj.setdefault("s3_bucket", None) return DuckDbConnectionSettings(raw_obj) except JSONDecodeError as e: raise Exception( @@ -900,9 +961,44 @@ class Windmill: ) -> PolarsConnectionSettings: """ Convenient helpers that takes an S3 resource as input and returns the settings necessary to - initiate an S3 connection from Polars + initiate an S3 connection from Polars. + + Without a resource path, the connection goes to the workspace storage through Windmill's + S3 proxy when that storage is S3: address it as ``s3:///`` with the + storage's own bucket, returned as ``s3_bucket``. Pass the resource path to connect with + the storage's own credentials instead. """ s3_resource_path = parse_resource_syntax(s3_resource_path) or s3_resource_path + proxy = self._s3_proxy_connection() if s3_resource_path == "" else None + if proxy is not None: + client_kwargs = {"region_name": proxy["region"]} + storage_options = { + "aws_endpoint_url": proxy["endpoint_url"], + "aws_access_key_id": proxy["access_key"], + "aws_secret_access_key": proxy["secret_key"], + "aws_region": proxy["region"], + "aws_allow_http": str(not proxy["use_ssl"]).lower(), + "aws_virtual_hosted_style_request": "false", + } + if self.verify is not True: + client_kwargs["verify"] = self.verify + if self.verify is False: + storage_options["allow_invalid_certificates"] = "true" + return PolarsConnectionSettings( + { + "s3fs_args": { + "endpoint_url": proxy["endpoint_url"], + "key": proxy["access_key"], + "secret": proxy["secret_key"], + "use_ssl": proxy["use_ssl"], + "cache_regions": False, + "client_kwargs": client_kwargs, + "config_kwargs": {"s3": {"addressing_style": "path"}}, + }, + "storage_options": storage_options, + "s3_bucket": proxy["bucket"], + } + ) try: raw_obj = self.post( f"/w/{self.workspace}/job_helpers/v2/polars_connection_settings", @@ -910,6 +1006,7 @@ class Windmill: if s3_resource_path == "" else {"s3_resource_path": s3_resource_path}, ).json() + raw_obj.setdefault("s3_bucket", None) return PolarsConnectionSettings(raw_obj) except JSONDecodeError as e: raise Exception( @@ -922,9 +1019,30 @@ class Windmill: ) -> Boto3ConnectionSettings: """ Convenient helpers that takes an S3 resource as input and returns the settings necessary to - initiate an S3 connection using boto3 + initiate an S3 connection using boto3. + + Without a resource path, the connection goes to the workspace storage through Windmill's + S3 proxy when that storage is S3: pass the storage's own bucket as ``Bucket``. The proxy + serves object reads, writes, deletes, listing and multipart uploads of that bucket only; + presigned URLs, other buckets and bucket-level APIs need the storage's own credentials, + which passing the resource path returns. """ s3_resource_path = parse_resource_syntax(s3_resource_path) or s3_resource_path + proxy = self._s3_proxy_connection() if s3_resource_path == "" else None + if proxy is not None: + # botocore addresses a custom endpoint path-style, which the proxy's routes need. + settings = Boto3ConnectionSettings( + { + "endpoint_url": proxy["endpoint_url"], + "region_name": proxy["region"], + "use_ssl": proxy["use_ssl"], + "aws_access_key_id": proxy["access_key"], + "aws_secret_access_key": proxy["secret_key"], + } + ) + if self.verify is not True: + settings["verify"] = self.verify + return settings try: s3_resource = self.post( f"/w/{self.workspace}/job_helpers/v2/s3_resource_info", diff --git a/python-client/wmill/wmill/s3_types.py b/python-client/wmill/wmill/s3_types.py index 1b43b65972..058eab68fe 100644 --- a/python-client/wmill/wmill/s3_types.py +++ b/python-client/wmill/wmill/s3_types.py @@ -57,6 +57,7 @@ class PolarsConnectionSettings(dict): """Polars S3 connection settings containing S3FS args and storage options.""" s3fs_args: S3FsArgs storage_options: StorageOptions + s3_bucket: Optional[str] def __getattr__(self, attr): return _item_as_attr(self, attr) @@ -78,6 +79,7 @@ class Boto3ConnectionSettings(dict): class DuckDbConnectionSettings(dict): """DuckDB S3 connection settings as a configuration string.""" connection_settings_str: str + s3_bucket: Optional[str] def __getattr__(self, attr): return _item_as_attr(self, attr) diff --git a/system_prompts/auto-generated/prompts.ts b/system_prompts/auto-generated/prompts.ts index 59b00c2821..417f7bbe7e 100644 --- a/system_prompts/auto-generated/prompts.ts +++ b/system_prompts/auto-generated/prompts.ts @@ -2623,15 +2623,31 @@ def get_flow_user_state(key: str) -> Any def version() # Convenient helpers that takes an S3 resource as input and returns the settings necessary to -# initiate an S3 connection from DuckDB +# initiate an S3 connection from DuckDB. +# +# Without a resource path, the connection goes to the workspace storage through Windmill's +# S3 proxy when that storage is S3: address it as \`\`s3:///\`\` with the +# storage's own bucket, returned as \`\`s3_bucket\`\`. Pass the resource path to connect with +# the storage's own credentials instead. def get_duckdb_connection_settings(s3_resource_path: str = '') -> DuckDbConnectionSettings | None # Convenient helpers that takes an S3 resource as input and returns the settings necessary to -# initiate an S3 connection from Polars +# initiate an S3 connection from Polars. +# +# Without a resource path, the connection goes to the workspace storage through Windmill's +# S3 proxy when that storage is S3: address it as \`\`s3:///\`\` with the +# storage's own bucket, returned as \`\`s3_bucket\`\`. Pass the resource path to connect with +# the storage's own credentials instead. def get_polars_connection_settings(s3_resource_path: str = '') -> PolarsConnectionSettings # Convenient helpers that takes an S3 resource as input and returns the settings necessary to -# initiate an S3 connection using boto3 +# initiate an S3 connection using boto3. +# +# Without a resource path, the connection goes to the workspace storage through Windmill's +# S3 proxy when that storage is S3: pass the storage's own bucket as \`\`Bucket\`\`. The proxy +# serves object reads, writes, deletes, listing and multipart uploads of that bucket only; +# presigned URLs, other buckets and bucket-level APIs need the storage's own credentials, +# which passing the resource path returns. def get_boto3_connection_settings(s3_resource_path: str = '') -> Boto3ConnectionSettings # Load a file from the workspace s3 bucket and returns its content as bytes. diff --git a/system_prompts/auto-generated/script.md b/system_prompts/auto-generated/script.md index 8271a528b4..096cabab93 100644 --- a/system_prompts/auto-generated/script.md +++ b/system_prompts/auto-generated/script.md @@ -2370,15 +2370,31 @@ def get_flow_user_state(key: str) -> Any def version() # Convenient helpers that takes an S3 resource as input and returns the settings necessary to -# initiate an S3 connection from DuckDB +# initiate an S3 connection from DuckDB. +# +# Without a resource path, the connection goes to the workspace storage through Windmill's +# S3 proxy when that storage is S3: address it as ``s3:///`` with the +# storage's own bucket, returned as ``s3_bucket``. Pass the resource path to connect with +# the storage's own credentials instead. def get_duckdb_connection_settings(s3_resource_path: str = '') -> DuckDbConnectionSettings | None # Convenient helpers that takes an S3 resource as input and returns the settings necessary to -# initiate an S3 connection from Polars +# initiate an S3 connection from Polars. +# +# Without a resource path, the connection goes to the workspace storage through Windmill's +# S3 proxy when that storage is S3: address it as ``s3:///`` with the +# storage's own bucket, returned as ``s3_bucket``. Pass the resource path to connect with +# the storage's own credentials instead. def get_polars_connection_settings(s3_resource_path: str = '') -> PolarsConnectionSettings # Convenient helpers that takes an S3 resource as input and returns the settings necessary to -# initiate an S3 connection using boto3 +# initiate an S3 connection using boto3. +# +# Without a resource path, the connection goes to the workspace storage through Windmill's +# S3 proxy when that storage is S3: pass the storage's own bucket as ``Bucket``. The proxy +# serves object reads, writes, deletes, listing and multipart uploads of that bucket only; +# presigned URLs, other buckets and bucket-level APIs need the storage's own credentials, +# which passing the resource path returns. def get_boto3_connection_settings(s3_resource_path: str = '') -> Boto3ConnectionSettings # Load a file from the workspace s3 bucket and returns its content as bytes. diff --git a/system_prompts/auto-generated/sdks/python.md b/system_prompts/auto-generated/sdks/python.md index 1964c88685..1a772ae6ee 100644 --- a/system_prompts/auto-generated/sdks/python.md +++ b/system_prompts/auto-generated/sdks/python.md @@ -265,15 +265,31 @@ def get_flow_user_state(key: str) -> Any def version() # Convenient helpers that takes an S3 resource as input and returns the settings necessary to -# initiate an S3 connection from DuckDB +# initiate an S3 connection from DuckDB. +# +# Without a resource path, the connection goes to the workspace storage through Windmill's +# S3 proxy when that storage is S3: address it as ``s3:///`` with the +# storage's own bucket, returned as ``s3_bucket``. Pass the resource path to connect with +# the storage's own credentials instead. def get_duckdb_connection_settings(s3_resource_path: str = '') -> DuckDbConnectionSettings | None # Convenient helpers that takes an S3 resource as input and returns the settings necessary to -# initiate an S3 connection from Polars +# initiate an S3 connection from Polars. +# +# Without a resource path, the connection goes to the workspace storage through Windmill's +# S3 proxy when that storage is S3: address it as ``s3:///`` with the +# storage's own bucket, returned as ``s3_bucket``. Pass the resource path to connect with +# the storage's own credentials instead. def get_polars_connection_settings(s3_resource_path: str = '') -> PolarsConnectionSettings # Convenient helpers that takes an S3 resource as input and returns the settings necessary to -# initiate an S3 connection using boto3 +# initiate an S3 connection using boto3. +# +# Without a resource path, the connection goes to the workspace storage through Windmill's +# S3 proxy when that storage is S3: pass the storage's own bucket as ``Bucket``. The proxy +# serves object reads, writes, deletes, listing and multipart uploads of that bucket only; +# presigned URLs, other buckets and bucket-level APIs need the storage's own credentials, +# which passing the resource path returns. def get_boto3_connection_settings(s3_resource_path: str = '') -> Boto3ConnectionSettings # Load a file from the workspace s3 bucket and returns its content as bytes. diff --git a/system_prompts/auto-generated/skills/write-script-python3/SKILL.md b/system_prompts/auto-generated/skills/write-script-python3/SKILL.md index c12c44e4f0..3afa72e5a9 100644 --- a/system_prompts/auto-generated/skills/write-script-python3/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-python3/SKILL.md @@ -476,15 +476,31 @@ def get_flow_user_state(key: str) -> Any def version() # Convenient helpers that takes an S3 resource as input and returns the settings necessary to -# initiate an S3 connection from DuckDB +# initiate an S3 connection from DuckDB. +# +# Without a resource path, the connection goes to the workspace storage through Windmill's +# S3 proxy when that storage is S3: address it as ``s3:///`` with the +# storage's own bucket, returned as ``s3_bucket``. Pass the resource path to connect with +# the storage's own credentials instead. def get_duckdb_connection_settings(s3_resource_path: str = '') -> DuckDbConnectionSettings | None # Convenient helpers that takes an S3 resource as input and returns the settings necessary to -# initiate an S3 connection from Polars +# initiate an S3 connection from Polars. +# +# Without a resource path, the connection goes to the workspace storage through Windmill's +# S3 proxy when that storage is S3: address it as ``s3:///`` with the +# storage's own bucket, returned as ``s3_bucket``. Pass the resource path to connect with +# the storage's own credentials instead. def get_polars_connection_settings(s3_resource_path: str = '') -> PolarsConnectionSettings # Convenient helpers that takes an S3 resource as input and returns the settings necessary to -# initiate an S3 connection using boto3 +# initiate an S3 connection using boto3. +# +# Without a resource path, the connection goes to the workspace storage through Windmill's +# S3 proxy when that storage is S3: pass the storage's own bucket as ``Bucket``. The proxy +# serves object reads, writes, deletes, listing and multipart uploads of that bucket only; +# presigned URLs, other buckets and bucket-level APIs need the storage's own credentials, +# which passing the resource path returns. def get_boto3_connection_settings(s3_resource_path: str = '') -> Boto3ConnectionSettings # Load a file from the workspace s3 bucket and returns its content as bytes.