Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add new directive to validate auth token with feature flag #138

Closed
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

- add new directive to validate auth token, with feature flag to enable the validation

## [0.44.1] - 2023-11-09

### Fixed
Expand Down
1 change: 1 addition & 0 deletions graphql/directives.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ directive @withPermissions on FIELD | FIELD_DEFINITION
directive @checkUserAccess on FIELD | FIELD_DEFINITION
directive @checkAdminAccess on FIELD | FIELD_DEFINITION
directive @auditAccess on FIELD | FIELD_DEFINITION
directive @checkAccessWithFeatureFlag on FIELD | FIELD_DEFINITION
24 changes: 20 additions & 4 deletions graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,23 @@ type Query {
pageSize: Int = 25
sortOrder: String = "ASC"
sortedBy: String = "name"
): OrganizationResult @cacheControl(scope: PUBLIC, maxAge: SHORT) @auditAccess
): OrganizationResult
@cacheControl(scope: PUBLIC, maxAge: SHORT)
@auditAccess
@checkAccessWithFeatureFlag

getOrganizationsWithoutSalesManager: [Organization]
@cacheControl(scope: PRIVATE)

getOrganizationById(id: ID): Organization
@cacheControl(scope: PUBLIC, maxAge: SHORT)
@auditAccess
@checkAccessWithFeatureFlag
getOrganizationByIdStorefront(id: ID): Organization
@withSession
@cacheControl(scope: PRIVATE)
@auditAccess
@checkAccessWithFeatureFlag
getCostCenters(
search: String
page: Int = 1
Expand All @@ -43,7 +48,10 @@ type Query {
pageSize: Int = 25
sortOrder: String = "ASC"
sortedBy: String = "name"
): CostCenterResult @cacheControl(scope: PUBLIC, maxAge: SHORT) @auditAccess
): CostCenterResult
@cacheControl(scope: PUBLIC, maxAge: SHORT)
@auditAccess
@checkAccessWithFeatureFlag
getCostCentersByOrganizationIdStorefront(
id: ID
search: String
Expand All @@ -55,10 +63,12 @@ type Query {
getCostCenterById(id: ID!): CostCenter
@cacheControl(scope: PUBLIC, maxAge: SHORT)
@auditAccess
@checkAccessWithFeatureFlag
getCostCenterByIdStorefront(id: ID): CostCenter
@withSession
@cacheControl(scope: PRIVATE, maxAge: SHORT)
@auditAccess
@checkAccessWithFeatureFlag
getUsers(organizationId: ID, costCenterId: ID): [B2BUser]
@withSession
@checkUserAccess
Expand All @@ -78,26 +88,29 @@ type Query {
getPaymentTerms: [PaymentTerm]
@cacheControl(scope: PUBLIC, maxAge: SHORT)
@auditAccess

@checkAccessWithFeatureFlag
getOrganizationsByEmail(email: String): [B2BOrganization]
@checkUserAccess
@cacheControl(scope: PRIVATE)
@auditAccess
Copy link
Contributor

@enzomerca enzomerca Nov 23, 2023

Choose a reason for hiding this comment

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

getOrganizationsByEmail is on https://vtex-dev.atlassian.net/browse/B2BTEAM-1285 but we did not add the @checkAccessWithFeatureFlag for it

Copy link
Contributor Author

@Rudge Rudge Nov 27, 2023

Choose a reason for hiding this comment

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

It is not necessary because there is already a directive @checkUserAccess. I am removing it from the task.


checkOrganizationIsActive(id: String): Boolean @cacheControl(scope: PRIVATE)
getSalesChannels: [Channels]
@cacheControl(scope: PUBLIC, maxAge: SHORT)
@auditAccess
@checkAccessWithFeatureFlag
getBinding(email: String!): Boolean
@withSession
@cacheControl(scope: PUBLIC, maxAge: SHORT)
@auditAccess
@checkAccessWithFeatureFlag
getMarketingTags(costId: ID!): MarketingTags
@cacheControl(scope: PUBLIC, maxAge: SHORT)
@auditAccess
@checkAccessWithFeatureFlag
getB2BSettings: B2BSettings
@cacheControl(scope: PUBLIC, maxAge: SHORT)
@auditAccess
@checkAccessWithFeatureFlag
getSellers: [Seller] @checkAdminAccess @cacheControl(scope: PRIVATE)
}

Expand Down Expand Up @@ -228,10 +241,13 @@ type Mutation {
@withSession
@withPermissions
@cacheControl(scope: PRIVATE)
@auditAccess
impersonateB2BUser(id: ID!): MutationResponse
@withSession
@withPermissions
@cacheControl(scope: PRIVATE)
@auditAccess
@checkAccessWithFeatureFlag
saveSalesChannels(channels: [SalesChannelsInput]!): MutationResponse
@checkAdminAccess
@cacheControl(scope: PRIVATE)
Expand Down
4 changes: 2 additions & 2 deletions node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.44.1",
"dependencies": {
"@types/lodash": "4.14.74",
"@vtex/api": "6.45.22",
"@vtex/api": "6.46.0",
"atob": "^2.1.2",
"co-body": "^6.0.0",
"graphql": "^14.5.0",
Expand All @@ -20,7 +20,7 @@
"@types/jsonwebtoken": "^8.5.0",
"@types/node": "^12.12.21",
"@types/ramda": "types/npm-ramda#dist",
"@vtex/api": "6.45.22",
"@vtex/api": "6.46.0",
"@vtex/prettier-config": "^0.3.1",
"@vtex/tsconfig": "^0.6.0",
"jest": "27.5.1",
Expand Down
10 changes: 6 additions & 4 deletions node/resolvers/directives.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { WithSession } from './directives/withSession'
import { WithPermissions } from './directives/withPermissions'
import { AuditAccess } from './directives/auditAccess'
import { CheckAccessWithFeatureFlag } from './directives/checkAccessWithFeatureFlag'
import { CheckAdminAccess } from './directives/checkAdminAccess'
import { CheckUserAccess } from './directives/checkUserAccess'
import { AuditAccess } from './directives/auditAccess'
import { WithPermissions } from './directives/withPermissions'
import { WithSession } from './directives/withSession'

export const schemaDirectives = {
auditAccess: AuditAccess as any,
checkAccessWithFeatureFlag: CheckAccessWithFeatureFlag as any,
checkAdminAccess: CheckAdminAccess as any,
checkUserAccess: CheckUserAccess as any,
withPermissions: WithPermissions as any,
withSession: WithSession as any,
auditAccess: AuditAccess as any,
}
35 changes: 35 additions & 0 deletions node/resolvers/directives/checkAccessWithFeatureFlag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { GraphQLField } from 'graphql'
import { defaultFieldResolver } from 'graphql'
import { SchemaDirectiveVisitor } from 'graphql-tools'

import { checkUserOrAdminTokenAccess } from './checkUserAccess'

export class CheckAccessWithFeatureFlag extends SchemaDirectiveVisitor {
public visitFieldDefinition(field: GraphQLField<any, any>) {
const { resolve = defaultFieldResolver } = field

field.resolve = async (
root: any,
args: any,
context: Context,
info: any
) => {
const {
vtex: { adminUserAuthToken },
clients: { masterdata },
} = context

const config: { enable: boolean } = await masterdata.getDocument({
dataEntity: 'auth_validation_config',
fields: ['enable'],
id: 'b2b-organizations-graphql',
})

if (config?.enable) {
await checkUserOrAdminTokenAccess(context, adminUserAuthToken)
}

return resolve(root, args, context, info)
}
}
}
107 changes: 67 additions & 40 deletions node/resolvers/directives/checkUserAccess.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,65 @@
import { SchemaDirectiveVisitor } from 'graphql-tools'
import { AuthenticationError, ForbiddenError } from '@vtex/api'
import type { GraphQLField } from 'graphql'
import { defaultFieldResolver } from 'graphql'
import { AuthenticationError, ForbiddenError } from '@vtex/api'
import { SchemaDirectiveVisitor } from 'graphql-tools'

export async function checkUserOrAdminTokenAccess(
ctx: Context,
token?: string,
operation?: string
) {
const {
vtex: { storeUserAuthToken, logger },
clients: { identity, vtexId },
} = ctx

if (!token && !storeUserAuthToken) {
logger.warn({
message: `CheckUserAccess: No admin or store token was provided for ${operation}`,
operation,
})
throw new AuthenticationError('No admin or store token was provided')
}

if (token) {
try {
await identity.validateToken({ token })
} catch (err) {
logger.warn({
error: err,
message: `CheckUserAccess: Invalid admin token for ${operation}`,
operation,
token,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

This PR is just missing the changes you made on that other similar for storefront-permissions, where you removed the token field from the logs and also removed operation from the log messages.

throw new ForbiddenError('Unauthorized Access')
}
} else if (storeUserAuthToken) {
let authUser = null

try {
authUser = await vtexId.getAuthenticatedUser(storeUserAuthToken)
if (!authUser?.user) {
logger.warn({
message: `CheckUserAccess: No valid user found by store user token for ${operation}`,
operation,
})
authUser = null
}
} catch (err) {
logger.warn({
error: err,
message: `CheckUserAccess: Invalid store user token for ${operation}`,
operation,
token: storeUserAuthToken,
})
authUser = null
}

if (!authUser) {
throw new ForbiddenError('Unauthorized Access')
}
}
}

export class CheckUserAccess extends SchemaDirectiveVisitor {
public visitFieldDefinition(field: GraphQLField<any, any>) {
Expand All @@ -14,8 +72,8 @@ export class CheckUserAccess extends SchemaDirectiveVisitor {
info: any
) => {
const {
vtex: { adminUserAuthToken, storeUserAuthToken, logger },
clients: { identity, vtexId },
vtex: { adminUserAuthToken },
clients: { identity },
} = context

let token = adminUserAuthToken
Expand All @@ -31,42 +89,11 @@ export class CheckUserAccess extends SchemaDirectiveVisitor {
context.vtex.adminUserAuthToken = token
}

if (!token && !storeUserAuthToken) {
throw new AuthenticationError('No admin or store token was provided')
}

if (token) {
try {
await identity.validateToken({ token })
} catch (err) {
logger.warn({
error: err,
message: 'CheckUserAccess: Invalid admin token',
token,
})
throw new ForbiddenError('Unauthorized Access')
}
} else if (storeUserAuthToken) {
let authUser = null

try {
authUser = await vtexId.getAuthenticatedUser(storeUserAuthToken)
if (!authUser?.user) {
authUser = null
}
} catch (err) {
logger.warn({
error: err,
message: 'CheckUserAccess: Invalid store user token',
token: storeUserAuthToken,
})
authUser = null
}

if (!authUser) {
throw new ForbiddenError('Unauthorized Access')
}
}
await checkUserOrAdminTokenAccess(
context,
token,
field.astNode?.name?.value
)

return resolve(root, args, context, info)
}
Expand Down
3 changes: 2 additions & 1 deletion node/utils/metrics/metrics.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import axios from 'axios'

const ANALYTICS_URL = 'https://rc.vtex.com/api/analytics/schemaless-events'
const ANALYTICS_URL =
'https://analytics.vtex.com/api/analytics/schemaless-events'
Copy link
Contributor

Choose a reason for hiding this comment

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

why did this change? What is the difference between rc and analytics urls?

Copy link
Contributor Author

@Rudge Rudge Nov 16, 2023

Choose a reason for hiding this comment

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

I found some issues with using rc and the documentation suggests using analytics.

"This can be done by sending events to the following endpoint: https://analytics.vtex.com/api/analytics/schemaless-events (For applications on the VTEX Network) or to https://rc.vtex.com/api/analytics/schemaless-events (For applications outside the VTEX Network, such as Front End Applications)"


export const B2B_METRIC_NAME = 'b2b-suite-buyerorg-data'

Expand Down
8 changes: 4 additions & 4 deletions node/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -836,10 +836,10 @@
dependencies:
"@types/yargs-parser" "*"

"@vtex/api@6.45.22":
version "6.45.22"
resolved "https://registry.yarnpkg.com/@vtex/api/-/api-6.45.22.tgz#fa9bbfde1a4d4fbbaf6cce9f6dbc9bb9ee929ba3"
integrity sha512-g5cGUDhF4FADgSMpQmce/bnIZumwGlPG2cabwbQKIQ+cCFMZqOEM/n+YQb1+S8bCyHkzW3u/ZABoyCKi5/nxxg==
"@vtex/api@6.46.0":
version "6.46.0"
resolved "https://registry.yarnpkg.com/@vtex/api/-/api-6.46.0.tgz#208d14b96cbc8fd5eb6bd18fbd0c8424886e6154"
integrity sha512-XAvJlD1FG1GynhPXiMcayunahFCL2r3ilO5MHAWKxYvB/ljyxi4+U+rVpweeaQGpxHfhKHdfPe7qNEEh2oa2lw==
dependencies:
"@types/koa" "^2.11.0"
"@types/koa-compose" "^3.2.3"
Expand Down
Loading