Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#IOPID-2785] Move StoreSpidLogs to io-profile-async #230

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/thick-pets-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"io-profile-async": minor
"io-profile": minor
---

Move `StoreSpidLogs` function from `io-profile` to `io-profile-async`
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"bindings": [
{
"queueName": "spidmsgitems",
"connection": "LogsStorageConnection",
"connection": "IOPSTLOGS_STORAGE_CONNECTION_STRING",
"name": "spidMsgItem",
"type": "queueTrigger",
"direction": "in"
Expand All @@ -11,10 +11,10 @@
"type": "blob",
"name": "spidRequestResponse",
"path": "spidassertions/{fiscalCode}-{createdAtDay}-{spidRequestId}-{loginType}.json",
"connection": "LogsStorageConnection",
"connection": "IOPSTLOGS_STORAGE_CONNECTION_STRING",
"direction": "out"
}
],
"disabled": false,
"scriptFile": "../dist/StoreSpidLogs/index.js"
}
"scriptFile": "../dist/main.js",
"entryPoint": "StoreSpidLogs"
}
4 changes: 4 additions & 0 deletions apps/io-profile-async/env.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@ EXPIRED_SESSION_ADVISOR_QUEUE=test-expired-session-trigger-queue
IOPSTAPP_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://azurite:20003/devstoreaccount1;QueueEndpoint=http://azurite:20004/devstoreaccount1;TableEndpoint=http://azurite:20005/devstoreaccount1;
MIGRATE_SERVICES_PREFERENCES_PROFILE_QUEUE_NAME=test-profile-migrate-services-preferences-from-legacy

# StoreSpidLogs Config (NOTE: the key is the one used for testing)
IOPSTLOGS_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://azurite:20003/devstoreaccount1;QueueEndpoint=http://azurite:20004/devstoreaccount1;TableEndpoint=http://azurite:20005/devstoreaccount1;
SPID_LOGS_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC76C3tOj7lPiJ5sg2lU6j2dZEa\nE9GW+v1YrOajfCHijBo6VLSMH6nrO3fZM3C8oNsYrH8jyeZlcu8ZdKMaRECegVUv\nYwCyICrs58l1pA0qCo+o/0jWUaCWQY5SAX2eAni7PQGzSTQRu93Ac4BnI0PDvqxY\nQ1mRn/iy0NVMMxhDUwIDAQAB\n-----END PUBLIC KEY-----"

FF_DRY_RUN=false
1 change: 1 addition & 0 deletions apps/io-profile-async/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@types/node-fetch": "^2.6.2",
"@types/nodemailer": "^6.4.17",
"@vitest/coverage-v8": "~1.5.0",
"date-fns": "^2.30.0",
"dependency-check": "^4.1.0",
"dotenv": "^8.2.0",
"dotenv-cli": "^3.1.0",
Expand Down
4 changes: 4 additions & 0 deletions apps/io-profile-async/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ export const IConfig = t.intersection([
IOPSTAPP_STORAGE_CONNECTION_STRING: NonEmptyString,
MIGRATE_SERVICES_PREFERENCES_PROFILE_QUEUE_NAME: NonEmptyString,

// StoreSpidLogs Config
IOPSTLOGS_STORAGE_CONNECTION_STRING: NonEmptyString,
SPID_LOGS_PUBLIC_KEY: NonEmptyString,

isProduction: t.boolean
}),
BackendInternalConfig,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable functional/immutable-data */
import { IPString, NonEmptyString } from "@pagopa/ts-commons/lib/strings";
import { describe, expect, it, assert } from "vitest";

import * as E from "fp-ts/lib/Either";

import { format } from "date-fns";
import { TelemetryClient } from "applicationinsights";
import { toPlainText } from "@pagopa/ts-commons/lib/encrypt";
import { aFiscalCode } from "../../../../io-profile/src/__mocks__/mocks";
import { HandlerOutput, makeHandler } from "../store-spid-logs";
import { StoreSpidLogsQueueMessage } from "../../types/store-spid-logs-queue-message";

import { mockQueueHandlerInputMocks } from "../__mocks__/handlerMocks";
import { trackerMock } from "../__mocks__/tracker.mock";

const today = format(new Date(), "yyyy-MM-dd");
const aDate = new Date();

const aPublicKey = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC76C3tOj7lPiJ5sg2lU6j2dZEa
E9GW+v1YrOajfCHijBo6VLSMH6nrO3fZM3C8oNsYrH8jyeZlcu8ZdKMaRECegVUv
YwCyICrs58l1pA0qCo+o/0jWUaCWQY5SAX2eAni7PQGzSTQRu93Ac4BnI0PDvqxY
Q1mRn/iy0NVMMxhDUwIDAQAB
-----END PUBLIC KEY-----` as NonEmptyString;

const aRSAPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQC76C3tOj7lPiJ5sg2lU6j2dZEaE9GW+v1YrOajfCHijBo6VLSM
H6nrO3fZM3C8oNsYrH8jyeZlcu8ZdKMaRECegVUvYwCyICrs58l1pA0qCo+o/0jW
UaCWQY5SAX2eAni7PQGzSTQRu93Ac4BnI0PDvqxYQ1mRn/iy0NVMMxhDUwIDAQAB
AoGBAKmCGWwXTwWdt5vwcz7g6VrrU6oilr+MS17jGmwAXtDvcfmM0BJXvgDl9IeL
T/fZY8wuT8MJLz31IJvmC/x19ZN7gsnp/Hi5L3gouQMQFGwaDUsO10gqGEoJXJCp
kNS7PV8Zp1Z6aBg5zd4A0Hc271qOY5VUwKuT9zIzkDtHlq6BAkEA23J6jH3nEyom
4oV/oRMpX6xHm6tciFvpVXK/kZNl8rXVUj9BHbyDG59q8eZ0HcJVt8dTFGtwmxnU
Bs/v1DPlZQJBANs0yHV8wPr0Oj3dTVZ/zePMSYDXpJjWbbquM0kKcWAeyqiRC/hX
rPaum46QWn978zLCOWLvg2FroFGYzLaqNlcCQQC4ILT83sMdTHf2BweQ0nAbq4Ul
88GfVGdS4AYnEqMu5C0KZrKvTbZAXiGwuKnjMmUT37Yw4vlH2oMR+DUGO0kVAkBv
qJBfwD9w1Y0BTEQDxrAy1DGwzqeKLtfQGsIG96nOw4CJovDM/KQfN8wHL6LZg2Lb
PTIMImLy8ebFCadleIibAkBnlE+V7qUL+a/XBMazEffUVsMoPN9iv7G6UPUyOipN
hG0enuvF/PmgeLYsbFtozTm2/mQNqBV6USME39kPoKY1
-----END RSA PRIVATE KEY-----`;

const aSpidMsgItem: StoreSpidLogsQueueMessage = {
createdAt: aDate,
createdAtDay: today,
fiscalCode: aFiscalCode,
ip: "192.168.1.6" as IPString,
loginType: "LV" as NonEmptyString,
requestPayload:
"<?xml version='1.0' encoding='UTF-8'?><note ID='AAAA_BBBB'><to>Azure</to><from>Azure</from><heading>Reminder</heading><body>New append from local dev - REQUEST</body></note>",
responsePayload:
"<?xml version='1.0' encoding='UTF-8'?><note ID='AAAA_BBBB'><to>Azure</to><from>Azure</from><heading>Reminder</heading><body>New append from local dev - RESPONSE</body></note>",
spidRequestId: "AAAA_BBBB"
};

const anotherSpidMsgItem: StoreSpidLogsQueueMessage = {
createdAt: aDate,
createdAtDay: today,
fiscalCode: aFiscalCode,
ip: "192.168.1.7" as IPString,
loginType: "LEGACY" as NonEmptyString,
requestPayload:
"<?xml version='1.0' encoding='UTF-8'?><note ID='CCCC_DDDD'><to>Azure</to><from>Azure</from><heading>Reminder</heading><body>New append from local dev - REQUEST</body></note>",
responsePayload:
"<?xml version='1.0' encoding='UTF-8'?><note ID='CCCC_DDDD'><to>Azure</to><from>Azure</from><heading>Reminder</heading><body>New append from local dev - RESPONSE</body></note>",
spidRequestId: "CCCC_DDDD"
};

// --------------
// Mocks
// --------------

const mockedDependencies = {
...mockQueueHandlerInputMocks(StoreSpidLogsQueueMessage, aSpidMsgItem),
spidLogsPublicKey: aPublicKey,
tracker: trackerMock,
// Subdependencies, unused in this tests
telemetryClient: (null as any) as TelemetryClient
};

// --------------
// Tests
// --------------

describe("StoreSpidLogs", () => {
it("should store both SPID request/response published into the queue", async () => {
const handler = makeHandler({
...mockedDependencies,
input: { ...aSpidMsgItem }
});
const result = await handler();

assert(E.isRight(result));

if (E.isRight(result)) {
const blob = result.right as Exclude<HandlerOutput, void>;
const encryptedSpidBlobItem = blob.spidRequestResponse;

expect(blob).not.toHaveProperty("loginType");
expect(encryptedSpidBlobItem).not.toHaveProperty("loginType");

expect(encryptedSpidBlobItem).toMatchObject({
createdAt: aSpidMsgItem.createdAt,
ip: aSpidMsgItem.ip,
spidRequestId: aSpidMsgItem.spidRequestId
});

const decryptedRequestPayload = toPlainText(
aRSAPrivateKey,
encryptedSpidBlobItem.encryptedRequestPayload
);
const decryptedResponsePayload = toPlainText(
aRSAPrivateKey,
encryptedSpidBlobItem.encryptedResponsePayload
);

if (
E.isRight(decryptedRequestPayload) &&
E.isRight(decryptedResponsePayload)
) {
expect(decryptedRequestPayload.right).toEqual(
aSpidMsgItem.requestPayload
);
expect(decryptedResponsePayload.right).toEqual(
aSpidMsgItem.responsePayload
);
} else {
expect(true).toBeFalsy();
}
}
});

it("should encrypt two different messages with the same Cipher instance and decrypt with another one", async () => {
const handler = makeHandler({
...mockedDependencies,
input: { ...aSpidMsgItem }
});
const result = await handler();

assert(E.isRight(result));

if (E.isRight(result)) {
const blob = result.right as Exclude<HandlerOutput, void>;
const encryptedSpidBlobItem = blob.spidRequestResponse;
const decryptedRequestPayload = toPlainText(
aRSAPrivateKey,
encryptedSpidBlobItem.encryptedRequestPayload
);
const decryptedResponsePayload = toPlainText(
aRSAPrivateKey,
encryptedSpidBlobItem.encryptedResponsePayload
);

if (
E.isRight(decryptedRequestPayload) &&
E.isRight(decryptedResponsePayload)
) {
expect(decryptedRequestPayload.right).toEqual(
aSpidMsgItem.requestPayload
);
expect(decryptedResponsePayload.right).toEqual(
aSpidMsgItem.responsePayload
);
} else {
expect(true).toBeFalsy();
}
}

const handler2 = makeHandler({
...mockedDependencies,
input: { ...anotherSpidMsgItem }
});
const secondBlobItem = await handler2();

assert(E.isRight(secondBlobItem));
if (E.isRight(secondBlobItem)) {
const secondBlob = secondBlobItem.right as Exclude<HandlerOutput, void>;
const secondEncryptedSpidBlobItem = secondBlob.spidRequestResponse;

const secondDecryptedRequestPayload = toPlainText(
aRSAPrivateKey,
secondEncryptedSpidBlobItem.encryptedRequestPayload
);
const secondDecryptedResponsePayload = toPlainText(
aRSAPrivateKey,
secondEncryptedSpidBlobItem.encryptedResponsePayload
);

if (
E.isRight(secondDecryptedRequestPayload) &&
E.isRight(secondDecryptedResponsePayload)
) {
expect(secondDecryptedRequestPayload.right).toEqual(
anotherSpidMsgItem.requestPayload
);
expect(secondDecryptedResponsePayload.right).toEqual(
anotherSpidMsgItem.responsePayload
);
} else {
expect(true).toBeFalsy();
}
}
});
});
92 changes: 92 additions & 0 deletions apps/io-profile-async/src/functions/store-spid-logs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as TE from "fp-ts/lib/TaskEither";
import * as RTE from "fp-ts/lib/ReaderTaskEither";

import * as H from "@pagopa/handler-kit";
import { azureFunction } from "@pagopa/handler-kit-azure-func";

import {
EncryptedPayload,
toEncryptedPayload
} from "@pagopa/ts-commons/lib/encrypt";
import { NonEmptyString } from "@pagopa/ts-commons/lib/strings";

import { pipe } from "fp-ts/lib/function";

import * as t from "io-ts";

import { readableReport } from "@pagopa/ts-commons/lib/reporters";
import { sequenceS } from "fp-ts/lib/Apply";
import {
SpidBlobItem,
StoreSpidLogsQueueMessage
} from "../types/store-spid-logs-queue-message";
import { QueuePermanentError } from "../utils/queue-utils";
import { Tracker, TrackerRepositoryDependency } from "../repositories";

export type HandlerDependencies = {
tracker: Tracker;
spidLogsPublicKey: NonEmptyString;
} & TrackerRepositoryDependency;

export type HandlerOutput = void | {
spidRequestResponse: SpidBlobItem;
};

const encrypt: (
plainText: string
) => RTE.ReaderTaskEither<
HandlerDependencies,
QueuePermanentError,
EncryptedPayload
> = plainText => ({ spidLogsPublicKey }) =>
pipe(
toEncryptedPayload(spidLogsPublicKey, plainText),
TE.fromEither,
TE.mapLeft(e => new QueuePermanentError(`Cannot encrypt payload ${e}`))
);

export const makeHandler: H.Handler<
StoreSpidLogsQueueMessage,
HandlerOutput,
HandlerDependencies
> = H.of(queueInput => deps =>
pipe(
sequenceS(TE.ApplicativePar)({
encryptedRequestPayload: encrypt(queueInput.requestPayload)(deps),
encryptedResponsePayload: encrypt(queueInput.responsePayload)(deps)
}),
TE.map(item => ({
...queueInput,
...item
})),
TE.chain((encryptedBlobItem: SpidBlobItem) =>
pipe(
t.exact(SpidBlobItem).decode(encryptedBlobItem),
TE.fromEither,
TE.map(spidBlobItem => ({
spidRequestResponse: spidBlobItem
})),
TE.mapLeft(
errs =>
new QueuePermanentError(
`Cannot decode payload${readableReport(errs)}`
)
)
)
),
TE.orElseW(error => {
if (error instanceof QueuePermanentError) {
return TE.fromTask(
deps.tracker.trackEvent(
"io.citizen-auth.prof-async.store-spid-logs.error.permanent" as NonEmptyString,
error.message as NonEmptyString
)(deps)
);
} else {
return TE.left(error);
}
})
)
);

export const StoreSpidLogsFunction = azureFunction(makeHandler);
9 changes: 9 additions & 0 deletions apps/io-profile-async/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
} from "./functions/migrate-service-preference-from-legacy";
import { repository as servicePreferencesRepository } from "./repositories/service-preferences";
import { tracker } from "./repositories/tracker";
import { StoreSpidLogsFunction } from "./functions/store-spid-logs";
import { StoreSpidLogsQueueMessage } from "./types/store-spid-logs-queue-message";

const config = getConfigOrThrow();

Expand Down Expand Up @@ -74,3 +76,10 @@ export const MigrateServicePreferenceFromLegacy = MigrateServicePreferenceFromLe
telemetryClient
}
);

export const StoreSpidLogs = StoreSpidLogsFunction({
inputDecoder: StoreSpidLogsQueueMessage,
spidLogsPublicKey: config.SPID_LOGS_PUBLIC_KEY,
tracker,
telemetryClient
});
Loading
Loading