Skip to content

Commit

Permalink
Merge pull request #42 from vtex-apps/feature/B2BTEAM-1433-security-m…
Browse files Browse the repository at this point in the history
…etrics

feat: add audit metrics for authenticated access
  • Loading branch information
Rudge authored Nov 7, 2023
2 parents bba20f9 + 7031d81 commit c48dbdd
Show file tree
Hide file tree
Showing 11 changed files with 230 additions and 98 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](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- add an authentication metric to check if the access is authenticated

## [2.3.1] - 2023-09-13

### Fixed
Expand Down
1 change: 1 addition & 0 deletions graphql/directives.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ directive @withSession on FIELD | FIELD_DEFINITION
directive @withPermissions on FIELD | FIELD_DEFINITION
directive @withSegment on FIELD | FIELD_DEFINITION
directive @checkAdminAccess on FIELD | FIELD_DEFINITION
directive @auditAccess on FIELD | FIELD_DEFINITION
11 changes: 7 additions & 4 deletions graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ type Query {
}

type Mutation {
createQuote(input: QuoteInput!): String @withPermissions @withSession @withSegment
createQuote(input: QuoteInput!): String
@withPermissions
@withSession
@withSegment
updateQuote(input: QuoteUpdateInput!): String @withPermissions @withSession
useQuote(id: String, orderFormId: String): String
@withPermissions
@withSession
clearCart(orderFormId: String): String
saveAppSettings(input: AppSettingsInput!): AppSettings
@cacheControl(scope: PRIVATE)
clearCart(orderFormId: String): String @auditAccess
saveAppSettings(input: AppSettingsInput!): AppSettings
@cacheControl(scope: PRIVATE)
@checkAdminAccess
}
16 changes: 16 additions & 0 deletions node/clients/metrics.ts
Original file line number Diff line number Diff line change
@@ -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)
}
43 changes: 43 additions & 0 deletions node/metrics/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Logger } from '@vtex/api/lib/service/logger/logger'

import type { Metric } from '../clients/metrics'
import { B2B_METRIC_NAME, 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-quotes-graphql-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
47 changes: 26 additions & 21 deletions node/metrics/createQuote.ts
Original file line number Diff line number Diff line change
@@ -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 UserData = {
orgId: string
Expand Down Expand Up @@ -39,7 +39,20 @@ type CreateQuoteFieldsMetric = {
send_to_sales_rep: boolean
}

type CreateQuoteMetric = Metric & { fields: CreateQuoteFieldsMetric }
export class CreateQuoteMetric implements Metric {
public readonly description: string
public readonly kind: string
public readonly account: string
public readonly fields: CreateQuoteFieldsMetric
public readonly name = B2B_METRIC_NAME

constructor(account: string, fields: CreateQuoteFieldsMetric) {
this.account = account
this.fields = fields
this.kind = 'create-quote-graphql-event'
this.description = 'Create Quotation Action - Graphql'
}
}

const buildQuoteMetric = (
metricsParam: CreateQuoteMetricParam
Expand All @@ -48,24 +61,16 @@ const buildQuoteMetric = (
const accountName = namespaces?.account?.accountName?.value
const userEmail = namespaces?.profile?.email?.value

const metric: CreateQuoteMetric = {
name: 'b2b-suite-buyerorg-data',
kind: 'create-quote-graphql-event',
description: 'Create Quotation Action - Graphql',
account: accountName,
fields: {
buyer_org_id: metricsParam.userData?.orgId,
cost_center_id: metricsParam.userData?.costId,
member_email: userEmail,
role: metricsParam.userData?.roleId,
creation_date: metricsParam.creationDate,
quote_id: metricsParam.quoteId,
quote_reference_name: metricsParam.quoteReferenceName,
send_to_sales_rep: metricsParam.sendToSalesRep,
},
}

return metric
return new CreateQuoteMetric(accountName, {
buyer_org_id: metricsParam.userData?.orgId,
cost_center_id: metricsParam.userData?.costId,
member_email: userEmail,
role: metricsParam.userData?.roleId,
creation_date: metricsParam.creationDate,
quote_id: metricsParam.quoteId,
quote_reference_name: metricsParam.quoteReferenceName,
send_to_sales_rep: metricsParam.sendToSalesRep,
})
}

export const sendCreateQuoteMetric = async (
Expand Down
31 changes: 0 additions & 31 deletions node/metrics/metrics.ts

This file was deleted.

43 changes: 24 additions & 19 deletions node/metrics/sendMessage.ts
Original file line number Diff line number Diff line change
@@ -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 Quote = {
costCenter: string
Expand All @@ -23,27 +23,32 @@ type SendMessageFieldsMetric = {
sent_date: string
}

type SendMessageMetric = Metric & { fields: SendMessageFieldsMetric }
export class SendMessageMetric implements Metric {
public readonly description: string
public readonly kind: string
public readonly account: string
public readonly fields: SendMessageFieldsMetric
public readonly name = B2B_METRIC_NAME

constructor(account: string, fields: SendMessageFieldsMetric) {
this.account = account
this.fields = fields
this.kind = 'send-message-graphql-event'
this.description = 'Send Message Action - Graphql'
}
}

const buildSendMessageMetric = (
metricParam: SendMessageMetricParam
): SendMessageMetric => {
const metric: SendMessageMetric = {
name: 'b2b-suite-buyerorg-data',
kind: 'send-message-graphql-event',
description: 'Send Message Action - Graphql',
account: metricParam.account,
fields: {
buyer_org_name: metricParam.quote?.organization,
cost_center_name: metricParam.quote?.costCenter,
quote_id: metricParam.quote?.id,
template_name: metricParam.templateName,
sent_to: metricParam.sentTo,
sent_date: new Date().toISOString(),
},
}

return metric
return new SendMessageMetric(metricParam.account, {
buyer_org_name: metricParam.quote?.organization,
cost_center_name: metricParam.quote?.costCenter,
quote_id: metricParam.quote?.id,
template_name: metricParam.templateName,
sent_to: metricParam.sentTo,
sent_date: new Date().toISOString(),
})
}

export const sendMessageMetric = async (
Expand Down
51 changes: 28 additions & 23 deletions node/metrics/useQuote.ts
Original file line number Diff line number Diff line change
@@ -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 UseQuoteFieldsMetric = {
quote_id: string
Expand All @@ -14,7 +14,20 @@ type UseQuoteFieldsMetric = {
quote_last_update: string
}

type UseQuoteMetric = Metric & { fields: UseQuoteFieldsMetric }
export class UseQuoteMetric implements Metric {
public readonly description: string
public readonly kind: string
public readonly account: string
public readonly fields: UseQuoteFieldsMetric
public readonly name = B2B_METRIC_NAME

constructor(account: string, fields: UseQuoteFieldsMetric) {
this.account = account
this.fields = fields
this.kind = 'use-quote-graphql-event'
this.description = 'Use Quotation Action - Graphql'
}
}

export type UseQuoteMetricsParams = {
quote: Quote
Expand All @@ -28,26 +41,18 @@ const buildUseQuoteMetric = (
): UseQuoteMetric => {
const { quote, orderFormId, account, userEmail } = metricsParam

const metric: UseQuoteMetric = {
name: 'b2b-suite-buyerorg-data',
kind: 'use-quote-graphql-event',
description: 'Use Quotation Action - Graphql',
account,
fields: {
buyer_org_id: quote.organization,
cost_center_id: quote.costCenter,
quote_id: quote.id,
quote_reference_name: quote.referenceName,
order_form_id: orderFormId,
quote_creation_date: quote.creationDate,
quote_use_date: new Date().toISOString(),
creator_email: quote.creatorEmail,
user_email: userEmail,
quote_last_update: quote.lastUpdate,
},
}

return metric
return new UseQuoteMetric(account, {
buyer_org_id: quote.organization,
cost_center_id: quote.costCenter,
quote_id: quote.id,
quote_reference_name: quote.referenceName,
order_form_id: orderFormId,
quote_creation_date: quote.creationDate,
quote_use_date: new Date().toISOString(),
creator_email: quote.creatorEmail,
user_email: userEmail,
quote_last_update: quote.lastUpdate,
})
}

export const sendUseQuoteMetric = async (
Expand Down
2 changes: 2 additions & 0 deletions node/resolvers/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { WithPermissions } from './directives/withPermissions'
import { WithSession } from './directives/withSession'
import { WithSegment } from './directives/withSegment'
import { CheckAdminAccess } from './directives/checkAdminAccess'
import { AuditAccess } from './directives/auditAccess'

export const schemaDirectives = {
withPermissions: WithPermissions as any,
withSession: WithSession as any,
withSegment: WithSegment as any,
checkAdminAccess: CheckAdminAccess as any,
auditAccess: AuditAccess as any,
}
Loading

0 comments on commit c48dbdd

Please sign in to comment.