From 300f5095cd247965d99627d9a5530020a058e8c4 Mon Sep 17 00:00:00 2001 From: Rudge Date: Mon, 30 Oct 2023 22:46:16 -0300 Subject: [PATCH 1/8] feat: add audit metrics for authenticated access jira: B2BTEAM-1433 --- CHANGELOG.md | 4 +++ graphql/directives.graphql | 1 + graphql/schema.graphql | 6 ++-- node/clients/metrics.ts | 16 +++++++++ node/directives/auditAccess.ts | 62 ++++++++++++++++++++++++++++++++++ node/directives/index.ts | 2 ++ node/metrics/auth.ts | 42 +++++++++++++++++++++++ 7 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 node/clients/metrics.ts create mode 100644 node/directives/auditAccess.ts create mode 100644 node/metrics/auth.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ac726f2..ff48150 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 an authentication metric to check if the access is authenticated + ## [1.36.0] - 2023-08-09 ### Added diff --git a/graphql/directives.graphql b/graphql/directives.graphql index 6ac7146..7a3de32 100644 --- a/graphql/directives.graphql +++ b/graphql/directives.graphql @@ -3,3 +3,4 @@ directive @checkAdminAccess on FIELD | FIELD_DEFINITION directive @withSession on FIELD_DEFINITION directive @withSender on FIELD_DEFINITION directive @withUserPermissions on FIELD_DEFINITION +directive @auditAccess on FIELD | FIELD_DEFINITION diff --git a/graphql/schema.graphql b/graphql/schema.graphql index a35c59a..e36f0bd 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -11,6 +11,7 @@ type Query { @cacheControl(scope: PRIVATE, maxAge: SHORT) @settings(settingsType: "workspace") @withSession + @auditAccess getFeaturesByModule(module: String!): Feature @settings(settingsType: "workspace") @@ -23,9 +24,9 @@ type Query { getUser(id: ID!): User @cacheControl(scope: PRIVATE) getB2BUser(id: ID!): User @cacheControl(scope: PRIVATE) - checkCustomerSchema: Boolean @cacheControl(scope: PRIVATE) + checkCustomerSchema: Boolean @cacheControl(scope: PRIVATE) @auditAccess - getUserByEmail(email: String!): [User] @cacheControl(scope: PRIVATE) + getUserByEmail(email: String!): [User] @cacheControl(scope: PRIVATE) @auditAccess listAllUsers: [User] @cacheControl(scope: PRIVATE, maxAge: SHORT) @@ -140,6 +141,7 @@ type Mutation { setCurrentOrganization(orgId: ID!, costId: ID!): MutationResponse @withSession @cacheControl(scope: PRIVATE) + @auditAccess } type UserImpersonation { diff --git a/node/clients/metrics.ts b/node/clients/metrics.ts new file mode 100644 index 0000000..df2d8ec --- /dev/null +++ b/node/clients/metrics.ts @@ -0,0 +1,16 @@ +import axios from 'axios' + +const ANALYTICS_URL = 'https://rc.vtex.com/api/analytics/schemaless-events' + +export const B2B_METRIC_NAME = 'b2b-suite-buyerorg-data' + +export interface Metric { + readonly account: string + readonly kind: string + readonly description: string + readonly name: typeof B2B_METRIC_NAME +} + +export const sendMetric = async (metric: Metric) => { + await axios.post(ANALYTICS_URL, metric) +} diff --git a/node/directives/auditAccess.ts b/node/directives/auditAccess.ts new file mode 100644 index 0000000..ee50427 --- /dev/null +++ b/node/directives/auditAccess.ts @@ -0,0 +1,62 @@ +import type { GraphQLField } from 'graphql' +import { defaultFieldResolver } from 'graphql' +import { SchemaDirectiveVisitor } from 'graphql-tools' +import sendAuthMetric, {AuthMetric} from "../metrics/auth"; +import {checkUserPermission} from "../resolvers/Queries/Users"; + + +export class AuditAccess extends SchemaDirectiveVisitor { + public visitFieldDefinition(field: GraphQLField) { + const { resolve = defaultFieldResolver } = field + + field.resolve = async ( + root: any, + args: any, + context: Context, + info: any + ) => { + const { + vtex: { adminUserAuthToken, storeUserAuthToken, account, logger }, + request, + } = context + + const operation = field.astNode?.name?.value ?? request.url + const forwardedHost = request.headers['x-forwarded-host'] as string + const caller = request.headers['x-vtex-caller'] as string + + const hasAdminToken = + !!(adminUserAuthToken ?? + (context?.headers.vtexidclientautcookie as string)) + + const hasStoreToken = !!storeUserAuthToken + const hasApiToken = !!request.headers['vtex-api-apptoken'] + + let role + let permissions = [] + + if (hasAdminToken || hasStoreToken) { + const checkUserPermissions = await checkUserPermission(null, + { skipError: true }, + context + ) + role = checkUserPermissions.role.slug + permissions = checkUserPermissions.permissions + } + + const authMetric = new AuthMetric(account, { + caller, + forwardedHost, + hasAdminToken, + hasApiToken, + hasStoreToken, + operation, + permissions, + role, + }) + + sendAuthMetric(logger, authMetric) + + return resolve(root, args, context, info) + } + } +} diff --git a/node/directives/index.ts b/node/directives/index.ts index 9d7f577..473fa47 100644 --- a/node/directives/index.ts +++ b/node/directives/index.ts @@ -3,6 +3,7 @@ import { WithSender } from './withSender' import { WithUserPermissions } from './withUserPermissions' import { CheckAdminAccess } from './checkAdminAccess' import { CheckUserAccess } from './checkUserAccess' +import {AuditAccess} from "./auditAccess"; export const schemaDirectives = { checkAdminAccess: CheckAdminAccess as any, @@ -10,4 +11,5 @@ export const schemaDirectives = { withSession: WithSession, withSender: WithSender, withUserPermissions: WithUserPermissions, + auditAccess: AuditAccess as any, } diff --git a/node/metrics/auth.ts b/node/metrics/auth.ts new file mode 100644 index 0000000..c0589f8 --- /dev/null +++ b/node/metrics/auth.ts @@ -0,0 +1,42 @@ +import type { Logger } from '@vtex/api/lib/service/logger/logger' +import {B2B_METRIC_NAME, Metric, sendMetric} from "../clients/metrics"; + + +export interface AuthAuditMetric { + operation: string + forwardedHost: string + caller: string + role: string + permissions: string[] + hasAdminToken: boolean + hasStoreToken: boolean + hasApiToken: boolean +} + +export class AuthMetric implements Metric { + public readonly description: string + public readonly kind: string + public readonly account: string + public readonly fields: AuthAuditMetric + public readonly name = B2B_METRIC_NAME + + constructor(account: string, fields: AuthAuditMetric) { + this.account = account + this.fields = fields + this.kind = 'b2b-storefront-permissions-auth-event' + this.description = 'Auth metric event' + } +} + +const sendAuthMetric = async (logger: Logger, authMetric: AuthMetric) => { + try { + await sendMetric(authMetric) + } catch (error) { + logger.error({ + error, + message: `Error to send metrics from auth metric`, + }) + } +} + +export default sendAuthMetric From 353878fcc8bad85c932adcbd2d8228bbf361dae0 Mon Sep 17 00:00:00 2001 From: Rudge Date: Mon, 30 Oct 2023 22:53:06 -0300 Subject: [PATCH 2/8] style: fix lint errors --- node/directives/auditAccess.ts | 16 +++++++++------- node/directives/index.ts | 2 +- node/metrics/auth.ts | 3 ++- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/node/directives/auditAccess.ts b/node/directives/auditAccess.ts index ee50427..88c9043 100644 --- a/node/directives/auditAccess.ts +++ b/node/directives/auditAccess.ts @@ -1,9 +1,9 @@ import type { GraphQLField } from 'graphql' import { defaultFieldResolver } from 'graphql' import { SchemaDirectiveVisitor } from 'graphql-tools' -import sendAuthMetric, {AuthMetric} from "../metrics/auth"; -import {checkUserPermission} from "../resolvers/Queries/Users"; +import sendAuthMetric, { AuthMetric } from '../metrics/auth' +import { checkUserPermission } from '../resolvers/Queries/Users' export class AuditAccess extends SchemaDirectiveVisitor { public visitFieldDefinition(field: GraphQLField) { @@ -24,9 +24,9 @@ export class AuditAccess extends SchemaDirectiveVisitor { const forwardedHost = request.headers['x-forwarded-host'] as string const caller = request.headers['x-vtex-caller'] as string - const hasAdminToken = - !!(adminUserAuthToken ?? - (context?.headers.vtexidclientautcookie as string)) + const hasAdminToken = !!( + adminUserAuthToken ?? (context?.headers.vtexidclientautcookie as string) + ) const hasStoreToken = !!storeUserAuthToken const hasApiToken = !!request.headers['vtex-api-apptoken'] @@ -35,10 +35,12 @@ export class AuditAccess extends SchemaDirectiveVisitor { let permissions = [] if (hasAdminToken || hasStoreToken) { - const checkUserPermissions = await checkUserPermission(null, + const checkUserPermissions = await checkUserPermission( + null, { skipError: true }, context - ) + ) + role = checkUserPermissions.role.slug permissions = checkUserPermissions.permissions } diff --git a/node/directives/index.ts b/node/directives/index.ts index 473fa47..35b6665 100644 --- a/node/directives/index.ts +++ b/node/directives/index.ts @@ -3,7 +3,7 @@ import { WithSender } from './withSender' import { WithUserPermissions } from './withUserPermissions' import { CheckAdminAccess } from './checkAdminAccess' import { CheckUserAccess } from './checkUserAccess' -import {AuditAccess} from "./auditAccess"; +import { AuditAccess } from './auditAccess' export const schemaDirectives = { checkAdminAccess: CheckAdminAccess as any, diff --git a/node/metrics/auth.ts b/node/metrics/auth.ts index c0589f8..837cc13 100644 --- a/node/metrics/auth.ts +++ b/node/metrics/auth.ts @@ -1,6 +1,7 @@ import type { Logger } from '@vtex/api/lib/service/logger/logger' -import {B2B_METRIC_NAME, Metric, sendMetric} from "../clients/metrics"; +import type { Metric } from '../clients/metrics' +import { B2B_METRIC_NAME, sendMetric } from '../clients/metrics' export interface AuthAuditMetric { operation: string From ba52796b20687224800a947b39988848f4dcd4f9 Mon Sep 17 00:00:00 2001 From: Rudge Date: Tue, 31 Oct 2023 17:56:30 -0300 Subject: [PATCH 3/8] refactor: metric method --- node/utils/metrics/changeTeam.ts | 24 +++++++++++++++++------- node/utils/metrics/metrics.ts | 21 --------------------- 2 files changed, 17 insertions(+), 28 deletions(-) delete mode 100644 node/utils/metrics/metrics.ts diff --git a/node/utils/metrics/changeTeam.ts b/node/utils/metrics/changeTeam.ts index f8a04d0..0e7ed7a 100644 --- a/node/utils/metrics/changeTeam.ts +++ b/node/utils/metrics/changeTeam.ts @@ -1,5 +1,5 @@ -import type { Metric } from './metrics' -import { sendMetric } from './metrics' +import type { Metric } from '../../clients/metrics' +import { B2B_METRIC_NAME, sendMetric } from '../../clients/metrics' type ChangeTeamFieldsMetric = { date: string @@ -10,7 +10,20 @@ type ChangeTeamFieldsMetric = { new_cost_center_id: string } -type ChangeTeamMetric = Metric & { fields: ChangeTeamFieldsMetric } +export class ChangeTeamMetric implements Metric { + public readonly description: string + public readonly kind: string + public readonly account: string + public readonly fields: ChangeTeamFieldsMetric + public readonly name = B2B_METRIC_NAME + + constructor(account: string, fields: ChangeTeamFieldsMetric) { + this.account = account + this.fields = fields + this.kind = 'change-team-graphql-event' + this.description = 'User change team/organization - Graphql' + } +} export type ChangeTeamParams = { account: string @@ -23,10 +36,7 @@ export type ChangeTeamParams = { const buildMetric = (metricParams: ChangeTeamParams): ChangeTeamMetric => { return { - name: 'b2b-suite-buyerorg-data' as const, account: metricParams.account, - kind: 'change-team-graphql-event', - description: 'User change team/organization - Graphql', fields: { date: new Date().toISOString(), user_id: metricParams.userId, @@ -35,7 +45,7 @@ const buildMetric = (metricParams: ChangeTeamParams): ChangeTeamMetric => { new_org_id: metricParams.orgId, new_cost_center_id: metricParams.costCenterId, }, - } + } as ChangeTeamMetric } export const sendChangeTeamMetric = async (metricParams: ChangeTeamParams) => { diff --git a/node/utils/metrics/metrics.ts b/node/utils/metrics/metrics.ts deleted file mode 100644 index 07c4e32..0000000 --- a/node/utils/metrics/metrics.ts +++ /dev/null @@ -1,21 +0,0 @@ -import axios from 'axios' - -const ANALYTICS_URL = 'https://rc.vtex.com/api/analytics/schemaless-events' - -export type ChangeTeamMetric = { - kind: 'change-team-graphql-event' - description: 'User change team/organization - Graphql' -} - -export type Metric = { - name: 'b2b-suite-buyerorg-data' - account: string -} & ChangeTeamMetric - -export const sendMetric = async (metric: Metric) => { - try { - await axios.post(ANALYTICS_URL, metric) - } catch (error) { - console.warn('Unable to log metrics', error) - } -} From f7b6f787ee37a4a4f2804aa1c15cf0b5b8772f38 Mon Sep 17 00:00:00 2001 From: Rudge Date: Tue, 31 Oct 2023 19:11:56 -0300 Subject: [PATCH 4/8] feat: add audit access directive --- graphql/schema.graphql | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/graphql/schema.graphql b/graphql/schema.graphql index e36f0bd..d54ea48 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -2,6 +2,7 @@ type Query { getAppSettings: SettingsResponse @cacheControl(scope: PRIVATE) @settings(settingsType: "workspace") + @auditAccess getRole(id: ID!): Role @cacheControl(scope: PRIVATE, maxAge: SHORT) @@ -11,7 +12,7 @@ type Query { @cacheControl(scope: PRIVATE, maxAge: SHORT) @settings(settingsType: "workspace") @withSession - @auditAccess + @auditAccess getFeaturesByModule(module: String!): Feature @settings(settingsType: "workspace") @@ -20,15 +21,18 @@ type Query { listFeatures: [FullFeature] @settings(settingsType: "workspace") @cacheControl(scope: PRIVATE, maxAge: SHORT) + @auditAccess getUser(id: ID!): User @cacheControl(scope: PRIVATE) getB2BUser(id: ID!): User @cacheControl(scope: PRIVATE) checkCustomerSchema: Boolean @cacheControl(scope: PRIVATE) @auditAccess - getUserByEmail(email: String!): [User] @cacheControl(scope: PRIVATE) @auditAccess + getUserByEmail(email: String!): [User] + @cacheControl(scope: PRIVATE) + @auditAccess - listAllUsers: [User] @cacheControl(scope: PRIVATE, maxAge: SHORT) + listAllUsers: [User] @cacheControl(scope: PRIVATE, maxAge: SHORT) @auditAccess listUsers(organizationId: ID, costCenterId: ID, roleId: ID): [User] @cacheControl(scope: PRIVATE, maxAge: SHORT) @@ -76,9 +80,11 @@ type Mutation { name: String! slug: String features: [FeatureInput] - ): MutationResponse @cacheControl(scope: PRIVATE) + ): MutationResponse @cacheControl(scope: PRIVATE) @auditAccess - deleteRole(id: ID!): MutationResponse @cacheControl(scope: PRIVATE) + deleteRole(id: ID!): MutationResponse + @cacheControl(scope: PRIVATE) + @auditAccess saveUser( id: ID From 9d8b2815c36b4334d9876f1d0a9e7ae0b607e3b8 Mon Sep 17 00:00:00 2001 From: Rudge Date: Tue, 31 Oct 2023 19:23:22 -0300 Subject: [PATCH 5/8] fix: use getUserPermission with async --- node/directives/auditAccess.ts | 79 ++++++++++++++++++---------------- node/metrics/auth.ts | 4 +- 2 files changed, 45 insertions(+), 38 deletions(-) diff --git a/node/directives/auditAccess.ts b/node/directives/auditAccess.ts index 88c9043..09dce94 100644 --- a/node/directives/auditAccess.ts +++ b/node/directives/auditAccess.ts @@ -15,50 +15,57 @@ export class AuditAccess extends SchemaDirectiveVisitor { context: Context, info: any ) => { - const { - vtex: { adminUserAuthToken, storeUserAuthToken, account, logger }, - request, - } = context + this.sendAuthMetric(field, context) - const operation = field.astNode?.name?.value ?? request.url - const forwardedHost = request.headers['x-forwarded-host'] as string - const caller = request.headers['x-vtex-caller'] as string - - const hasAdminToken = !!( - adminUserAuthToken ?? (context?.headers.vtexidclientautcookie as string) - ) + return resolve(root, args, context, info) + } + } - const hasStoreToken = !!storeUserAuthToken - const hasApiToken = !!request.headers['vtex-api-apptoken'] + private async sendAuthMetric( + field: GraphQLField, + context: Context + ) { + const { + vtex: { adminUserAuthToken, storeUserAuthToken, account, logger }, + request, + } = context - let role - let permissions = [] + const operation = field.astNode?.name?.value ?? request.url + const forwardedHost = request.headers['x-forwarded-host'] as string + const caller = request.headers['x-vtex-caller'] as string - if (hasAdminToken || hasStoreToken) { - const checkUserPermissions = await checkUserPermission( - null, - { skipError: true }, - context - ) + const hasAdminToken = !!( + adminUserAuthToken ?? (context?.headers.vtexidclientautcookie as string) + ) - role = checkUserPermissions.role.slug - permissions = checkUserPermissions.permissions - } + const hasStoreToken = !!storeUserAuthToken + const hasApiToken = !!request.headers['vtex-api-apptoken'] - const authMetric = new AuthMetric(account, { - caller, - forwardedHost, - hasAdminToken, - hasApiToken, - hasStoreToken, - operation, - permissions, - role, - }) + let role + let permissions - sendAuthMetric(logger, authMetric) + if (hasAdminToken || hasStoreToken) { + const userPermissions = await checkUserPermission( + null, + { skipError: true }, + context + ) - return resolve(root, args, context, info) + role = userPermissions?.role?.slug + permissions = userPermissions?.permissions } + + const authMetric = new AuthMetric(account, { + caller, + forwardedHost, + hasAdminToken, + hasApiToken, + hasStoreToken, + operation, + permissions, + role, + }) + + await sendAuthMetric(logger, authMetric) } } diff --git a/node/metrics/auth.ts b/node/metrics/auth.ts index 837cc13..1a115df 100644 --- a/node/metrics/auth.ts +++ b/node/metrics/auth.ts @@ -7,8 +7,8 @@ export interface AuthAuditMetric { operation: string forwardedHost: string caller: string - role: string - permissions: string[] + role?: string + permissions?: string[] hasAdminToken: boolean hasStoreToken: boolean hasApiToken: boolean From 5b7e3f646b589ba74b008e2225e2785671b4c550 Mon Sep 17 00:00:00 2001 From: Rudge Date: Wed, 1 Nov 2023 10:18:58 -0300 Subject: [PATCH 6/8] feat: get sender from graphql to use in metric --- node/directives/auditAccess.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/node/directives/auditAccess.ts b/node/directives/auditAccess.ts index 09dce94..b6a6c8f 100644 --- a/node/directives/auditAccess.ts +++ b/node/directives/auditAccess.ts @@ -21,10 +21,7 @@ export class AuditAccess extends SchemaDirectiveVisitor { } } - private async sendAuthMetric( - field: GraphQLField, - context: Context - ) { + private async sendAuthMetric(field: GraphQLField, context: any) { const { vtex: { adminUserAuthToken, storeUserAuthToken, account, logger }, request, @@ -32,7 +29,8 @@ export class AuditAccess extends SchemaDirectiveVisitor { const operation = field.astNode?.name?.value ?? request.url const forwardedHost = request.headers['x-forwarded-host'] as string - const caller = request.headers['x-vtex-caller'] as string + const caller = + context.vtex.sender ?? (request.headers['x-vtex-caller'] as string) const hasAdminToken = !!( adminUserAuthToken ?? (context?.headers.vtexidclientautcookie as string) From c7bf20febf5b95a4b68d54edd57334b7489f0862 Mon Sep 17 00:00:00 2001 From: Rudge Date: Wed, 1 Nov 2023 15:39:02 -0300 Subject: [PATCH 7/8] fix: adjust the class instance for metrics --- node/utils/metrics/changeTeam.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/node/utils/metrics/changeTeam.ts b/node/utils/metrics/changeTeam.ts index 0e7ed7a..553fba9 100644 --- a/node/utils/metrics/changeTeam.ts +++ b/node/utils/metrics/changeTeam.ts @@ -35,17 +35,14 @@ export type ChangeTeamParams = { } const buildMetric = (metricParams: ChangeTeamParams): ChangeTeamMetric => { - return { - account: metricParams.account, - fields: { - date: new Date().toISOString(), - user_id: metricParams.userId, - user_role: metricParams.userRole, - user_email: metricParams.userEmail, - new_org_id: metricParams.orgId, - new_cost_center_id: metricParams.costCenterId, - }, - } as ChangeTeamMetric + return new ChangeTeamMetric(metricParams.account, { + date: new Date().toISOString(), + user_id: metricParams.userId, + user_role: metricParams.userRole, + user_email: metricParams.userEmail, + new_org_id: metricParams.orgId, + new_cost_center_id: metricParams.costCenterId, + }) } export const sendChangeTeamMetric = async (metricParams: ChangeTeamParams) => { From 4b0740a89a669ce259046dc5bd6ef8f45aae0f9c Mon Sep 17 00:00:00 2001 From: Rudge Date: Mon, 6 Nov 2023 12:29:09 -0300 Subject: [PATCH 8/8] feat: add directive withSender for operations with auditAccess --- graphql/schema.graphql | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/graphql/schema.graphql b/graphql/schema.graphql index d54ea48..bc4c7fd 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -2,6 +2,7 @@ type Query { getAppSettings: SettingsResponse @cacheControl(scope: PRIVATE) @settings(settingsType: "workspace") + @withSender @auditAccess getRole(id: ID!): Role @cacheControl(scope: PRIVATE, maxAge: SHORT) @@ -12,6 +13,7 @@ type Query { @cacheControl(scope: PRIVATE, maxAge: SHORT) @settings(settingsType: "workspace") @withSession + @withSender @auditAccess getFeaturesByModule(module: String!): Feature @@ -21,18 +23,26 @@ type Query { listFeatures: [FullFeature] @settings(settingsType: "workspace") @cacheControl(scope: PRIVATE, maxAge: SHORT) + @withSender @auditAccess getUser(id: ID!): User @cacheControl(scope: PRIVATE) getB2BUser(id: ID!): User @cacheControl(scope: PRIVATE) - checkCustomerSchema: Boolean @cacheControl(scope: PRIVATE) @auditAccess + checkCustomerSchema: Boolean + @cacheControl(scope: PRIVATE) + @withSender + @auditAccess getUserByEmail(email: String!): [User] @cacheControl(scope: PRIVATE) + @withSender @auditAccess - listAllUsers: [User] @cacheControl(scope: PRIVATE, maxAge: SHORT) @auditAccess + listAllUsers: [User] + @cacheControl(scope: PRIVATE, maxAge: SHORT) + @withSender + @auditAccess listUsers(organizationId: ID, costCenterId: ID, roleId: ID): [User] @cacheControl(scope: PRIVATE, maxAge: SHORT) @@ -80,10 +90,11 @@ type Mutation { name: String! slug: String features: [FeatureInput] - ): MutationResponse @cacheControl(scope: PRIVATE) @auditAccess + ): MutationResponse @cacheControl(scope: PRIVATE) @withSender @auditAccess deleteRole(id: ID!): MutationResponse @cacheControl(scope: PRIVATE) + @withSender @auditAccess saveUser( @@ -147,6 +158,7 @@ type Mutation { setCurrentOrganization(orgId: ID!, costId: ID!): MutationResponse @withSession @cacheControl(scope: PRIVATE) + @withSender @auditAccess }