diff --git a/.changeset/tricky-wolves-allow.md b/.changeset/tricky-wolves-allow.md new file mode 100644 index 00000000..ad953932 --- /dev/null +++ b/.changeset/tricky-wolves-allow.md @@ -0,0 +1,5 @@ +--- +"@pagopa/io-session-manager": minor +--- + +Added appinsights event tracking on lollipop operations diff --git a/apps/session-manager/src/app.ts b/apps/session-manager/src/app.ts index e5079124..94a56570 100644 --- a/apps/session-manager/src/app.ts +++ b/apps/session-manager/src/app.ts @@ -246,6 +246,7 @@ export const newApp: ( expressLollipopMiddleware( APIClients.fnLollipopAPIClient, REDIS_CLIENT_SELECTOR, + appInsightsClient, ), pipe( toExpressHandler({ diff --git a/apps/session-manager/src/controllers/authentication.ts b/apps/session-manager/src/controllers/authentication.ts index 373fe252..00baf8ad 100644 --- a/apps/session-manager/src/controllers/authentication.ts +++ b/apps/session-manager/src/controllers/authentication.ts @@ -78,6 +78,7 @@ import { getRequestIDFromResponse } from "../utils/spid"; import { AssertionRef } from "../generated/backend/AssertionRef"; import { CreateNewProfileDependencies } from "../services/profile"; import { AccessToken } from "../generated/public/AccessToken"; +import { AppInsightsDeps } from "../utils/appinsights"; import { SESSION_ID_LENGTH_BYTES, SESSION_TOKEN_LENGTH_BYTES } from "./session"; import { AuthenticationController } from "."; @@ -94,14 +95,14 @@ export type AcsDependencies = RedisRepo.RedisRepositoryDeps & FnLollipopRepo.LollipopApiDeps & RevokeAssertionRefDeps & CreateNewProfileDependencies & - NotificationsRepo.NotificationsueueDeps & { + NotificationsRepo.NotificationsueueDeps & + AppInsightsDeps & { isLollipopEnabled: boolean; getClientErrorRedirectionUrl: ( params: ClientErrorRedirectionUrlParams, ) => UrlFromString; getClientProfileRedirectionUrl: (token: string) => UrlFromString; // eslint-disable-next-line @typescript-eslint/no-explicit-any - appInsightsTelemetryClient?: any; allowedCieTestFiscalCodes: ReadonlyArray; hasUserAgeLimitEnabled: boolean; standardTokenDurationSecs: Second; diff --git a/apps/session-manager/src/controllers/pagopa.ts b/apps/session-manager/src/controllers/pagopa.ts index dd3330cc..ce55b825 100644 --- a/apps/session-manager/src/controllers/pagopa.ts +++ b/apps/session-manager/src/controllers/pagopa.ts @@ -15,7 +15,6 @@ import { FnAppAPIRepositoryDeps } from "../repositories/fn-app-api"; import { WithExpressRequest } from "../utils/express"; import { WithUser } from "../utils/user"; import { ProfileService, RedisSessionStorageService } from "../services"; -import { EmailAddress } from "../generated/backend/EmailAddress"; import { InitializedProfile } from "../generated/backend/InitializedProfile"; import { RedisRepositoryDeps } from "../repositories/redis"; import { PagoPAUser } from "../generated/pagopa/PagoPAUser"; diff --git a/apps/session-manager/src/services/lollipop.ts b/apps/session-manager/src/services/lollipop.ts index 903e1b16..f5bdb9ce 100644 --- a/apps/session-manager/src/services/lollipop.ts +++ b/apps/session-manager/src/services/lollipop.ts @@ -27,6 +27,7 @@ import { LcParams } from "../generated/lollipop-api/LcParams"; import { RedisRepositoryDeps } from "../repositories/redis"; import { ActivatedPubKey } from "../generated/lollipop-api/ActivatedPubKey"; import { AssertionTypeEnum } from "../generated/lollipop-api/AssertionType"; +import { AppInsightsDeps } from "../utils/appinsights"; import { RedisSessionStorageService } from "."; const LOLLIPOP_ERROR_EVENT_NAME = "lollipop.error.acs"; @@ -99,9 +100,8 @@ export const activateLolliPoPKey = ( fiscalCode: FiscalCode; assertion: NonEmptyString; getExpirePubKeyFn: IO; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - appInsightsTelemetryClient?: any; - } & GenerateLCParamsDeps, + } & GenerateLCParamsDeps & + AppInsightsDeps, ): TE.TaskEither => pipe( TE.tryCatch( @@ -165,10 +165,8 @@ export const deleteAssertionRefAssociation: ( eventMessage: string, ) => RTE.ReaderTaskEither< LollipopRevokeRepo.RevokeAssertionRefDeps & - RedisRepositoryDeps & { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - appInsightsTelemetryClient?: any; - }, + RedisRepositoryDeps & + AppInsightsDeps, Error, boolean > = (fiscalCode, assertionRefToRevoke, eventName, eventMessage) => (deps) => { diff --git a/apps/session-manager/src/utils/appinsights.ts b/apps/session-manager/src/utils/appinsights.ts index 0ac67772..90da9f6a 100644 --- a/apps/session-manager/src/utils/appinsights.ts +++ b/apps/session-manager/src/utils/appinsights.ts @@ -6,6 +6,10 @@ import { import { hashFiscalCode } from "@pagopa/ts-commons/lib/hash"; import { User } from "../types/user"; +export type AppInsightsDeps = { + appInsightsTelemetryClient?: appInsights.TelemetryClient; +}; + const SESSION_TRACKING_ID_KEY = "session_tracking_id"; const USER_TRACKING_ID_KEY = "user_tracking_id"; diff --git a/apps/session-manager/src/utils/lollipop.ts b/apps/session-manager/src/utils/lollipop.ts index 253ccc6c..eb5c8c79 100644 --- a/apps/session-manager/src/utils/lollipop.ts +++ b/apps/session-manager/src/utils/lollipop.ts @@ -34,9 +34,10 @@ import { import { NextFunction, Request, Response } from "express"; import { ulid } from "ulid"; import { Errors } from "io-ts"; +import { sha256 } from "@pagopa/io-functions-commons/dist/src/utils/crypto"; import { withValidatedOrValidationError } from "../utils/responses"; import { NewPubKey } from "../generated/lollipop-api/NewPubKey"; -import { FnLollipopRepo } from "../repositories"; +import { FnLollipopRepo, RedisRepo } from "../repositories"; import { JwkPubKeyHashAlgorithmEnum } from "../generated/lollipop-api/JwkPubKeyHashAlgorithm"; import { LollipopLocalsType, @@ -54,11 +55,11 @@ import { generateLCParams } from "../services/lollipop"; import { AssertionRefSha384 } from "../generated/lollipop-api/AssertionRefSha384"; import { AssertionRefSha512 } from "../generated/lollipop-api/AssertionRefSha512"; import { DomainErrorTypes } from "../models/domain-errors"; -import { RedisRepositoryDeps } from "../repositories/redis"; import { errorsToError } from "./errors"; import { ResLocals } from "./express"; import { withOptionalUserFromRequest } from "./user"; import { log } from "./logger"; +import { AppInsightsDeps } from "./appinsights"; const getLoginErrorEventName = "lollipop.error.get-login"; @@ -141,6 +142,7 @@ export const lollipopLoginHandler = properties: { message: `Error calling reservePubKey endpoint: ${error.message}`, }, + tagOverrides: { samplingEnabled: "false" }, }); return error; }, @@ -161,6 +163,7 @@ export const lollipopLoginHandler = properties: { message: `Error calling reservePubKey endpoint: ${e.message}`, }, + tagOverrides: { samplingEnabled: "false" }, }); return e; }, @@ -208,6 +211,7 @@ export const lollipopLoginMiddleware = appInsightsTelemetryClient, )(req).then((_) => (LollipopLoginParams.is(_) ? undefined : _)); +const LOLLIPOP_SIGN_ERROR_EVENT_NAME = "lollipop.error.sign"; const NONCE_REGEX = new RegExp(';?nonce="([^"]+)";?'); // Take the first occurrence of the field keyid into the signature-params const KEY_ID_REGEX = new RegExp(';?keyid="([^"]+)";?'); @@ -255,27 +259,33 @@ const getKeyThumbprintFromSignature = ( const getAndValidateAssertionRefForUser = ( fiscalCode: FiscalCode, - /* TODO: add event logging - * operationId: NonEmptyString, - */ + operationId: NonEmptyString, keyThumbprint: Thumbprint, ): RTE.ReaderTaskEither< - { redisClientSelector: RedisClientSelectorType }, + RedisRepo.RedisRepositoryDeps & AppInsightsDeps, IResponseErrorInternal | IResponseErrorForbiddenNotAuthorized, AssertionRef > => - ({ redisClientSelector }) => + ({ redisClientSelector, appInsightsTelemetryClient }) => pipe( RedisSessionStorageService.getLollipopAssertionRefForUser({ redisClientSelector, fiscalCode, }), - // TODO: send error event if taskEither results to left TE.mapLeft((err) => { log.error( "lollipopMiddleware|error reading the assertionRef from redis [%s]", err.message, ); + appInsightsTelemetryClient?.trackEvent({ + name: `An error occurs retrieving the assertion ref from Redis | ${err.message}`, + properties: { + fiscal_code: sha256(fiscalCode), + name: LOLLIPOP_SIGN_ERROR_EVENT_NAME, + operation_id: operationId, + }, + tagOverrides: { samplingEnabled: "false" }, + }); return ResponseErrorInternal("Error retrieving the assertionRef"); }), TE.chainW(TE.fromOption(() => ResponseErrorForbiddenNotAuthorized)), @@ -287,7 +297,18 @@ const getAndValidateAssertionRefForUser = `${getAlgoFromAssertionRef(assertionRef)}-${keyThumbprint}`, () => ResponseErrorForbiddenNotAuthorized, ), - // TODO: send error event if taskEither results to left + TE.mapLeft((error) => { + appInsightsTelemetryClient?.trackEvent({ + name: `AssertionRef is different from stored one`, + properties: { + fiscal_code: sha256(fiscalCode), + name: LOLLIPOP_SIGN_ERROR_EVENT_NAME, + operation_id: operationId, + }, + tagOverrides: { samplingEnabled: "false" }, + }); + return error; + }), ), ), ); @@ -333,47 +354,57 @@ export const extractLollipopLocalsFromLollipopHeaders = lollipopHeaders: LollipopRequiredHeaders, fiscalCode?: FiscalCode, ): RTE.ReaderTaskEither< - FnLollipopRepo.LollipopApiDeps & RedisRepositoryDeps, + FnLollipopRepo.LollipopApiDeps & + RedisRepo.RedisRepositoryDeps & + AppInsightsDeps, IResponseErrorInternal | IResponseErrorForbiddenNotAuthorized, LollipopLocalsType > => - ({ fnLollipopAPIClient, redisClientSelector }) => + ({ fnLollipopAPIClient, redisClientSelector, appInsightsTelemetryClient }) => pipe( TE.of(getNonceOrUlid(lollipopHeaders["signature-input"])), TE.bindTo("operationId"), - TE.bind("keyThumbprint", ({ operationId: _operationId }) => + TE.bind("keyThumbprint", ({ operationId }) => pipe( getKeyThumbprintFromSignature(lollipopHeaders["signature-input"]), - // TODO: send error event if either results to left - E.mapLeft(() => - ResponseErrorInternal("Invalid assertionRef in signature params"), - ), + E.mapLeft(() => { + appInsightsTelemetryClient?.trackEvent({ + name: "AssertionRef in signature-input is missing or invalid", + properties: { + fiscal_code: fiscalCode ? sha256(fiscalCode) : undefined, + name: LOLLIPOP_SIGN_ERROR_EVENT_NAME, + operation_id: operationId, + }, + tagOverrides: { samplingEnabled: "false" }, + }); + return ResponseErrorInternal( + "Invalid assertionRef in signature params", + ); + }), TE.fromEither, ), ), - TE.bind( - "assertionRefSet", - ({ keyThumbprint, operationId: _operationId }) => - pipe( - O.fromNullable(fiscalCode), - O.map((fc) => - pipe( - getAndValidateAssertionRefForUser( - fc, - /* operationId, */ - keyThumbprint, - )({ redisClientSelector }), - TE.map((assertionRef) => [assertionRef]), - ), - ), - O.getOrElse(() => - TE.of([ - `sha256-${keyThumbprint}` as AssertionRef, - `sha384-${keyThumbprint}` as AssertionRef, - `sha512-${keyThumbprint}` as AssertionRef, - ]), + TE.bind("assertionRefSet", ({ keyThumbprint, operationId }) => + pipe( + O.fromNullable(fiscalCode), + O.map((fc) => + pipe( + getAndValidateAssertionRefForUser( + fc, + operationId, + keyThumbprint, + )({ redisClientSelector, appInsightsTelemetryClient }), + TE.map((assertionRef) => [assertionRef]), ), ), + O.getOrElse(() => + TE.of([ + `sha256-${keyThumbprint}` as AssertionRef, + `sha384-${keyThumbprint}` as AssertionRef, + `sha512-${keyThumbprint}` as AssertionRef, + ]), + ), + ), ), TE.bindW("lcParams", ({ assertionRefSet, operationId }) => pipe( @@ -401,7 +432,7 @@ export const extractLollipopLocalsFromLollipopHeaders = ), ), ), - TE.chainFirst(({ operationId: _operationId, lcParams, keyThumbprint }) => + TE.chainFirst(({ operationId, lcParams, keyThumbprint }) => pipe( O.fromNullable(fiscalCode), O.map(() => TE.of(true)), @@ -409,9 +440,9 @@ export const extractLollipopLocalsFromLollipopHeaders = pipe( getAndValidateAssertionRefForUser( lcParams.fiscal_code, - /* operationId, */ + operationId, keyThumbprint, - )({ redisClientSelector }), + )({ redisClientSelector, appInsightsTelemetryClient }), TE.map(() => true), ), ), @@ -430,14 +461,26 @@ export const extractLollipopLocalsFromLollipopHeaders = ...lollipopHeaders, }) as LollipopLocalsType, ), - // TODO: info event of the sending data to the third party + TE.map((lcLocals) => { + appInsightsTelemetryClient?.trackEvent({ + name: "Lollipop locals to be sent to third party api", + properties: { + ...Object.keys(lcLocals), + name: "lollipop.locals.info", + }, + tagOverrides: { samplingEnabled: "false" }, + }); + return lcLocals; + }), ); export const expressLollipopMiddleware: ( lollipopApiClient: LollipopApiClient, redisClientSelector: RedisClientSelectorType, + appInsightsTelemetryClient?: appInsights.TelemetryClient, ) => (req: Request, res: Response, next: NextFunction) => Promise = - (fnLollipopAPIClient, redisClientSelector) => (req, res, next) => + (fnLollipopAPIClient, redisClientSelector, appInsightsTelemetryClient) => + (req, res, next) => pipe( TE.tryCatch( () => @@ -447,7 +490,11 @@ export const expressLollipopMiddleware: ( extractLollipopLocalsFromLollipopHeaders( lollipopHeaders, O.toUndefined(user)?.fiscal_code, - )({ fnLollipopAPIClient, redisClientSelector }), + )({ + fnLollipopAPIClient, + redisClientSelector, + appInsightsTelemetryClient, + }), TE.map((lollipopLocals) => { // eslint-disable-next-line functional/immutable-data res.locals = { ...res.locals, ...lollipopLocals };