Skip to content

UN-3487 [FIX] Restrict the S3/MinIO connector to one bucket - #2295

Merged
kirtimanmishrazipstack merged 9 commits into
mainfrom
UN-3487-s3-bucket-restriction
Sep 24, 2026
Merged

kirtimanmishrazipstack merged 9 commits into
mainfrom
UN-3487-s3-bucket-restriction

Conversation

@kirtimanmishrazipstack

@kirtimanmishrazipstack kirtimanmishrazipstack commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

What

  • S3/MinIO connectors must now be pointed at one specific bucket. They can no longer reach any bucket the underlying AWS credentials happen to have access to.
  • Fixed a related access-control bug: any member of an org could open someone else's storage connector just by knowing its ID, regardless of whether it was shared with them.

Why

On this deployment, every S3 connector shares one AWS identity, so today any connector can reach any bucket that identity can reach — including other teams' data. Locking each connector to a single bucket keeps every team confined to its own bucket.

The access-control bug was fixed alongside it because without it, restricting a connector to one bucket wouldn't have meant much — anyone who knew a connector's ID could still open it directly, sharing settings or not.

How

  • A bucket is now a required field when setting up an S3/MinIO connector. Existing connectors without one stop working until an admin edits them to add one.
  • Every list, read, and write on a connector is confined to its configured bucket, no matter what the underlying AWS credentials could otherwise reach.
  • Storage connectors can now only be opened by their owner or someone they've been explicitly shared with — no longer by anyone in the org who has the ID.

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

Yes, intentionally. Any existing S3/MinIO connector without a bucket configured will stop working the next time it's used, until an admin edits it to add one. If a connector's saved folder path already started with the bucket name, that path needs to be shortened by one segment once a bucket is added, since the bucket is no longer part of the browsable path. Cloud storage (UCS) connectors are unaffected.

Database Migrations

  • None

Env Config

  • None

Relevant Docs

  • Follow-up, not blocking: unstract-docs #246 stops recommending full-account S3 access in the connector setup guide.

Related Issues or PRs

Dependencies Versions

  • None

Notes on Testing

  • New tests cover: a connector without a bucket refuses to start; a connector's listing/reading/writing all stay inside its bucket; and the access-control fix — only the owner or someone explicitly shared with can open a connector, everyone else gets a 404.
  • Full unit and integration suites pass with no regressions. End-to-end wasn't re-run since this change isn't on that path.

Screenshots

1

Checklist

I have read and understood the Contribution Guidelines.

The S3/MinIO connector had no bucket field, so its reach was bounded only
by whatever the underlying credential (often a shared IAM role) could see
account-wide. Any connector could browse into any bucket that role had
access to, including other teams' data.

- `bucket` is now a required field on the connector schema, matching
  Azure's existing (but unenforced) precedent — this one is actually
  enforced.
- `MinioFS.get_fsspec_fs()` wraps the filesystem in fsspec's own
  `DirFileSystem` when a bucket is set, confining every list/read/write
  (and `test_credentials`) to that one bucket regardless of what the
  credential could otherwise reach. UCS opts out via `_REQUIRES_BUCKET`,
  since it restricts access through its own `path` setting instead.
- Fixed a related IDOR in `file_management`: `list`/`download`/`upload`
  resolved a connector by a bare `ConnectorInstance.objects.get(pk=id)`
  with no ownership/group check, org-scoped only. Any org member holding
  a connector's id could browse/read/write through it regardless of
  sharing. Now scoped through `ConnectorInstance.objects.for_user()`, the
  same queryset every other connector view already uses.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

via Greptile

RetriggerConfidence Score: 5/5

The PR appears safe to merge; the prior bucket-prefix and default-walk defects are fixed, and no new actionable failure is established.

Summary

Restricts each S3/MinIO connector to its configured bucket and scopes file-management access to connectors available to the requesting user.

  • Requires a bucket for S3/MinIO connectors while retaining UCS compatibility.
  • Wraps S3 access in a bucket-scoped filesystem and normalizes traversal metadata.
  • Applies ownership and sharing checks to listing, downloading, and uploading.
  • Updates integration and end-to-end fixtures for bucket-relative paths.
  • Adds regression coverage for access control, bucket confinement, and root-path handling.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
    U[Authenticated user] --> V[FileManagementViewSet]
    V --> Q[ConnectorInstance.objects.for_user]
    Q -->|authorized connector| M[MinioFS]
    Q -->|not owned or shared| N[404 Not Found]
    M --> D[Bucket-scoped DirFileSystem]
    D --> B[Configured S3 or MinIO bucket]
Loading

Reviews (9) · Last reviewed commit: "Merge branch 'main' into UN-3487-s3-buck..."

Comment thread unstract/connectors/src/unstract/connectors/filesystems/minio/minio.py Outdated
Greptile flagged (P1): DirFileSystem's own `.path` attribute is the
wrapped bucket prefix, but file_management_helper's root-path fallback
treated it as a connector-level default root and substituted it as the
operation path — every root-level list/upload then resolved to
`<bucket>/<bucket>/...` instead of `<bucket>/...`.

- Factored the fallback into `_default_root_path()`, which now excludes
  DirFileSystem explicitly: its `.path` never needs re-applying, every
  operation already resolves relative to it.
- Added regression tests exercising the real `FileManagerHelper.list_files`
  / `upload_file` against a bucket-scoped MinioFS with a mocked S3 backend,
  asserting the underlying call receives a single bucket prefix.
- Fixed the same root cause in the e2e ETL fixture: its connector had no
  `bucket` (now required), and its configured `folders`/`outputFolder`
  included the bucket name as a path segment — now implicit via the
  connector's own scope, per the same fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… bucket

TestMinoFS::test_minio asserted that connecting without a bucket lists
every bucket in the account — exactly the behavior this ticket removes.
Bootstrap the test bucket via a plain S3FileSystem (the connector itself
can no longer discover buckets this way), then assert the bucket-scoped
connection lists successfully.

CI signal: this is what "test (integration)" failed on in both runs, not
a new issue. The accompanying test_pg_barrier.py failures in that same
job are a pre-existing, unrelated CI DNS flake (identical on both runs,
"could not translate host name unstract-db"), not caused by this PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ames

Found by tracing the e2e ETL failure through the compose stack's own
logs (docker-compose-logs.txt from the CI run), not assumed: the worker
log showed "Failed to prepare input file and metadata:
unstract/unstract/e2e-in-.../probe.txt" — a double bucket prefix, but
from a different cause than the earlier file_management_helper fix.

fsspec's DirFileSystem.walk() relpaths the directory string it yields,
but not the `name` field inside each file/dir entry's own metadata dict
— those still carry the wrapped fs's raw, bucket-prefixed key (ls()
already fixes every entry; walk() doesn't, confirmed by reading fsspec's
own source and reproducing it directly against a mocked S3FileSystem).
Workflow-execution file discovery walks (UnstractFileSystem.list_files,
shared by backend and workers); the UI file browser lists — which is why
this was invisible to the file_management-focused testing so far.

_BucketScopedFileSystem wraps DirFileSystem and relpaths both dict keys
and each entry's `name` field for walk()/_walk(), so discovery sees the
same bucket-relative paths ls() already produced correctly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Greptile P1: the walk()/_walk() override always treated dirs/files as
detail=True dicts, calling .items() unconditionally. fsspec's own
default is detail=False (confirmed by reading AbstractFileSystem.walk's
source), which yields plain basename lists — every caller using the
default contract got an AttributeError instead of results.

Also caught, verified empirically against the real base implementation:
my own relpath was wrong on the dict-keyed (detail=True) path too. The
dict key is already a bare basename — only each entry's own `name`
field carries the bucket-qualified path. Relpath-ing the key itself
would hit `DirFileSystem._relpath`'s own assertion on real data (a
basename never starts with the bucket prefix); the prior test happened
to pass only because its mock's key and `name` field were identical,
which isn't how fsspec actually shapes walk() results.

Re-checked Greptile's second (repeated) finding on this same review —
the file_management_helper double-prefix fix from an earlier commit is
still intact: `_BucketScopedFileSystem` is a `DirFileSystem` subclass,
so the existing `isinstance(fs, DirFileSystem)` exclusion still applies.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nector form

The connector config form renders fields in json_schema.json's property
order. Bucket landed second, right after the connector name, ahead of
the credential fields users fill in first. Moved it to the end so the
form still reads top-to-bottom the way users configure it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e toast

Connector error messages come wrapped in a markdown code fence
(handle_s3fs_exception), which CustomMarkdown renders as a bare <pre>.
<pre> defaults to white-space: pre, so a long single-line message like
the "Invalid Secret" one never wraps — it just overflows past the
fixed-width toast instead of staying inside it. Added pre-wrap +
overflow-wrap so long connector errors wrap like the rest of the toast.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Frontend Lint Report (Biome)

✅ All checks passed! No linting or formatting issues found.

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
✅ e2e-api-deployment e2e 3 0 0 0 14.7
✅ e2e-coowners e2e 1 0 0 0 1.1
✅ e2e-etl e2e 1 0 0 0 15.9
✅ e2e-login e2e 2 0 0 0 1.1
✅ e2e-prompt-studio e2e 1 0 0 0 7.1
✅ e2e-smoke e2e 2 0 0 0 0.8
✅ e2e-workflow e2e 1 0 0 0 17.9
❌ frontend unit 0 1 0 0 0.0
✅ integration-backend integration 603 0 0 26 46.3
✅ integration-connectors integration 1 0 0 7 7.8
❌ integration-workers integration 159 5 0 1 47.7
❌ ui e2e 0 1 0 0 0.0
✅ unit-backend unit 1320 0 0 1 46.1
✅ unit-connectors unit 72 0 0 0 9.8
✅ unit-core unit 237 0 0 0 3.0
✅ unit-platform-service unit 15 0 0 0 2.6
✅ unit-rig unit 120 0 0 0 4.5
✅ unit-runner unit 10 0 0 0 3.0
✅ unit-sdk1 unit 587 0 0 0 29.8
✅ unit-workers unit 1362 0 0 1 126.4
TOTAL 4497 7 0 36 385.5

Critical paths

⚠️ Critical paths not yet covered

  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (declared coverage: no groups declared)
✅ Covered critical paths
  • auth-login — covered by e2e-login
  • adapter-register-llm — covered by integration-backend
  • workflow-author — covered by integration-backend
  • co-owner-manage — covered by integration-backend, e2e-coowners
  • workflow-create-execute — covered by e2e-workflow
  • api-deployment-provision — covered by integration-backend
  • api-deployment-auth — covered by integration-backend
  • api-deployment-run — covered by e2e-api-deployment
  • mcp-server-auth — covered by integration-backend
  • mcp-platform-auth — covered by integration-backend
  • platform-key-whoami — covered by integration-backend
  • prompt-studio-author — covered by integration-backend
  • prompt-studio-fetch-response — covered by e2e-prompt-studio
  • connector-register-test — covered by integration-backend
  • pipeline-etl-execute — covered by e2e-etl
  • usage-aggregate-read — covered by integration-backend
  • usage-token-tracking — covered by e2e-api-deployment
  • callback-result-delivery — covered by e2e-api-deployment

@kirtimanmishrazipstack
kirtimanmishrazipstack merged commit 89a4434 into main Sep 24, 2026
11 checks passed
@kirtimanmishrazipstack
kirtimanmishrazipstack deleted the UN-3487-s3-bucket-restriction branch September 24, 2026 11:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants