Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added
- Bucket restrictions when uploading files

## [0.7.4] - 2025-06-16

### Changed
Expand Down
7 changes: 7 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
"path": "/api/vtexid/pub/authenticated/user"
}
},
{
"name": "outbound-access",
"attrs": {
"host": "{{account}}.vtexcommercestable.com.br",
"path": "/api/license-manager/pvt/accounts/{{account}}/products/*"
}
},
{
"name": "sphinx-is-admin"
}
Expand Down
3 changes: 1 addition & 2 deletions node/config/allowList.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export const ALLOW_LIST = [
'storecomponents',
export const ALLOW_LIST = [
'adidasuy',
'amakha',
'ambientegourmetb2b',
Expand Down
11 changes: 11 additions & 0 deletions node/config/bucketRestrictions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const BUCKET_RESTRICTIONS = [

// Only users with permission to access the page that allows logo uploads
// can upload files to the logo bucket
{
bucket: 'logo',
productCode: '13', //UI resources
resourceCode: 'MarketplaceNetwork', //Access the Marketplace Network
errorMessage: 'User does not have access to upload logo files',
}
]
93 changes: 55 additions & 38 deletions node/directives/auth.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,71 @@
import { defaultFieldResolver, GraphQLField } from 'graphql'
import { SchemaDirectiveVisitor } from 'graphql-tools'

import axios from 'axios'
import { ALLOW_LIST } from '../config/allowList'
import { BUCKET_RESTRICTIONS } from '../config/bucketRestrictions'

export const authFromCookie = async (ctx: any, operationName: string) => {
const {
clients: { sphinx, vtexID },
vtex: { authToken },
} = ctx
async function getUserEmail(ctx: any): Promise<string> {
const { clients: { vtexID }, vtex: { authToken } } = ctx

const vtexIdToken =
ctx.cookies.get('VtexIdclientAutCookie') ??
ctx.request.header.vtexidclientautcookie

if (!vtexIdToken) {
return 'User must be logged to access this resource'
}
if (!vtexIdToken) throw new Error('User must be logged to access this resource')

const { user: email } = (await vtexID.getIdUser(vtexIdToken, authToken)) || {
user: '',
}
const { user: email } = (await vtexID.getIdUser(vtexIdToken, authToken)) || { user: '' }

if (!email) throw new Error('Could not find user specified by token.')

return email
}

async function getUserCanAccessResource(
authToken: string, account: string, userEmail: string, productCode: string, resourceCode: string): Promise<boolean> {

const url = `http://${account}.vtexcommercestable.com.br/api/license-manager/pvt/accounts/${account}/products/${productCode}/logins/${userEmail}/resources/${resourceCode}/granted`

const req = await axios.request({
headers: { 'Authorization': authToken },
method: 'get',
url,
})

return req.data
}

if (!email) {
return 'Could not find user specified by token.'
async function checkAuthorizationRestrictions(
{ ctx, operationName, args, email }: { ctx: any, operationName: string, args: any, email: string }) {

const { account, authToken } = ctx.vtex
const { sphinx } = ctx.clients

if (operationName === 'uploadFile' && ALLOW_LIST.includes(account)) return true

if (operationName === 'uploadFile') {
const restriction = BUCKET_RESTRICTIONS.find(
({ bucket }) => bucket === args.bucket)

if (restriction) {
const canAccess = await getUserCanAccessResource(
authToken,
account,
email,
restriction.productCode,
restriction.resourceCode
)

if (canAccess) return true
throw new Error(restriction.errorMessage)
}
}

if (operationName === 'deleteFile') {
// Only admin users can delete files
const isAdminUser = await sphinx.isAdmin(email)
const isAdmin = await sphinx.isAdmin(email)

if (!isAdminUser) {
return 'User is not admin and can not access resource.'
}
if (isAdmin) return true
throw new Error('User is not admin and can not access resource.')
}

return true
Expand All @@ -41,28 +75,11 @@ export class Authorization extends SchemaDirectiveVisitor {
public visitFieldDefinition(field: GraphQLField<any, any>) {
const { resolve = defaultFieldResolver } = field

// eslint-disable-next-line max-params
field.resolve = async (root, args, ctx, info) => {
const operationName = info.fieldName
let isAllowed = false

if (operationName === 'uploadFile') {
const isInAllowList = ALLOW_LIST.includes(ctx.vtex.account)

if (isInAllowList) {
isAllowed = true
}
}

if (!isAllowed) {
const cookieAllowsAccess = await authFromCookie(ctx, operationName)

if (cookieAllowsAccess !== true) {
throw new Error(cookieAllowsAccess)
}
}

const email = await getUserEmail(ctx)
await checkAuthorizationRestrictions({ ctx, operationName, args, email })
return resolve(root, args, ctx, info)
}
}
}
}