From 49e471191845ceb238bf9a66d00bd03500f5c3f8 Mon Sep 17 00:00:00 2001 From: Rudge Date: Wed, 15 Nov 2023 17:28:49 -0300 Subject: [PATCH 1/2] feat: add directive to validate auth token and feature flag jira: B2BTEAM-1484 --- CHANGELOG.md | 4 + graphql/directives.graphql | 1 + graphql/schema.graphql | 38 ++++++- node/directives/checkAccessWithFeatureFlag.ts | 34 +++++++ node/directives/checkUserAccess.ts | 99 +++++++++++-------- node/directives/index.ts | 2 + 6 files changed, 132 insertions(+), 46 deletions(-) create mode 100644 node/directives/checkAccessWithFeatureFlag.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 18c04bd..0485b3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 + ## [1.37.2] - 2023-11-10 ### Fixed diff --git a/graphql/directives.graphql b/graphql/directives.graphql index 7a3de32..0b3a418 100644 --- a/graphql/directives.graphql +++ b/graphql/directives.graphql @@ -4,3 +4,4 @@ directive @withSession on FIELD_DEFINITION directive @withSender on FIELD_DEFINITION directive @withUserPermissions on FIELD_DEFINITION directive @auditAccess on FIELD | FIELD_DEFINITION +directive @checkAccessWithFeatureFlag on FIELD | FIELD_DEFINITION diff --git a/graphql/schema.graphql b/graphql/schema.graphql index bc4c7fd..480f20a 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -4,6 +4,7 @@ type Query { @settings(settingsType: "workspace") @withSender @auditAccess + @checkAccessWithFeatureFlag getRole(id: ID!): Role @cacheControl(scope: PRIVATE, maxAge: SHORT) @@ -15,6 +16,7 @@ type Query { @withSession @withSender @auditAccess + @checkAccessWithFeatureFlag getFeaturesByModule(module: String!): Feature @settings(settingsType: "workspace") @@ -25,6 +27,7 @@ type Query { @cacheControl(scope: PRIVATE, maxAge: SHORT) @withSender @auditAccess + @checkAccessWithFeatureFlag getUser(id: ID!): User @cacheControl(scope: PRIVATE) getB2BUser(id: ID!): User @cacheControl(scope: PRIVATE) @@ -33,22 +36,27 @@ type Query { @cacheControl(scope: PRIVATE) @withSender @auditAccess + @checkAccessWithFeatureFlag getUserByEmail(email: String!): [User] @cacheControl(scope: PRIVATE) @withSender @auditAccess + @checkAccessWithFeatureFlag listAllUsers: [User] @cacheControl(scope: PRIVATE, maxAge: SHORT) @withSender @auditAccess + @checkAccessWithFeatureFlag listUsers(organizationId: ID, costCenterId: ID, roleId: ID): [User] @cacheControl(scope: PRIVATE, maxAge: SHORT) @deprecated( reason: "This query is deprecated, use listUsersPaginated query instead." ) + @auditAccess + @checkAccessWithFeatureFlag listUsersPaginated( organizationId: ID @@ -59,7 +67,10 @@ type Query { search: String sortOrder: String sortedBy: String - ): UserPagination @cacheControl(scope: PRIVATE, maxAge: SHORT) + ): UserPagination + @cacheControl(scope: PRIVATE, maxAge: SHORT) + @auditAccess + @checkAccessWithFeatureFlag checkImpersonation: UserImpersonation @settings(settingsType: "workspace") @@ -72,14 +83,25 @@ type Query { @withSender @cacheControl(scope: PRIVATE) - getSessionWatcher: Boolean @cacheControl(scope: PRIVATE) + getSessionWatcher: Boolean + @cacheControl(scope: PRIVATE) + @auditAccess + @checkAccessWithFeatureFlag - getUsersByEmail(email: String!): [User] @cacheControl(scope: PRIVATE) + getUsersByEmail(email: String!): [User] + @cacheControl(scope: PRIVATE) + @auditAccess + @checkAccessWithFeatureFlag - getActiveUserByEmail(email: String!): User @cacheControl(scope: PRIVATE) + getActiveUserByEmail(email: String!): User + @cacheControl(scope: PRIVATE) + @auditAccess + @checkAccessWithFeatureFlag getOrganizationsByEmail(email: String!): [Organization] @cacheControl(scope: PRIVATE) + @auditAccess + @checkAccessWithFeatureFlag } type Mutation { @@ -90,12 +112,17 @@ type Mutation { name: String! slug: String features: [FeatureInput] - ): MutationResponse @cacheControl(scope: PRIVATE) @withSender @auditAccess + ): MutationResponse + @cacheControl(scope: PRIVATE) + @withSender + @auditAccess + @checkAccessWithFeatureFlag deleteRole(id: ID!): MutationResponse @cacheControl(scope: PRIVATE) @withSender @auditAccess + @checkAccessWithFeatureFlag saveUser( id: ID @@ -160,6 +187,7 @@ type Mutation { @cacheControl(scope: PRIVATE) @withSender @auditAccess + @checkAccessWithFeatureFlag } type UserImpersonation { diff --git a/node/directives/checkAccessWithFeatureFlag.ts b/node/directives/checkAccessWithFeatureFlag.ts new file mode 100644 index 0000000..728e91f --- /dev/null +++ b/node/directives/checkAccessWithFeatureFlag.ts @@ -0,0 +1,34 @@ +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) { + const { resolve = defaultFieldResolver } = field + + field.resolve = async ( + root: any, + args: any, + context: Context, + info: any + ) => { + const { + clients: { masterdata }, + } = context + + const config: { enable: boolean } = await masterdata.getDocument({ + dataEntity: 'auth_validation_config', + fields: ['enable'], + id: 'storefront-permissions', + }) + + if (config?.enable) { + await checkUserOrAdminTokenAccess(context, field.astNode?.name?.value) + } + + return resolve(root, args, context, info) + } + } +} diff --git a/node/directives/checkUserAccess.ts b/node/directives/checkUserAccess.ts index 6e74e6c..e32ca34 100644 --- a/node/directives/checkUserAccess.ts +++ b/node/directives/checkUserAccess.ts @@ -3,6 +3,63 @@ import type { GraphQLField } from 'graphql' import { defaultFieldResolver } from 'graphql' import { SchemaDirectiveVisitor } from 'graphql-tools' +export async function checkUserOrAdminTokenAccess( + ctx: Context, + operation?: string +) { + const { + vtex: { adminUserAuthToken, storeUserAuthToken, logger }, + clients: { identity, vtexId }, + } = ctx + + if (!adminUserAuthToken && !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 (adminUserAuthToken) { + try { + await identity.validateToken({ token: adminUserAuthToken }) + } catch (err) { + logger.warn({ + error: err, + message: `CheckUserAccess: Invalid admin token for ${operation}`, + operation, + token: adminUserAuthToken, + }) + 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: adminUserAuthToken, + }) + authUser = null + } + + if (!authUser) { + throw new ForbiddenError('Unauthorized Access') + } + } +} + export class CheckUserAccess extends SchemaDirectiveVisitor { public visitFieldDefinition(field: GraphQLField) { const { resolve = defaultFieldResolver } = field @@ -13,47 +70,7 @@ export class CheckUserAccess extends SchemaDirectiveVisitor { context: Context, info: any ) => { - const { - vtex: { adminUserAuthToken, storeUserAuthToken, logger }, - clients: { identity, vtexId }, - } = context - - if (!adminUserAuthToken && !storeUserAuthToken) { - throw new AuthenticationError('No admin or store token was provided') - } - - if (adminUserAuthToken) { - try { - await identity.validateToken({ token: adminUserAuthToken }) - } catch (err) { - logger.warn({ - error: err, - message: 'CheckUserAccess: Invalid admin token', - token: adminUserAuthToken, - }) - 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: adminUserAuthToken, - }) - authUser = null - } - - if (!authUser) { - throw new ForbiddenError('Unauthorized Access') - } - } + await checkUserOrAdminTokenAccess(context, field.astNode?.name?.value) return resolve(root, args, context, info) } diff --git a/node/directives/index.ts b/node/directives/index.ts index 35b6665..ceaea69 100644 --- a/node/directives/index.ts +++ b/node/directives/index.ts @@ -4,8 +4,10 @@ import { WithUserPermissions } from './withUserPermissions' import { CheckAdminAccess } from './checkAdminAccess' import { CheckUserAccess } from './checkUserAccess' import { AuditAccess } from './auditAccess' +import { CheckAccessWithFeatureFlag } from './checkAccessWithFeatureFlag' export const schemaDirectives = { + checkAccessWithFeatureFlag: CheckAccessWithFeatureFlag as any, checkAdminAccess: CheckAdminAccess as any, checkUserAccess: CheckUserAccess as any, withSession: WithSession, From d8055a72ddb5197893cde1dfa3f80bcbc0bafcb8 Mon Sep 17 00:00:00 2001 From: Rudge Date: Mon, 27 Nov 2023 15:05:37 -0300 Subject: [PATCH 2/2] fix: adjust check user logs. - remove token from field and operation from message --- node/directives/checkUserAccess.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/node/directives/checkUserAccess.ts b/node/directives/checkUserAccess.ts index e32ca34..b9d49d0 100644 --- a/node/directives/checkUserAccess.ts +++ b/node/directives/checkUserAccess.ts @@ -14,7 +14,7 @@ export async function checkUserOrAdminTokenAccess( if (!adminUserAuthToken && !storeUserAuthToken) { logger.warn({ - message: `CheckUserAccess: No admin or store token was provided for ${operation}`, + message: `CheckUserAccess: No admin or store token was provided`, operation, }) throw new AuthenticationError('No admin or store token was provided') @@ -26,9 +26,8 @@ export async function checkUserOrAdminTokenAccess( } catch (err) { logger.warn({ error: err, - message: `CheckUserAccess: Invalid admin token for ${operation}`, + message: `CheckUserAccess: Invalid admin token`, operation, - token: adminUserAuthToken, }) throw new ForbiddenError('Unauthorized Access') } @@ -39,7 +38,7 @@ export async function checkUserOrAdminTokenAccess( authUser = await vtexId.getAuthenticatedUser(storeUserAuthToken) if (!authUser?.user) { logger.warn({ - message: `CheckUserAccess: No valid user found by store user token for ${operation}`, + message: `CheckUserAccess: No valid user found by store user token`, operation, }) authUser = null @@ -47,9 +46,8 @@ export async function checkUserOrAdminTokenAccess( } catch (err) { logger.warn({ error: err, - message: `CheckUserAccess: Invalid store user token for ${operation}`, + message: `CheckUserAccess: Invalid store user token`, operation, - token: adminUserAuthToken, }) authUser = null }