Skip to content

Commit

Permalink
feat: add directive to validate auth token and feature flag
Browse files Browse the repository at this point in the history
jira: B2BTEAM-1484
  • Loading branch information
Rudge committed Nov 15, 2023
1 parent 7429754 commit 49e4711
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 46 deletions.
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

## [1.37.2] - 2023-11-10

### Fixed
Expand Down
1 change: 1 addition & 0 deletions graphql/directives.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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
38 changes: 33 additions & 5 deletions graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ type Query {
@settings(settingsType: "workspace")
@withSender
@auditAccess
@checkAccessWithFeatureFlag

getRole(id: ID!): Role @cacheControl(scope: PRIVATE, maxAge: SHORT)

Expand All @@ -15,6 +16,7 @@ type Query {
@withSession
@withSender
@auditAccess
@checkAccessWithFeatureFlag

getFeaturesByModule(module: String!): Feature
@settings(settingsType: "workspace")
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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")
Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -160,6 +187,7 @@ type Mutation {
@cacheControl(scope: PRIVATE)
@withSender
@auditAccess
@checkAccessWithFeatureFlag
}

type UserImpersonation {
Expand Down
34 changes: 34 additions & 0 deletions node/directives/checkAccessWithFeatureFlag.ts
Original file line number Diff line number Diff line change
@@ -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<any, any>) {
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)
}
}
}
99 changes: 58 additions & 41 deletions node/directives/checkUserAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any, any>) {
const { resolve = defaultFieldResolver } = field
Expand All @@ -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)
}
Expand Down
2 changes: 2 additions & 0 deletions node/directives/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 49e4711

Please sign in to comment.