Skip to content

feat: forward end-user token and expose bucket-policy GraphQL proxy - #34

Open
iago1501 wants to merge 5 commits into
masterfrom
feat/bucket-access-control-integration
Open

feat: forward end-user token and expose bucket-policy GraphQL proxy#34
iago1501 wants to merge 5 commits into
masterfrom
feat/bucket-access-control-integration

Conversation

@iago1501

@iago1501 iago1501 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements specs/bucket-access-control-integration.md (US-1, US-2, US-3 — Approved):

  • US-1: FileManager's outbound VtexIdclientAutCookie header now carries the resolved end-user token (adminUserAuthToken cookie → raw vtexidclientautcookie header → storeUserAuthToken cookie — same precedence as the existing @requiresAuth directive) instead of context.authToken (this app's own token). The header is omitted entirely for anonymous callers, never sent empty. This unblocks vtex.file-manager's upcoming per-bucket access-control enforcement (US-4) from correctly classifying callers instead of treating all proxied traffic as anonymous.
  • US-2/US-3: adds a thin GraphQL proxy over vtex.file-manager's private /policies/* REST API — listBucketPolicies (transparently paginated), getBucketPolicy, setBucketPolicy, deleteBucketPolicy — with no independent permission logic in this app; every error (including 403) from file-manager is surfaced to the caller unchanged. POST /policies/{bucket}/manifest is not exposed (exclusive to builder-hub's service-token flow).
  • Declares the vtex.file-manager:file-manager-bucket-config-rw resource policy in manifest.json, required for /policies/* calls to reach file-manager's controller at all (Decision 6).

Depends on #33 (spec approval, already merged into this branch's history) and, for /policies/* to actually authorize a caller in production, on vtex.file-manager publishing that new resource policy grant (tracked in that repo's own bucket-access-control spec, US-2/US-4; routing itself is already fixed there, see vtex/file-manager#94).

How to test

Run vtex link on this branch against a dev workspace where vtex.file-manager's bucket-access-control branch is also linked, then:

GQL_URL=https://app.io.vtex.com/vtex.file-manager-graphql/v0/{account}/{workspace}/_v/graphql

(replace {account}/{workspace} with the ones you linked to, e.g. storecomponents/cookiehop825; USER_TOKEN = your own vtex login session token from ~/.vtex/session/tokens.json)

# Call Auth Expected Notes
1 POST {GQL_URL} { settings { maxFileSizeMB } }, no token without auth 401 from Vtex.Kube.Router (Credential required...) This graphql endpoint requires some credential at the gateway for any operation, not just @requiresAuth fields
2 Same query, with VtexIdclientAutCookie: $USER_TOKEN with auth 200, {"data":{"settings":{"maxFileSizeMB":4}}} Confirms the graphql server accepts a plain end-user session token at the gateway
3 { getFile(path: "doesnotexist.json") } (public route), with $USER_TOKEN with auth 200 GraphQL body with a File Not Found error, status: 404 End-to-end proof of US-1: forwarded the resolved user token to vtex.file-manager, which executed real business logic (not a gateway rejection) and returned a genuine 404. This route is public: true on file-manager's side
4 { getFileUrl(path: "foo.json", bucket: "images") } (private route, public: false, already existed before this PR), with $USER_TOKEN with auth Same as #3 — genuine business 404, not a gateway rejection Key isolation test: proves app-to-app calls to private file-manager routes work end-to-end through this app today, for routes covered by the existing file-manager-read-write policy
5 { listBucketPolicies { bucket effectivePolicy { readAccess writeAccess } } } (private, new route), with $USER_TOKEN, account not granted file-manager-bucket-config-rw with auth, without the resource 200 GraphQL envelope with a GraphQL-level error: "Request failed with status code 403" (occasionally surfaces as 500 — see note) After fixing file-manager's routing bug (vtex/file-manager#94), this now reaches file-manager's real LicenseManagerService check and gets a genuine "not granted" 403 back through this proxy unchanged — not a gateway-level 404 anymore. Confirmed the exact same request against file-manager directly (bypassing this proxy) returns 403 3/3 times. The occasional 500 instead of 403 is this sandbox's intermittent DNS resolution failure reaching license-manager.vtex.com from file-manager's side (Name or service not known) — an environment/network characteristic of the dev sandbox, not a bug in either app's code
6 mutation { setBucketPolicy(bucket: "images", readAccess: PUBLIC, writeAccess: AUTHENTICATED) { readAccess writeAccess } }, with $USER_TOKEN, same as #5 with auth, without the resource Same as #5 Request body confirmed correctly converted to file-manager's wire format ("public"/"authenticated") before being sent
7 Same as #5/#6, but with file-manager's LicenseManager gate temporarily simulated as granted (see vtex/file-manager#94's "Happy path" section) with auth, resource simulated as granted 200 end-to-end through this proxy: listBucketPolicies/getBucketPolicy return the real vbase-backed policy, setBucketPolicy/deleteBucketPolicy mutations succeed and are reflected on the next read Proves this app's resolver/client code is correct end-to-end, independent of the still-pending resource-policy grant
8 { __type(name: "AccessLevel") { enumValues { name } } } any PUBLIC, AUTHENTICATED, ACCOUNT_ADMINISTRATOR Schema introspection sanity check

Item 5/6's 403 (not 404) is the concrete proof that the only remaining gap is the resource-policy grant itself (file-manager's LicenseManager check correctly says "not granted" for this account) -- not a routing/wiring problem on either side, which is what the previous test round (before file-manager#94's routing fix) couldn't distinguish. Item 7 removes that last variable by simulating the grant and shows the full proxy — schema, client, resolvers — working end-to-end with real data. Items 3/4 remain the strongest "no regression" signal: the exact same FileManager client code, on a pre-existing private route, already worked before this PR and still does.

@vtex-io-ci-cd

vtex-io-ci-cd Bot commented Aug 25, 2026

Copy link
Copy Markdown

Hi! I'm VTEX IO CI/CD Bot and I'll be helping you to publish your app! 🤖

Please select which version do you want to release:

  • Patch (backwards-compatible bug fixes)

  • Minor (backwards-compatible functionality)

  • Major (incompatible API changes)

And then you just need to merge your PR when you are ready! There is no need to create a release commit/tag.

  • No thanks, I would rather do it manually 😞

@vtex-io-docs-bot

Copy link
Copy Markdown

Beep boop 🤖

I noticed you didn't make any changes at the docs/ folder

  • There's nothing new to document 🤔
  • I'll do it later 😞

In order to keep track, I'll create an issue if you decide now is not a good time

  • I just updated 🎉🎉

iago1501 and others added 2 commits August 25, 2026 15:53
US-1: FileManager's constructor stopped reading context.authToken and now
receives an explicit userToken argument, forwarded as VtexIdclientAutCookie
only when present (never sent empty). The three-way token resolution
already used by the @requiresAuth directive (adminUserAuthToken cookie ->
raw vtexidclientautcookie header -> storeUserAuthToken cookie) is extracted
into a shared, exported resolveUserToken() helper in node/directives/auth.ts
so both call sites can never diverge. All four file resolvers
(getFile/getFileUrl/uploadFile/deleteFile) now pass resolveUserToken(ctx)
to FileManager.

US-2/US-3: adds listPolicies/getPolicy/setAdminPolicy/deleteAdminPolicy to
FileManager (no 404-remapping, errors incl. 403 rethrown as-is) plus
toWireAccessLevel/fromWireAccessLevel/mapPolicyViewFromWire helpers to
convert between this app's upper-snake AccessLevel enum and file-manager's
lowercase/kebab wire format. Adds the matching listBucketPolicies (with
transparent nextMarker pagination)/getBucketPolicy/setBucketPolicy/
deleteBucketPolicy resolvers, reusing resolveUserToken(ctx). No
POST /policies/{bucket}/manifest exposure, no independent permission logic
(LicenseManager on file-manager's side remains the only authorization
source).

node/FileManager.test.ts covers header inclusion/omission, that
context.authToken is never read, access-level round-tripping, wire-format
request bodies, response mapping with null-policy preservation, and
unchanged error propagation for all four new methods.

Co-authored-by: Cursor <cursoragent@cursor.com>
…source policy

Adds AccessLevel enum, BucketPolicy/BucketPolicyView types, and the four
new @requiresAuth operations (listBucketPolicies, getBucketPolicy queries;
setBucketPolicy, deleteBucketPolicy mutations) to the existing single
Query/Mutation blocks in graphql/schema.graphql.

Declares the vtex.file-manager:file-manager-bucket-config-rw resource
policy in manifest.json -- without it, kube-router has no grant to let
this app's /policies/* calls reach file-manager's controller at all,
regardless of the caller's own LicenseManager permissions (Decision 6).

Co-authored-by: Cursor <cursoragent@cursor.com>
@iago1501
iago1501 force-pushed the feat/bucket-access-control-integration branch from 2a966e4 to a716ddc Compare August 25, 2026 18:55
iago1501 and others added 2 commits August 25, 2026 18:53
…r route

vtex.file-manager renamed its private policy routes' literal path segment
from "policies" to "bucket-access-policies" to fix a routing collision (the
platform's injected router was matching by trailing path segments, so a
public GET to a bucket literally named "policies" got hijacked to the
private policy-read action). Update listPolicies, getPolicy, setAdminPolicy
and deleteAdminPolicy to call the new path, and update the matching test
assertions.

Co-authored-by: Cursor <cursoragent@cursor.com>
Mirrors vtex.file-manager's revert of the policy routes from
/bucket-access-policies back to /policies -- keeps this client in sync
with the upstream route names and the existing policies.json VRN grant.

Co-authored-by: Cursor <cursoragent@cursor.com>
@iago1501 iago1501 self-assigned this Aug 26, 2026
@iago1501 iago1501 added the enhancement New feature or request label Aug 26, 2026
setBucketPolicy, deleteBucketPolicy, listBucketPolicies and getBucketPolicy
were only gated on "user is logged in" at the GraphQL layer, same as any
other authenticated query. file-manager still enforces the real
file-manager-bucket-config License Manager resource downstream, so this
was never an actual bypass, but a non-admin store-token request would
reach file-manager just to be rejected there.

Extend the existing deleteFile admin gate to these bucket-policy
operations so non-admin requests are rejected in GraphQL, before ever
calling file-manager.

Co-authored-by: Cursor <cursoragent@cursor.com>

@monteirogc monteirogc left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

US-1 e o proxy de /policies/* estão no rumo certo (token do end-user, header omitido no anônimo, sem /manifest, resource name bate com file-manager#94).

Dois pontos pra fechar antes do approve. CI (vtexio/build) também vai continuar vermelho até o file-manager publicar file-manager-bucket-config-rw.

Comment thread node/directives/auth.ts
'setBucketPolicy',
'deleteBucketPolicy',
'listBucketPolicies',
'getBucketPolicy',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isso contradiz a Decision 5 da spec (Approved): /policies/* só leva @requiresAuth + License Manager no file-manager — sem Sphinx.

sphinx.isAdmin ≠ resource file-manager-bucket-config. Role com o resource e sem ser admin Sphinx leva false deny, e o caller deixa de ver o 403 original.

Reverter, ou emendar a Decision 5.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@requiresAuth só exige login — token de store passa. Sem um corte na borda, o customer chega no /policies/* só para o file-manager recusar no License Manager.

O isAdmin aqui é o mesmo filtro Admin vs store que deleteFile já usa neste app. Role no LM só se atribui a usuário Admin; store não tem o resource. Não é um oracle paralelo ao file-manager-bucket-config: Admin sem o resource ainda chega no file-manager e vê o 403 original. O Sphinx só corta a população (store) antes do round-trip.

O persona "tem o resource e não é admin Sphinx" não é um caso que a gente suporte — quem recebe a role já é Admin. Por isso não estamos emendando a Decision 5: o LM continua sendo quem autoriza qual admin gerencia policy.

Comment thread node/FileManager.ts
case 'account-administrator':
return 'ACCOUNT_ADMINISTRATOR'
default:
return 'PUBLIC'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fail-open pra PUBLIC. O converter do file-manager throw em valor inválido; aqui um wire missing/futuro aparece como público no Admin.

Throw no default.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants