From b591f222e6da5762ab04bbdf2fdb107481bd495d Mon Sep 17 00:00:00 2001 From: fabriziopapi Date: Thu, 2 Dec 2021 12:34:05 +0100 Subject: [PATCH 01/11] [#IC-131] (+) add GetImpersonateUserData --- .../__tests__/handler.test.ts | 193 ++++++++++++++++++ GetImpersonateUserData/function.json | 20 ++ GetImpersonateUserData/handler.ts | 111 ++++++++++ GetImpersonateUserData/index.ts | 59 ++++++ utils/apim.ts | 59 +++++- 5 files changed, 441 insertions(+), 1 deletion(-) create mode 100644 GetImpersonateUserData/__tests__/handler.test.ts create mode 100644 GetImpersonateUserData/function.json create mode 100644 GetImpersonateUserData/handler.ts create mode 100644 GetImpersonateUserData/index.ts diff --git a/GetImpersonateUserData/__tests__/handler.test.ts b/GetImpersonateUserData/__tests__/handler.test.ts new file mode 100644 index 00000000..d0d14272 --- /dev/null +++ b/GetImpersonateUserData/__tests__/handler.test.ts @@ -0,0 +1,193 @@ +import { ApiManagementClient } from "@azure/arm-apimanagement"; +import { GroupContract } from "@azure/arm-apimanagement/esm/models"; +import * as TE from "fp-ts/lib/TaskEither"; +import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; +import * as ApimUtils from "../../utils/apim"; +import { IAzureApimConfig, IServicePrincipalCreds } from "../../utils/apim"; +import { GetImpersonateUserHandler } from "../handler"; +import { RestError } from "@azure/ms-rest-js"; + +jest.mock("@azure/arm-apimanagement"); +jest.mock("@azure/graph"); + +const fakeServicePrincipalCredentials: IServicePrincipalCreds = { + clientId: "client-id", + secret: "secret", + tenantId: "tenant-id" +}; + +const fakeApimConfig: IAzureApimConfig = { + apim: "apim", + apimResourceGroup: "resource group", + subscriptionId: "subscription id" +}; + +const fakeUserName = "a-non-empty-string"; + +const mockUserGroupList = jest.fn(); +const mockUserGroupListNext = jest.fn(); +const mockUserSubscriptionGet = jest.fn(); + +const aValidSubscriptionId = "valid-subscription-id" as NonEmptyString; +const aNotExistingSubscriptionId = "not-existing-subscription-id" as NonEmptyString; +const aBreakingApimSubscriptionId = "broken-subscription-id" as NonEmptyString; +const aValidSubscriptionIdWithouthOwner = "no-owner-subscription-id" as NonEmptyString; + +const mockedSubscription = { + ownerId: "/users/userId" +}; + +const mockedSubscriptionWithoutOwner = { + displayName: "without-woner", + ownerId: undefined +}; + +const mockApiManagementClient = ApiManagementClient as jest.Mock; + +mockApiManagementClient.mockImplementation(() => ({ + userGroup: { + list: mockUserGroupList, + listNext: mockUserGroupListNext + }, + subscription: { + get: mockUserSubscriptionGet + } +})); + +mockUserSubscriptionGet.mockImplementation((_, __, subscriptionId) => { + if (subscriptionId === aValidSubscriptionId) { + return Promise.resolve(mockedSubscription); + } + if (subscriptionId === aValidSubscriptionIdWithouthOwner) { + return Promise.resolve(mockedSubscriptionWithoutOwner); + } + if (subscriptionId === aBreakingApimSubscriptionId) { + return Promise.reject(new RestError("generic error", "", 500)); + } + if (subscriptionId === aNotExistingSubscriptionId) { + return Promise.reject(new RestError("not found", "", 404)); + } + return fail(Error("The provided subscription id value is not handled")); +}); + +const spyOnGetApiClient = jest.spyOn(ApimUtils, "getApiClient"); +spyOnGetApiClient.mockImplementation(() => + TE.of(new mockApiManagementClient()) +); + +const mockLog = jest.fn(); +const mockedContext = { log: { error: mockLog } }; + +// eslint-disable-next-line sonar/sonar-max-lines-per-function +describe("GetImpersonateUserHandler", () => { + it("GIVEN a not working APIM client WHEN call the handler THEN an Internel Error is returned", async () => { + spyOnGetApiClient.mockImplementationOnce(() => + TE.left(Error("Error from ApiManagementClient constructor")) + ); + + const getImpersonateUserHandler = GetImpersonateUserHandler( + fakeServicePrincipalCredentials, + fakeApimConfig + ); + + const response = await getImpersonateUserHandler( + mockedContext as any, + undefined, + undefined + ); + + expect(response.kind).toEqual("IResponseErrorInternal"); + }); + + it("GIVEN a not working APIM server WHEN call the handler THEN an Internal Error is returned", async () => { + const handler = GetImpersonateUserHandler( + fakeServicePrincipalCredentials, + fakeApimConfig + ); + + const response = await handler( + mockedContext as any, + undefined, + aBreakingApimSubscriptionId + ); + + expect(response.kind).toEqual("IResponseErrorInternal"); + }); + + it("GIVEN a subscripion without owner WHEN call the handler THEN an Internal Error error is returned", async () => { + const handler = GetImpersonateUserHandler( + fakeServicePrincipalCredentials, + fakeApimConfig + ); + + const response = await handler( + mockedContext as any, + undefined, + aValidSubscriptionIdWithouthOwner + ); + + expect(response.kind).toEqual("IResponseErrorInternal"); + }); + + it("GIVEN a not existing subscripion id WHEN call the handler THEN an Not Found error is returned", async () => { + const handler = GetImpersonateUserHandler( + fakeServicePrincipalCredentials, + fakeApimConfig + ); + + const response = await handler( + mockedContext as any, + undefined, + aNotExistingSubscriptionId + ); + + expect(response.kind).toEqual("IResponseErrorNotFound"); + }); + + it("should return all the user subscriptions and groups", async () => { + const anApimGroupContract: GroupContract = { + description: "group description", + displayName: "groupName" + }; + + const someValidGroups: ReadonlyArray = [ + { ...anApimGroupContract, id: "group #1" }, + { ...anApimGroupContract, id: "group #2" } + ]; + const someMoreValidGroups: ReadonlyArray = [ + { ...anApimGroupContract, id: "group #3" }, + { ...anApimGroupContract, id: "group #4" } + ]; + + mockUserGroupList.mockImplementation(() => { + const apimResponse = someValidGroups; + // eslint-disable-next-line functional/immutable-data + apimResponse["nextLink"] = "next-page"; + return Promise.resolve(apimResponse); + }); + mockUserGroupListNext.mockImplementation(() => + Promise.resolve(someMoreValidGroups) + ); + + const handler = GetImpersonateUserHandler( + fakeServicePrincipalCredentials, + fakeApimConfig + ); + + const response = await handler( + mockedContext as any, + undefined as any, + aValidSubscriptionId as any + ); + + expect(response).toEqual( + expect.objectContaining({ + kind: "IResponseSuccessJson", + value: { + serviceId: "valid-subscription-id", + userGroup: "groupName,groupName,groupName,groupName" + } + }) + ); + }); +}); diff --git a/GetImpersonateUserData/function.json b/GetImpersonateUserData/function.json new file mode 100644 index 00000000..ebc4398c --- /dev/null +++ b/GetImpersonateUserData/function.json @@ -0,0 +1,20 @@ +{ + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "route": "adm/impersonate-user/{serviceId}", + "methods": [ + "get" + ] + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ], + "scriptFile": "../dist/GetUser/index.js" +} diff --git a/GetImpersonateUserData/handler.ts b/GetImpersonateUserData/handler.ts new file mode 100644 index 00000000..a58f9012 --- /dev/null +++ b/GetImpersonateUserData/handler.ts @@ -0,0 +1,111 @@ +import { Context } from "@azure/functions"; +import * as express from "express"; +import { pipe } from "fp-ts/lib/function"; +import * as TE from "fp-ts/TaskEither"; +import * as O from "fp-ts/Option"; +import { + AzureApiAuthMiddleware, + IAzureApiAuthorization, + UserGroup +} from "@pagopa/io-functions-commons/dist/src/utils/middlewares/azure_api_auth"; +import { ContextMiddleware } from "@pagopa/io-functions-commons/dist/src/utils/middlewares/context_middleware"; +import { RequiredParamMiddleware } from "@pagopa/io-functions-commons/dist/src/utils/middlewares/required_param"; +import { withRequestMiddlewares } from "@pagopa/io-functions-commons/dist/src/utils/request_middleware"; +import { wrapRequestHandler } from "@pagopa/ts-commons/lib/request_middleware"; +import { + IResponseErrorInternal, + IResponseErrorNotFound, + IResponseSuccessJson, + ResponseSuccessJson +} from "@pagopa/ts-commons/lib/responses"; + +import { ServiceId } from "../generated/definitions/ServiceId"; +import { + getSubscription, + extractUserId, + wrapWithIResponse +} from "../utils/apim"; +import { + getApiClient, + getUserGroups, + IAzureApimConfig, + IServicePrincipalCreds +} from "../utils/apim"; + +type IGetImpersonateUser = ( + context: Context, + auth: IAzureApiAuthorization, + serviceId: ServiceId +) => Promise< + | IResponseSuccessJson<{ + readonly serviceId: string; + readonly userGroup: string; + }> + | IResponseErrorNotFound + | IResponseErrorInternal +>; + +// eslint-disable-next-line prefer-arrow/prefer-arrow-functions +export function GetImpersonateUserHandler( + servicePrincipalCreds: IServicePrincipalCreds, + azureApimConfig: IAzureApimConfig +): IGetImpersonateUser { + return async (_context, _, serviceId): ReturnType => + pipe( + getApiClient(servicePrincipalCreds, azureApimConfig.subscriptionId), + TE.chain(apimC => + pipe( + getSubscription( + apimC, + azureApimConfig.apimResourceGroup, + azureApimConfig.apim, + serviceId + ), + TE.map(extractUserId), + TE.filterOrElseW( + O.isSome, + () => new Error("Missing owner for input service") + ), + TE.chain(userId => + getUserGroups( + apimC, + azureApimConfig.apimResourceGroup, + azureApimConfig.apim, + userId.value + ) + ) + ) + ), + TE.map(groups => groups.map(g => g.displayName).join(",")), + TE.map(groupsAsString => ({ serviceId, userGroup: groupsAsString })), + TE.map(ResponseSuccessJson), + wrapWithIResponse, + TE.toUnion, + x => x + )(); +} + +/** + * Wraps a GetUsers handler inside an Express request handler. + */ +// eslint-disable-next-line prefer-arrow/prefer-arrow-functions +export function GetImpersonateUser( + servicePrincipalCreds: IServicePrincipalCreds, + azureApimConfig: IAzureApimConfig +): express.RequestHandler { + const handler = GetImpersonateUserHandler( + servicePrincipalCreds, + azureApimConfig + ); + + const middlewaresWrap = withRequestMiddlewares( + // Extract Azure Functions bindings + ContextMiddleware(), + // Allow only users in the ApiUserAdmin group + AzureApiAuthMiddleware(new Set([UserGroup.ApiUserAdmin])), // FIXME: APiUserAdmin is too much!!!! + // Extract the serviceId value from the request + RequiredParamMiddleware("serviceId", ServiceId) + ); + + return wrapRequestHandler(middlewaresWrap(handler)); +} diff --git a/GetImpersonateUserData/index.ts b/GetImpersonateUserData/index.ts new file mode 100644 index 00000000..34d3584e --- /dev/null +++ b/GetImpersonateUserData/index.ts @@ -0,0 +1,59 @@ +import { Context } from "@azure/functions"; + +import * as express from "express"; +import * as winston from "winston"; + +import { secureExpressApp } from "@pagopa/io-functions-commons/dist/src/utils/express"; +import { AzureContextTransport } from "@pagopa/io-functions-commons/dist/src/utils/logging"; +import { setAppContext } from "@pagopa/io-functions-commons/dist/src/utils/middlewares/context_middleware"; + +import createAzureFunctionHandler from "@pagopa/express-azure-functions/dist/src/createAzureFunctionsHandler"; + +import { getConfigOrThrow } from "../utils/config"; +import { GetImpersonateUser } from "./handler"; + +const config = getConfigOrThrow(); + +const servicePrincipalCreds = { + clientId: config.SERVICE_PRINCIPAL_CLIENT_ID, + secret: config.SERVICE_PRINCIPAL_SECRET, + tenantId: config.SERVICE_PRINCIPAL_TENANT_ID +}; +const azureApimConfig = { + apim: config.AZURE_APIM, + apimResourceGroup: config.AZURE_APIM_RESOURCE_GROUP, + subscriptionId: config.AZURE_SUBSCRIPTION_ID +}; + +// eslint-disable-next-line functional/no-let +let logger: Context["log"] | undefined; +const contextTransport = new AzureContextTransport(() => logger, { + level: "debug" +}); +winston.add(contextTransport); + +// Setup Express +const app = express(); +secureExpressApp(app); + +// Add express route +app.get( + "/adm/users/:email", + GetImpersonateUser( + // adb2cCreds, + servicePrincipalCreds, + azureApimConfig + // adb2cTokenAttributeName + ) +); + +const azureFunctionHandler = createAzureFunctionHandler(app); + +// Binds the express app to an Azure Function handler +const httpStart = (context: Context): void => { + logger = context.log; + setAppContext(app, context); + azureFunctionHandler(context); +}; + +export default httpStart; diff --git a/utils/apim.ts b/utils/apim.ts index aa790db4..418bb76b 100644 --- a/utils/apim.ts +++ b/utils/apim.ts @@ -1,10 +1,21 @@ import { ApiManagementClient } from "@azure/arm-apimanagement"; -import { GroupContract } from "@azure/arm-apimanagement/esm/models"; +import { + GroupContract, + SubscriptionGetResponse +} from "@azure/arm-apimanagement/esm/models"; import { GraphRbacManagementClient } from "@azure/graph"; import * as msRestNodeAuth from "@azure/ms-rest-nodeauth"; import { toError } from "fp-ts/lib/Either"; import { pipe } from "fp-ts/lib/function"; import * as TE from "fp-ts/lib/TaskEither"; +import * as E from "fp-ts/Either"; +import * as O from "fp-ts/Option"; +import { + ResponseErrorNotFound, + ResponseErrorInternal, + IResponseErrorInternal, + IResponseErrorNotFound +} from "@pagopa/ts-commons/lib/responses"; export interface IServicePrincipalCreds { readonly clientId: string; @@ -18,6 +29,21 @@ export interface IAzureApimConfig { readonly apim: string; } +interface IRestError { + readonly statusCode: number; + readonly message?: string; +} + +const isRestError = (i: unknown): i is IRestError => + typeof i === "object" && "statusCode" in i; + +export type IApimErrors = IResponseErrorInternal | IResponseErrorNotFound; + +export const mapRestErrorWithIResponse = (e: Error): IApimErrors => + isRestError(e) && e.statusCode === 404 + ? ResponseErrorNotFound("Not Found", e.message) + : ResponseErrorInternal(e.message); + // eslint-disable-next-line prefer-arrow/prefer-arrow-functions export function getApiClient( servicePrincipalCreds: IServicePrincipalCreds, @@ -91,3 +117,34 @@ export function getUserGroups( }, toError) ); } + +export const getSubscription = ( + apimClient: ApiManagementClient, + apimResourceGroup: string, + apim: string, + serviceId: string +): TE.TaskEither => + pipe( + serviceId, + TE.right, + TE.chain(sid => + TE.tryCatch( + () => apimClient.subscription.get(apimResourceGroup, apim, sid), + E.toError + ) + ) + ); + +export const wrapWithIResponse = ( + fa: TE.TaskEither +): TE.TaskEither => + pipe(fa, TE.mapLeft(mapRestErrorWithIResponse)); + +export const extractUserId = ( + subscription: SubscriptionGetResponse +): O.Option => + pipe( + subscription.ownerId, + O.fromNullable, + O.map(str => str.substring(7)) // {userId} will be extracted from /users/{userId} + ); From 51d651c1081d3495004f3a44f9c2e64a461f11e1 Mon Sep 17 00:00:00 2001 From: fabriziopapi Date: Tue, 7 Dec 2021 17:29:45 +0100 Subject: [PATCH 02/11] [#IC-131] (+) rename impersonate function (+) move ImpersonatedData to io-fn-commons --- .../__tests__/handler.test.ts | 22 +++++++------- .../function.json | 4 +-- .../handler.ts | 30 +++++++++---------- .../index.ts | 10 ++----- openapi/index.yaml | 21 +++++++++++++ 5 files changed, 52 insertions(+), 35 deletions(-) rename {GetImpersonateUserData => GetImpersonateUser}/__tests__/handler.test.ts (88%) rename {GetImpersonateUserData => GetImpersonateUser}/function.json (71%) rename {GetImpersonateUserData => GetImpersonateUser}/handler.ts (83%) rename {GetImpersonateUserData => GetImpersonateUser}/index.ts (90%) diff --git a/GetImpersonateUserData/__tests__/handler.test.ts b/GetImpersonateUser/__tests__/handler.test.ts similarity index 88% rename from GetImpersonateUserData/__tests__/handler.test.ts rename to GetImpersonateUser/__tests__/handler.test.ts index d0d14272..7edb9229 100644 --- a/GetImpersonateUserData/__tests__/handler.test.ts +++ b/GetImpersonateUser/__tests__/handler.test.ts @@ -4,7 +4,7 @@ import * as TE from "fp-ts/lib/TaskEither"; import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; import * as ApimUtils from "../../utils/apim"; import { IAzureApimConfig, IServicePrincipalCreds } from "../../utils/apim"; -import { GetImpersonateUserHandler } from "../handler"; +import { GetImpersonateServiceHandler } from "../handler"; import { RestError } from "@azure/ms-rest-js"; jest.mock("@azure/arm-apimanagement"); @@ -79,18 +79,18 @@ const mockLog = jest.fn(); const mockedContext = { log: { error: mockLog } }; // eslint-disable-next-line sonar/sonar-max-lines-per-function -describe("GetImpersonateUserHandler", () => { +describe("GetImpersonateServiceHandler", () => { it("GIVEN a not working APIM client WHEN call the handler THEN an Internel Error is returned", async () => { spyOnGetApiClient.mockImplementationOnce(() => TE.left(Error("Error from ApiManagementClient constructor")) ); - const getImpersonateUserHandler = GetImpersonateUserHandler( + const getImpersonateServiceHandler = GetImpersonateServiceHandler( fakeServicePrincipalCredentials, fakeApimConfig ); - const response = await getImpersonateUserHandler( + const response = await getImpersonateServiceHandler( mockedContext as any, undefined, undefined @@ -100,7 +100,7 @@ describe("GetImpersonateUserHandler", () => { }); it("GIVEN a not working APIM server WHEN call the handler THEN an Internal Error is returned", async () => { - const handler = GetImpersonateUserHandler( + const handler = GetImpersonateServiceHandler( fakeServicePrincipalCredentials, fakeApimConfig ); @@ -115,7 +115,7 @@ describe("GetImpersonateUserHandler", () => { }); it("GIVEN a subscripion without owner WHEN call the handler THEN an Internal Error error is returned", async () => { - const handler = GetImpersonateUserHandler( + const handler = GetImpersonateServiceHandler( fakeServicePrincipalCredentials, fakeApimConfig ); @@ -130,7 +130,7 @@ describe("GetImpersonateUserHandler", () => { }); it("GIVEN a not existing subscripion id WHEN call the handler THEN an Not Found error is returned", async () => { - const handler = GetImpersonateUserHandler( + const handler = GetImpersonateServiceHandler( fakeServicePrincipalCredentials, fakeApimConfig ); @@ -144,7 +144,7 @@ describe("GetImpersonateUserHandler", () => { expect(response.kind).toEqual("IResponseErrorNotFound"); }); - it("should return all the user subscriptions and groups", async () => { + it("GIVEN an existing subscripion id WHEN call the handler THEN a proper Impersonated Service is returned", async () => { const anApimGroupContract: GroupContract = { description: "group description", displayName: "groupName" @@ -169,7 +169,7 @@ describe("GetImpersonateUserHandler", () => { Promise.resolve(someMoreValidGroups) ); - const handler = GetImpersonateUserHandler( + const handler = GetImpersonateServiceHandler( fakeServicePrincipalCredentials, fakeApimConfig ); @@ -184,8 +184,8 @@ describe("GetImpersonateUserHandler", () => { expect.objectContaining({ kind: "IResponseSuccessJson", value: { - serviceId: "valid-subscription-id", - userGroup: "groupName,groupName,groupName,groupName" + service_id: "valid-subscription-id", + user_groups: "groupName,groupName,groupName,groupName" } }) ); diff --git a/GetImpersonateUserData/function.json b/GetImpersonateUser/function.json similarity index 71% rename from GetImpersonateUserData/function.json rename to GetImpersonateUser/function.json index ebc4398c..f8d04f05 100644 --- a/GetImpersonateUserData/function.json +++ b/GetImpersonateUser/function.json @@ -5,7 +5,7 @@ "type": "httpTrigger", "direction": "in", "name": "req", - "route": "adm/impersonate-user/{serviceId}", + "route": "adm/impersonate-service/{serviceId}", "methods": [ "get" ] @@ -16,5 +16,5 @@ "name": "res" } ], - "scriptFile": "../dist/GetUser/index.js" + "scriptFile": "../dist/GetImperonateUser/index.js" } diff --git a/GetImpersonateUserData/handler.ts b/GetImpersonateUser/handler.ts similarity index 83% rename from GetImpersonateUserData/handler.ts rename to GetImpersonateUser/handler.ts index a58f9012..ce6466cd 100644 --- a/GetImpersonateUserData/handler.ts +++ b/GetImpersonateUser/handler.ts @@ -20,6 +20,7 @@ import { } from "@pagopa/ts-commons/lib/responses"; import { ServiceId } from "../generated/definitions/ServiceId"; +import { ImpersonatedService } from "../generated/definitions/ImpersonatedService"; import { getSubscription, extractUserId, @@ -32,25 +33,22 @@ import { IServicePrincipalCreds } from "../utils/apim"; -type IGetImpersonateUser = ( +type IGetImpersonateService = ( context: Context, auth: IAzureApiAuthorization, serviceId: ServiceId ) => Promise< - | IResponseSuccessJson<{ - readonly serviceId: string; - readonly userGroup: string; - }> + | IResponseSuccessJson | IResponseErrorNotFound | IResponseErrorInternal >; // eslint-disable-next-line prefer-arrow/prefer-arrow-functions -export function GetImpersonateUserHandler( +export function GetImpersonateServiceHandler( servicePrincipalCreds: IServicePrincipalCreds, azureApimConfig: IAzureApimConfig -): IGetImpersonateUser { - return async (_context, _, serviceId): ReturnType => +): IGetImpersonateService { + return async (_context, _, serviceId): ReturnType => pipe( getApiClient(servicePrincipalCreds, azureApimConfig.subscriptionId), TE.chain(apimC => @@ -77,23 +75,25 @@ export function GetImpersonateUserHandler( ) ), TE.map(groups => groups.map(g => g.displayName).join(",")), - TE.map(groupsAsString => ({ serviceId, userGroup: groupsAsString })), + TE.map(groupsAsString => ({ + service_id: serviceId, + user_groups: groupsAsString + })), TE.map(ResponseSuccessJson), wrapWithIResponse, - TE.toUnion, - x => x + TE.toUnion )(); } /** - * Wraps a GetUsers handler inside an Express request handler. + * Wraps a GetServices handler inside an Express request handler. */ // eslint-disable-next-line prefer-arrow/prefer-arrow-functions -export function GetImpersonateUser( +export function GetImpersonateService( servicePrincipalCreds: IServicePrincipalCreds, azureApimConfig: IAzureApimConfig ): express.RequestHandler { - const handler = GetImpersonateUserHandler( + const handler = GetImpersonateServiceHandler( servicePrincipalCreds, azureApimConfig ); @@ -102,7 +102,7 @@ export function GetImpersonateUser( // Extract Azure Functions bindings ContextMiddleware(), // Allow only users in the ApiUserAdmin group - AzureApiAuthMiddleware(new Set([UserGroup.ApiUserAdmin])), // FIXME: APiUserAdmin is too much!!!! + AzureApiAuthMiddleware(new Set([UserGroup.ApiUserAdmin])), // FIXME: APiUserAdmin is too much??? // Extract the serviceId value from the request RequiredParamMiddleware("serviceId", ServiceId) ); diff --git a/GetImpersonateUserData/index.ts b/GetImpersonateUser/index.ts similarity index 90% rename from GetImpersonateUserData/index.ts rename to GetImpersonateUser/index.ts index 34d3584e..29eae2f3 100644 --- a/GetImpersonateUserData/index.ts +++ b/GetImpersonateUser/index.ts @@ -10,7 +10,7 @@ import { setAppContext } from "@pagopa/io-functions-commons/dist/src/utils/middl import createAzureFunctionHandler from "@pagopa/express-azure-functions/dist/src/createAzureFunctionsHandler"; import { getConfigOrThrow } from "../utils/config"; -import { GetImpersonateUser } from "./handler"; +import { GetImpersonateService } from "./handler"; const config = getConfigOrThrow(); @@ -19,6 +19,7 @@ const servicePrincipalCreds = { secret: config.SERVICE_PRINCIPAL_SECRET, tenantId: config.SERVICE_PRINCIPAL_TENANT_ID }; + const azureApimConfig = { apim: config.AZURE_APIM, apimResourceGroup: config.AZURE_APIM_RESOURCE_GROUP, @@ -39,12 +40,7 @@ secureExpressApp(app); // Add express route app.get( "/adm/users/:email", - GetImpersonateUser( - // adb2cCreds, - servicePrincipalCreds, - azureApimConfig - // adb2cTokenAttributeName - ) + GetImpersonateService(servicePrincipalCreds, azureApimConfig) ); const azureFunctionHandler = createAzureFunctionHandler(app); diff --git a/openapi/index.yaml b/openapi/index.yaml index 46efbec6..418ac566 100644 --- a/openapi/index.yaml +++ b/openapi/index.yaml @@ -309,6 +309,25 @@ paths: description: Subscription not found "500": description: Internal server error + /impersonate-service/{serviceId}: + get: + summary: Gets the data to impersonate a service + operationId: getImpersonatedUser + parameters: + - name: serviceId + description: the serviceId of the target service. + in: path + type: string + required: true + responses: + "200": + description: data required to impersonte a service + schema: + $ref: "#/definitions/ImpersonatedService" + "404": + description: service or service owner not found + "500": + description: Internal server error /users: get: summary: Gets the list of users @@ -538,6 +557,8 @@ definitions: $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/FiscalCode" ExtendedProfile: $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/ExtendedProfile" + ImpersonatedService: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/IC-66--add-legal-message-for-app-and-impersonate-service-data/openapi/definitions.yaml#/ImpersonatedService" UserGroupsPayload: description: |- All the groups with which the user must be associated. From 17421ebd49adfb5634f7ecaf89259613d795c7f2 Mon Sep 17 00:00:00 2001 From: fabriziopapi Date: Tue, 7 Dec 2021 17:34:34 +0100 Subject: [PATCH 03/11] [#IC-131] (+) fix wrong impersonate operation name --- openapi/index.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi/index.yaml b/openapi/index.yaml index 418ac566..e3a080df 100644 --- a/openapi/index.yaml +++ b/openapi/index.yaml @@ -312,7 +312,7 @@ paths: /impersonate-service/{serviceId}: get: summary: Gets the data to impersonate a service - operationId: getImpersonatedUser + operationId: getImpersonatedService parameters: - name: serviceId description: the serviceId of the target service. From 80e09cff42e06155215702cc749f32045f15b55c Mon Sep 17 00:00:00 2001 From: fabriziopapi Date: Tue, 7 Dec 2021 17:51:36 +0100 Subject: [PATCH 04/11] [#IC-131] reduce impersonate permission --- GetImpersonateUser/handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GetImpersonateUser/handler.ts b/GetImpersonateUser/handler.ts index ce6466cd..b8b4fee3 100644 --- a/GetImpersonateUser/handler.ts +++ b/GetImpersonateUser/handler.ts @@ -102,7 +102,7 @@ export function GetImpersonateService( // Extract Azure Functions bindings ContextMiddleware(), // Allow only users in the ApiUserAdmin group - AzureApiAuthMiddleware(new Set([UserGroup.ApiUserAdmin])), // FIXME: APiUserAdmin is too much??? + AzureApiAuthMiddleware(new Set([UserGroup.ApiMessageWriteWithLegal])), // FIXME: APiUserAdmin is too much, could be useful define a new permission? // Extract the serviceId value from the request RequiredParamMiddleware("serviceId", ServiceId) ); From 0d272b20071f1a4414f2fac8879eb8539b1ad98a Mon Sep 17 00:00:00 2001 From: fabriziopapi Date: Fri, 10 Dec 2021 12:44:52 +0100 Subject: [PATCH 05/11] [#IC-131] (+) fix function name and path --- .../__tests__/handler.test.ts | 0 {GetImpersonateUser => GetImpersonateService}/function.json | 2 +- {GetImpersonateUser => GetImpersonateService}/handler.ts | 2 +- {GetImpersonateUser => GetImpersonateService}/index.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename {GetImpersonateUser => GetImpersonateService}/__tests__/handler.test.ts (100%) rename {GetImpersonateUser => GetImpersonateService}/function.json (84%) rename {GetImpersonateUser => GetImpersonateService}/handler.ts (95%) rename {GetImpersonateUser => GetImpersonateService}/index.ts (97%) diff --git a/GetImpersonateUser/__tests__/handler.test.ts b/GetImpersonateService/__tests__/handler.test.ts similarity index 100% rename from GetImpersonateUser/__tests__/handler.test.ts rename to GetImpersonateService/__tests__/handler.test.ts diff --git a/GetImpersonateUser/function.json b/GetImpersonateService/function.json similarity index 84% rename from GetImpersonateUser/function.json rename to GetImpersonateService/function.json index f8d04f05..9adab5dd 100644 --- a/GetImpersonateUser/function.json +++ b/GetImpersonateService/function.json @@ -16,5 +16,5 @@ "name": "res" } ], - "scriptFile": "../dist/GetImperonateUser/index.js" + "scri++ptFile": "../dist/GetImperonateService/index.js" } diff --git a/GetImpersonateUser/handler.ts b/GetImpersonateService/handler.ts similarity index 95% rename from GetImpersonateUser/handler.ts rename to GetImpersonateService/handler.ts index b8b4fee3..8058549b 100644 --- a/GetImpersonateUser/handler.ts +++ b/GetImpersonateService/handler.ts @@ -102,7 +102,7 @@ export function GetImpersonateService( // Extract Azure Functions bindings ContextMiddleware(), // Allow only users in the ApiUserAdmin group - AzureApiAuthMiddleware(new Set([UserGroup.ApiMessageWriteWithLegal])), // FIXME: APiUserAdmin is too much, could be useful define a new permission? + AzureApiAuthMiddleware(new Set([UserGroup.ApiUserAdmin])), // Extract the serviceId value from the request RequiredParamMiddleware("serviceId", ServiceId) ); diff --git a/GetImpersonateUser/index.ts b/GetImpersonateService/index.ts similarity index 97% rename from GetImpersonateUser/index.ts rename to GetImpersonateService/index.ts index 29eae2f3..4bf23ac3 100644 --- a/GetImpersonateUser/index.ts +++ b/GetImpersonateService/index.ts @@ -39,7 +39,7 @@ secureExpressApp(app); // Add express route app.get( - "/adm/users/:email", + "adm/impersonate-service/:serviceId", GetImpersonateService(servicePrincipalCreds, azureApimConfig) ); From 0acaecf063a960a965f8afa67cbf6db05132c18f Mon Sep 17 00:00:00 2001 From: Alessio Dore <57567806+AleDore@users.noreply.github.com> Date: Thu, 16 Dec 2021 09:59:42 +0100 Subject: [PATCH 06/11] update references to fn-commons --- openapi/index.yaml | 52 +++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/openapi/index.yaml b/openapi/index.yaml index e3a080df..ab54a7b2 100644 --- a/openapi/index.yaml +++ b/openapi/index.yaml @@ -496,7 +496,7 @@ definitions: required: - email EmailAddress: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/EmailAddress" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/EmailAddress" ServiceCollection: type: object properties: @@ -510,55 +510,55 @@ definitions: - items - page_size ProblemJson: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/ProblemJson" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/ProblemJson" Service: - "$ref": "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/Service" + "$ref": "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/Service" ServiceMetadata: - "$ref": "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/ServiceMetadata" + "$ref": "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/ServiceMetadata" CommonServiceMetadata: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/CommonServiceMetadata" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/CommonServiceMetadata" StandardServiceMetadata: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/StandardServiceMetadata" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/StandardServiceMetadata" SpecialServiceMetadata: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/SpecialServiceMetadata" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/SpecialServiceMetadata" ServiceScope: - "$ref": "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/ServiceScope" + "$ref": "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/ServiceScope" ServiceCategory: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/ServiceCategory" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/ServiceCategory" SpecialServiceCategory: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/SpecialServiceCategory" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/SpecialServiceCategory" StandardServiceCategory: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/StandardServiceCategory" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/StandardServiceCategory" ServicePayload: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/ServicePayload" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/ServicePayload" ExtendedServicePayload: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/ExtendedServicePayload" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/ExtendedServicePayload" HiddenServicePayload: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/HiddenServicePayload" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/HiddenServicePayload" VisibleServicePayload: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/VisibleServicePayload" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/VisibleServicePayload" CommonServicePayload: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/CommonServicePayload" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/CommonServicePayload" ServiceId: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/ServiceId" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/ServiceId" ServiceName: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/ServiceName" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/ServiceName" OrganizationName: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/OrganizationName" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/OrganizationName" DepartmentName: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/DepartmentName" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/DepartmentName" CIDR: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/CIDR" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/CIDR" MaxAllowedPaymentAmount: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/MaxAllowedPaymentAmount" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/MaxAllowedPaymentAmount" OrganizationFiscalCode: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/OrganizationFiscalCode" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/OrganizationFiscalCode" FiscalCode: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/FiscalCode" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/FiscalCode" ExtendedProfile: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.0.1/openapi/definitions.yaml#/ExtendedProfile" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/ExtendedProfile" ImpersonatedService: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/IC-66--add-legal-message-for-app-and-impersonate-service-data/openapi/definitions.yaml#/ImpersonatedService" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/ImpersonatedService" UserGroupsPayload: description: |- All the groups with which the user must be associated. From a7574e2d0f172d48f3db26476c466e9476ac2969 Mon Sep 17 00:00:00 2001 From: AleDore Date: Fri, 17 Dec 2021 18:46:03 +0100 Subject: [PATCH 07/11] Impersonate service refactor --- .../__tests__/handler.test.ts | 178 ++++++++---- GetImpersonateService/handler.ts | 70 +++-- openapi/index.yaml | 52 ++-- package.json | 2 +- utils/apim.ts | 105 ++++--- utils/test_config.ts | 11 + utils/test_move.ts | 266 ++++++++++++++++++ 7 files changed, 539 insertions(+), 145 deletions(-) create mode 100644 utils/test_config.ts create mode 100644 utils/test_move.ts diff --git a/GetImpersonateService/__tests__/handler.test.ts b/GetImpersonateService/__tests__/handler.test.ts index 7edb9229..271166ff 100644 --- a/GetImpersonateService/__tests__/handler.test.ts +++ b/GetImpersonateService/__tests__/handler.test.ts @@ -3,9 +3,16 @@ import { GroupContract } from "@azure/arm-apimanagement/esm/models"; import * as TE from "fp-ts/lib/TaskEither"; import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; import * as ApimUtils from "../../utils/apim"; -import { IAzureApimConfig, IServicePrincipalCreds } from "../../utils/apim"; +import { + ApimRestError, + IAzureApimConfig, + IServicePrincipalCreds +} from "../../utils/apim"; import { GetImpersonateServiceHandler } from "../handler"; import { RestError } from "@azure/ms-rest-js"; +import { pipe } from "fp-ts/lib/function"; +import { mapLeft } from "fp-ts/lib/Either"; +import { errorsToReadableMessages } from "@pagopa/ts-commons/lib/reporters"; jest.mock("@azure/arm-apimanagement"); jest.mock("@azure/graph"); @@ -24,10 +31,6 @@ const fakeApimConfig: IAzureApimConfig = { const fakeUserName = "a-non-empty-string"; -const mockUserGroupList = jest.fn(); -const mockUserGroupListNext = jest.fn(); -const mockUserSubscriptionGet = jest.fn(); - const aValidSubscriptionId = "valid-subscription-id" as NonEmptyString; const aNotExistingSubscriptionId = "not-existing-subscription-id" as NonEmptyString; const aBreakingApimSubscriptionId = "broken-subscription-id" as NonEmptyString; @@ -42,44 +45,65 @@ const mockedSubscriptionWithoutOwner = { ownerId: undefined }; -const mockApiManagementClient = ApiManagementClient as jest.Mock; +const anApimGroupContract: GroupContract = { + description: "group description", + displayName: "groupName" +}; -mockApiManagementClient.mockImplementation(() => ({ +const someValidGroups: ReadonlyArray = [ + { ...anApimGroupContract, id: "group #1" }, + { ...anApimGroupContract, id: "group #2" } +]; +const someMoreValidGroups: ReadonlyArray = [ + { ...anApimGroupContract, id: "group #3" }, + { ...anApimGroupContract, id: "group #4" } +]; + +const mockedUserWithoutEmail = { + name: "test", + surname: "test" +}; + +const mockUserGroupList = jest.fn().mockImplementation(() => { + const apimResponse = someValidGroups; + // eslint-disable-next-line functional/immutable-data + apimResponse["nextLink"] = "next-page"; + return Promise.resolve(apimResponse); +}); +const mockUserGroupListNext = jest + .fn() + .mockImplementation(() => Promise.resolve(someMoreValidGroups)); +const mockUserSubscriptionGet = jest + .fn() + .mockImplementation(() => Promise.resolve(mockedSubscription)); +const mockUserGet = jest + .fn() + .mockImplementation(() => Promise.resolve({ email: "user_email@mail.it" })); + +const mockApiManagementClient = { userGroup: { list: mockUserGroupList, listNext: mockUserGroupListNext }, subscription: { get: mockUserSubscriptionGet + }, + user: { + get: mockUserGet } -})); - -mockUserSubscriptionGet.mockImplementation((_, __, subscriptionId) => { - if (subscriptionId === aValidSubscriptionId) { - return Promise.resolve(mockedSubscription); - } - if (subscriptionId === aValidSubscriptionIdWithouthOwner) { - return Promise.resolve(mockedSubscriptionWithoutOwner); - } - if (subscriptionId === aBreakingApimSubscriptionId) { - return Promise.reject(new RestError("generic error", "", 500)); - } - if (subscriptionId === aNotExistingSubscriptionId) { - return Promise.reject(new RestError("not found", "", 404)); - } - return fail(Error("The provided subscription id value is not handled")); -}); +} as any; const spyOnGetApiClient = jest.spyOn(ApimUtils, "getApiClient"); -spyOnGetApiClient.mockImplementation(() => - TE.of(new mockApiManagementClient()) -); +spyOnGetApiClient.mockImplementation(() => TE.of(mockApiManagementClient)); const mockLog = jest.fn(); const mockedContext = { log: { error: mockLog } }; // eslint-disable-next-line sonar/sonar-max-lines-per-function describe("GetImpersonateServiceHandler", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); it("GIVEN a not working APIM client WHEN call the handler THEN an Internel Error is returned", async () => { spyOnGetApiClient.mockImplementationOnce(() => TE.left(Error("Error from ApiManagementClient constructor")) @@ -104,7 +128,9 @@ describe("GetImpersonateServiceHandler", () => { fakeServicePrincipalCredentials, fakeApimConfig ); - + mockUserSubscriptionGet.mockImplementationOnce(() => + Promise.reject(new RestError("generic error", "", 500)) + ); const response = await handler( mockedContext as any, undefined, @@ -114,27 +140,57 @@ describe("GetImpersonateServiceHandler", () => { expect(response.kind).toEqual("IResponseErrorInternal"); }); - it("GIVEN a subscripion without owner WHEN call the handler THEN an Internal Error error is returned", async () => { + it("GIVEN a subscription without owner WHEN call the handler THEN a Not Found Error error is returned", async () => { const handler = GetImpersonateServiceHandler( fakeServicePrincipalCredentials, fakeApimConfig ); + mockUserSubscriptionGet.mockImplementationOnce(() => + Promise.resolve(mockedSubscriptionWithoutOwner) + ); + const response = await handler( mockedContext as any, undefined, aValidSubscriptionIdWithouthOwner ); - expect(response.kind).toEqual("IResponseErrorInternal"); + expect(response.kind).toEqual("IResponseErrorNotFound"); }); - it("GIVEN a not existing subscripion id WHEN call the handler THEN an Not Found error is returned", async () => { + it("GIVEN a subscription with not existing owner WHEN call the handler THEN a Not Found Error error is returned", async () => { const handler = GetImpersonateServiceHandler( fakeServicePrincipalCredentials, fakeApimConfig ); + mockUserSubscriptionGet.mockImplementationOnce(() => + Promise.resolve(mockedSubscription) + ); + + mockUserGet.mockImplementationOnce(() => + Promise.reject(new RestError("not found", "Not Found", 404)) + ); + + const response = await handler( + mockedContext as any, + undefined, + aValidSubscriptionIdWithouthOwner + ); + + expect(response.kind).toEqual("IResponseErrorNotFound"); + }); + + it("GIVEN a user without email WHEN call the handler THEN a Not Found error is returned", async () => { + const handler = GetImpersonateServiceHandler( + fakeServicePrincipalCredentials, + fakeApimConfig + ); + mockUserSubscriptionGet.mockImplementationOnce(() => + Promise.resolve(mockedUserWithoutEmail) + ); + const response = await handler( mockedContext as any, undefined, @@ -144,31 +200,42 @@ describe("GetImpersonateServiceHandler", () => { expect(response.kind).toEqual("IResponseErrorNotFound"); }); - it("GIVEN an existing subscripion id WHEN call the handler THEN a proper Impersonated Service is returned", async () => { - const anApimGroupContract: GroupContract = { - description: "group description", - displayName: "groupName" - }; - - const someValidGroups: ReadonlyArray = [ - { ...anApimGroupContract, id: "group #1" }, - { ...anApimGroupContract, id: "group #2" } - ]; - const someMoreValidGroups: ReadonlyArray = [ - { ...anApimGroupContract, id: "group #3" }, - { ...anApimGroupContract, id: "group #4" } - ]; - - mockUserGroupList.mockImplementation(() => { - const apimResponse = someValidGroups; - // eslint-disable-next-line functional/immutable-data - apimResponse["nextLink"] = "next-page"; - return Promise.resolve(apimResponse); - }); - mockUserGroupListNext.mockImplementation(() => - Promise.resolve(someMoreValidGroups) + it("GIVEN an error while retrieving user WHEN call the handler THEN an Internal Error is returned", async () => { + const handler = GetImpersonateServiceHandler( + fakeServicePrincipalCredentials, + fakeApimConfig + ); + mockUserSubscriptionGet.mockImplementationOnce(() => + Promise.reject(new RestError("Internal Error", "Internal Error", 500)) + ); + + const response = await handler( + mockedContext as any, + undefined, + aNotExistingSubscriptionId + ); + + expect(response.kind).toEqual("IResponseErrorInternal"); + }); + it("GIVEN a not existing subscription id WHEN call the handler THEN a Not Found error is returned", async () => { + const handler = GetImpersonateServiceHandler( + fakeServicePrincipalCredentials, + fakeApimConfig + ); + mockUserSubscriptionGet.mockImplementationOnce(() => + Promise.reject(new RestError("not found", "Not Found", 404)) + ); + + const response = await handler( + mockedContext as any, + undefined, + aNotExistingSubscriptionId ); + expect(response.kind).toEqual("IResponseErrorNotFound"); + }); + + it("GIVEN an existing subscription id WHEN call the handler THEN a proper Impersonated Service is returned", async () => { const handler = GetImpersonateServiceHandler( fakeServicePrincipalCredentials, fakeApimConfig @@ -185,7 +252,8 @@ describe("GetImpersonateServiceHandler", () => { kind: "IResponseSuccessJson", value: { service_id: "valid-subscription-id", - user_groups: "groupName,groupName,groupName,groupName" + user_groups: "groupName,groupName,groupName,groupName", + user_email: "user_email@mail.it" } }) ); diff --git a/GetImpersonateService/handler.ts b/GetImpersonateService/handler.ts index 8058549b..32c0905b 100644 --- a/GetImpersonateService/handler.ts +++ b/GetImpersonateService/handler.ts @@ -16,16 +16,14 @@ import { IResponseErrorInternal, IResponseErrorNotFound, IResponseSuccessJson, + ResponseErrorInternal, + ResponseErrorNotFound, ResponseSuccessJson } from "@pagopa/ts-commons/lib/responses"; import { ServiceId } from "../generated/definitions/ServiceId"; import { ImpersonatedService } from "../generated/definitions/ImpersonatedService"; -import { - getSubscription, - extractUserId, - wrapWithIResponse -} from "../utils/apim"; +import { getSubscription, getUser, mapApimRestError } from "../utils/apim"; import { getApiClient, getUserGroups, @@ -43,6 +41,15 @@ type IGetImpersonateService = ( | IResponseErrorInternal >; +const chainNullableWithNotFound = ( + value: string +): TE.TaskEither => + pipe( + value, + O.fromNullable, + TE.fromOption(() => ResponseErrorNotFound("Not found", "Not Found")) + ); + // eslint-disable-next-line prefer-arrow/prefer-arrow-functions export function GetImpersonateServiceHandler( servicePrincipalCreds: IServicePrincipalCreds, @@ -51,6 +58,9 @@ export function GetImpersonateServiceHandler( return async (_context, _, serviceId): ReturnType => pipe( getApiClient(servicePrincipalCreds, azureApimConfig.subscriptionId), + TE.mapLeft(e => + ResponseErrorInternal(`Error while connecting to APIM ${e.message}`) + ), TE.chain(apimC => pipe( getSubscription( @@ -59,28 +69,46 @@ export function GetImpersonateServiceHandler( azureApimConfig.apim, serviceId ), - TE.map(extractUserId), - TE.filterOrElseW( - O.isSome, - () => new Error("Missing owner for input service") - ), + TE.mapLeft(mapApimRestError("Subscription")), + TE.map(subscription => subscription.ownerId), + TE.chainW(chainNullableWithNotFound), TE.chain(userId => - getUserGroups( - apimC, - azureApimConfig.apimResourceGroup, - azureApimConfig.apim, - userId.value + pipe( + getUserGroups( + apimC, + azureApimConfig.apimResourceGroup, + azureApimConfig.apim, + userId + ), + TE.mapLeft(e => + ResponseErrorInternal( + `Error while retrieving user groups ${e.message}` + ) + ), + TE.map(groups => groups.map(g => g.displayName).join(",")), + TE.map(groupsAsString => ({ + service_id: serviceId, + user_groups: groupsAsString + })), + TE.chain(result => + pipe( + getUser( + apimC, + azureApimConfig.apimResourceGroup, + azureApimConfig.apim, + userId + ), + TE.mapLeft(mapApimRestError("User")), + TE.map(user => user.email), + TE.chainW(chainNullableWithNotFound), + TE.map(user_email => ({ ...result, user_email })) + ) + ) ) ) ) ), - TE.map(groups => groups.map(g => g.displayName).join(",")), - TE.map(groupsAsString => ({ - service_id: serviceId, - user_groups: groupsAsString - })), TE.map(ResponseSuccessJson), - wrapWithIResponse, TE.toUnion )(); } diff --git a/openapi/index.yaml b/openapi/index.yaml index ab54a7b2..ae3fa844 100644 --- a/openapi/index.yaml +++ b/openapi/index.yaml @@ -496,7 +496,7 @@ definitions: required: - email EmailAddress: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/EmailAddress" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/EmailAddress" ServiceCollection: type: object properties: @@ -510,55 +510,55 @@ definitions: - items - page_size ProblemJson: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/ProblemJson" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/ProblemJson" Service: - "$ref": "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/Service" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/Service" ServiceMetadata: - "$ref": "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/ServiceMetadata" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/ServiceMetadata" CommonServiceMetadata: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/CommonServiceMetadata" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/CommonServiceMetadata" StandardServiceMetadata: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/StandardServiceMetadata" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/StandardServiceMetadata" SpecialServiceMetadata: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/SpecialServiceMetadata" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/SpecialServiceMetadata" ServiceScope: - "$ref": "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/ServiceScope" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/ServiceScope" ServiceCategory: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/ServiceCategory" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/ServiceCategory" SpecialServiceCategory: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/SpecialServiceCategory" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/SpecialServiceCategory" StandardServiceCategory: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/StandardServiceCategory" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/StandardServiceCategory" ServicePayload: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/ServicePayload" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/ServicePayload" ExtendedServicePayload: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/ExtendedServicePayload" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/ExtendedServicePayload" HiddenServicePayload: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/HiddenServicePayload" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/HiddenServicePayload" VisibleServicePayload: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/VisibleServicePayload" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/VisibleServicePayload" CommonServicePayload: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/CommonServicePayload" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/CommonServicePayload" ServiceId: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/ServiceId" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/ServiceId" ServiceName: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/ServiceName" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/ServiceName" OrganizationName: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/OrganizationName" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/OrganizationName" DepartmentName: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/DepartmentName" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/DepartmentName" CIDR: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/CIDR" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/CIDR" MaxAllowedPaymentAmount: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/MaxAllowedPaymentAmount" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/MaxAllowedPaymentAmount" OrganizationFiscalCode: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/OrganizationFiscalCode" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/OrganizationFiscalCode" FiscalCode: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/FiscalCode" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/FiscalCode" ExtendedProfile: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/ExtendedProfile" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/ExtendedProfile" ImpersonatedService: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.3.0/openapi/definitions.yaml#/ImpersonatedService" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v22.5.0/openapi/definitions.yaml#/ImpersonatedService" UserGroupsPayload: description: |- All the groups with which the user must be associated. diff --git a/package.json b/package.json index 46f46a9c..72ceafa6 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@azure/ms-rest-js": "^2.5.1", "@azure/ms-rest-nodeauth": "^2.0.6", "@pagopa/express-azure-functions": "^2.0.0", - "@pagopa/io-functions-commons": "^22.0.1", + "@pagopa/io-functions-commons": "^22.5.0", "@pagopa/ts-commons": "^10.0.1", "@types/archiver": "^3.1.1", "@types/randomstring": "^1.1.6", diff --git a/utils/apim.ts b/utils/apim.ts index 418bb76b..d7221e06 100644 --- a/utils/apim.ts +++ b/utils/apim.ts @@ -1,22 +1,23 @@ import { ApiManagementClient } from "@azure/arm-apimanagement"; import { GroupContract, - SubscriptionGetResponse + SubscriptionGetResponse, + UserGetResponse } from "@azure/arm-apimanagement/esm/models"; import { GraphRbacManagementClient } from "@azure/graph"; import * as msRestNodeAuth from "@azure/ms-rest-nodeauth"; import { toError } from "fp-ts/lib/Either"; -import { pipe } from "fp-ts/lib/function"; +import { flow, identity, pipe } from "fp-ts/lib/function"; +import * as t from "io-ts"; import * as TE from "fp-ts/lib/TaskEither"; import * as E from "fp-ts/Either"; -import * as O from "fp-ts/Option"; import { - ResponseErrorNotFound, - ResponseErrorInternal, IResponseErrorInternal, - IResponseErrorNotFound + IResponseErrorNotFound, + ResponseErrorInternal, + ResponseErrorNotFound } from "@pagopa/ts-commons/lib/responses"; - +import { parse } from "fp-ts/lib/Json"; export interface IServicePrincipalCreds { readonly clientId: string; readonly secret: string; @@ -29,21 +30,42 @@ export interface IAzureApimConfig { readonly apim: string; } -interface IRestError { - readonly statusCode: number; - readonly message?: string; -} +export type ApimMappedErrors = IResponseErrorInternal | IResponseErrorNotFound; -const isRestError = (i: unknown): i is IRestError => - typeof i === "object" && "statusCode" in i; +export const ApimRestError = t.interface({ + statusCode: t.number +}); +export type ApimRestError = t.TypeOf; -export type IApimErrors = IResponseErrorInternal | IResponseErrorNotFound; - -export const mapRestErrorWithIResponse = (e: Error): IApimErrors => - isRestError(e) && e.statusCode === 404 - ? ResponseErrorNotFound("Not Found", e.message) - : ResponseErrorInternal(e.message); +export const mapApimRestError = (resource: string) => ( + apimRestError: ApimRestError +): ApimMappedErrors => + apimRestError.statusCode === 404 + ? ResponseErrorNotFound("Not found", `${resource} Not found`) + : ResponseErrorInternal( + `Internal Error while retrieving ${resource} detail` + ); +export const chainApimMappedError = ( + te: TE.TaskEither +): TE.TaskEither => + pipe( + te, + TE.orElseW( + flow( + JSON.stringify, + parse, + E.chainW(ApimRestError.decode), + E.fold( + () => + TE.left({ + statusCode: 500 + }), + TE.left + ) + ) + ) + ); // eslint-disable-next-line prefer-arrow/prefer-arrow-functions export function getApiClient( servicePrincipalCreds: IServicePrincipalCreds, @@ -118,33 +140,32 @@ export function getUserGroups( ); } -export const getSubscription = ( +// eslint-disable-next-line prefer-arrow/prefer-arrow-functions +export function getUser( apimClient: ApiManagementClient, apimResourceGroup: string, apim: string, - serviceId: string -): TE.TaskEither => - pipe( - serviceId, - TE.right, - TE.chain(sid => - TE.tryCatch( - () => apimClient.subscription.get(apimResourceGroup, apim, sid), - E.toError - ) - ) + userId: string +): TE.TaskEither { + return pipe( + TE.tryCatch( + () => apimClient.user.get(apimResourceGroup, apim, userId), + identity + ), + chainApimMappedError ); +} -export const wrapWithIResponse = ( - fa: TE.TaskEither -): TE.TaskEither => - pipe(fa, TE.mapLeft(mapRestErrorWithIResponse)); - -export const extractUserId = ( - subscription: SubscriptionGetResponse -): O.Option => +export const getSubscription = ( + apimClient: ApiManagementClient, + apimResourceGroup: string, + apim: string, + serviceId: string +): TE.TaskEither => pipe( - subscription.ownerId, - O.fromNullable, - O.map(str => str.substring(7)) // {userId} will be extracted from /users/{userId} + TE.tryCatch( + () => apimClient.subscription.get(apimResourceGroup, apim, serviceId), + identity + ), + chainApimMappedError ); diff --git a/utils/test_config.ts b/utils/test_config.ts new file mode 100644 index 00000000..8f515ab0 --- /dev/null +++ b/utils/test_config.ts @@ -0,0 +1,11 @@ +export const servicePrincipalCreds = { + clientId: "bdb26925-f0de-4c7e-915f-26604f9b7baf", + secret: "AoV7Q~uP3zpBNbZ3vw3LZH8_0CcyEk1CZRFe6", + tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d" +}; + +export const azureApimConfig = { + apim: "io-p-apim-api", + apimResourceGroup: "io-p-rg-internal", + subscriptionId: "ec285037-c673-4f58-b594-d7c480da4e8b" +}; diff --git a/utils/test_move.ts b/utils/test_move.ts new file mode 100644 index 00000000..68c0ed17 --- /dev/null +++ b/utils/test_move.ts @@ -0,0 +1,266 @@ +/* eslint-disable functional/immutable-data */ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +import { ApiManagementClient } from "@azure/arm-apimanagement"; +import { + SubscriptionGetResponse, + UserContract, + UserGetResponse +} from "@azure/arm-apimanagement/esm/models"; +import { Either, isLeft, toError } from "fp-ts/lib/Either"; +import { pipe } from "fp-ts/lib/function"; +import * as TE from "fp-ts/lib/TaskEither"; +import * as E from "fp-ts/lib/Either"; +import * as T from "fp-ts/lib/Task"; +import * as AR from "fp-ts/lib/Array"; +import { groupContractToApiGroup } from "./conversions"; +import { + chainApimMappedError, + getApiClient, + getUserGroups, + IAzureApimConfig, + IServicePrincipalCreds +} from "./apim"; +import { azureApimConfig, servicePrincipalCreds } from "./test_config"; + +const getUserByEmail = ( + apimClient: ApiManagementClient, + userEmail: string +): TE.TaskEither => + pipe( + TE.tryCatch( + () => + apimClient.user.listByService( + azureApimConfig.apimResourceGroup, + azureApimConfig.apim, + { + filter: `email eq '${userEmail}'` + } + ), + toError + ), + TE.chain( + TE.fromPredicate( + results => results.length > 0, + () => new Error("Cannot find user by email") + ) + ), + TE.map(_ => _[0]) + ); +const getUserById = ( + apimClient: ApiManagementClient, + userId: string +): TE.TaskEither => + pipe( + TE.tryCatch( + () => + apimClient.user.get( + azureApimConfig.apimResourceGroup, + azureApimConfig.apim, + userId + ), + toError + ) + ); + +const getSubscription = ( + apimClient: ApiManagementClient, + subscriptionId: string +): TE.TaskEither => + pipe( + TE.tryCatch( + () => + apimClient.subscription.get( + azureApimConfig.apimResourceGroup, + azureApimConfig.apim, + subscriptionId + ), + toError + ) + ); + +const getSubscriptionUserId = ( + apimClient: ApiManagementClient, + subscriptionId: string +): TE.TaskEither => + pipe( + getSubscription(apimClient, subscriptionId), + TE.map(_ => _.ownerId.substring(_.ownerId.lastIndexOf("/") + 1)) + ); + +const updateSubscriptionOwner = ( + apimClient: ApiManagementClient, + subscription: SubscriptionGetResponse, + destinationOwnerId: string +): TE.TaskEither => + pipe( + TE.tryCatch( + () => + apimClient.subscription.createOrUpdate( + azureApimConfig.apimResourceGroup, + azureApimConfig.apim, + subscription.name, + { + displayName: subscription.displayName, + ownerId: destinationOwnerId, + scope: subscription.scope + } + ), + toError + ), + TE.map( + () => + `Update subscription ${subscription.name} with ownerId ${destinationOwnerId}` + ) + ); + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +const mergeFn = (te: T.Task>>) => + T.ApplicativePar.map(te, e => + e.reduce( + (acc, cur) => { + // our reducer is still pure, as we pass fresh object literal as initial value + isLeft(cur) + ? acc.errors.push(cur.left.message) + : acc.results.push(cur.right); + return acc; + }, + { errors: [], results: [] } + ) + ); + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +const testBySubId = ( + servicePrincipalCred: IServicePrincipalCreds, + azureApimCfg: IAzureApimConfig, + subscriptionId: string +) => + pipe( + getApiClient(servicePrincipalCred, azureApimCfg.subscriptionId), + TE.chain(apimClient => + pipe( + getSubscriptionUserId(apimClient, subscriptionId), + TE.chain(subscriptionOwnerId => + getUserById(apimClient, subscriptionOwnerId) + ), + + TE.chain(user => + pipe( + getUserGroups( + apimClient, + azureApimCfg.apimResourceGroup, + azureApimCfg.apim, + user.name + ), + TE.chain(groupContracts => + TE.fromEither( + AR.traverse(E.Applicative)(groupContractToApiGroup)([ + ...groupContracts + ]) + ) + ), + TE.map(groups => ({ + email: user.email, + groups, + id: user.id + })) + ) + ) + ) + ) + ); + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +const testBySubId2 = ( + servicePrincipalCred: IServicePrincipalCreds, + azureApimCfg: IAzureApimConfig, + subscriptionId: string +) => + pipe( + getApiClient(servicePrincipalCred, azureApimCfg.subscriptionId), + TE.chain(apimClient => + pipe( + getSubscriptionUserId(apimClient, subscriptionId), + TE.chain(subscriptionOwnerId => + getUserById(apimClient, subscriptionOwnerId) + ) + ) + ), + chainApimMappedError + ); + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +const test = ( + servicePrincipalCred: IServicePrincipalCreds, + azureApimCfg: IAzureApimConfig, + servicesToMigrate: ReadonlyArray, + orig_email: string = "l.franceschin@gmail.com", + dest_email: string = "postaforum@gmail.com" +) => + pipe( + getApiClient(servicePrincipalCred, azureApimCfg.subscriptionId), + TE.chain(apimClient => + pipe( + getUserByEmail(apimClient, orig_email), + TE.chain(origine => + pipe( + getUserByEmail(apimClient, dest_email), + TE.map(destinazione => ({ destinazione, origine })) + ) + ), + TE.chain(destOrig => + pipe( + AR.sequence(T.ApplicativePar)( + servicesToMigrate.map(serviceId => + pipe( + getSubscription(apimClient, serviceId), + TE.mapLeft( + e => + new Error( + `ERROR|${e.message} SubscriptionId = ${serviceId}` + ) + ), + TE.map(subscription => ({ ...destOrig, subscription })), + TE.chain( + TE.fromPredicate( + result => + result.origine.id === result.subscription.ownerId, + res => + new Error( + `ERROR|Subscription ${res.subscription.name} is not owned by ${res.origine.email}` + ) + ) + ), + TE.chain(_ => + updateSubscriptionOwner( + apimClient, + _.subscription, + _.destinazione.id + ) + ) + ) + ) + ), + mergeFn, + TE.fromTask + ) + ) + ) + ) + ); + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +const run = () => { + console.log(new Date()); + testBySubId2( + servicePrincipalCreds, + azureApimConfig, + "01F3YTAWFFKVVQXS8G4RM4J69M" + )() + .then(_ => { + console.log(new Date()); + console.log(_); + }) + .catch(console.log); +}; + +run(); From 941a615b8a92d3f9bf37f2ec30639e1019c108f5 Mon Sep 17 00:00:00 2001 From: Alessio Dore <57567806+AleDore@users.noreply.github.com> Date: Fri, 17 Dec 2021 18:48:38 +0100 Subject: [PATCH 08/11] Delete test_config.ts --- utils/test_config.ts | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 utils/test_config.ts diff --git a/utils/test_config.ts b/utils/test_config.ts deleted file mode 100644 index 8f515ab0..00000000 --- a/utils/test_config.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const servicePrincipalCreds = { - clientId: "bdb26925-f0de-4c7e-915f-26604f9b7baf", - secret: "AoV7Q~uP3zpBNbZ3vw3LZH8_0CcyEk1CZRFe6", - tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d" -}; - -export const azureApimConfig = { - apim: "io-p-apim-api", - apimResourceGroup: "io-p-rg-internal", - subscriptionId: "ec285037-c673-4f58-b594-d7c480da4e8b" -}; From 4e534a89b9f25f32b4095dcb96b1e265712631f1 Mon Sep 17 00:00:00 2001 From: AleDore Date: Fri, 17 Dec 2021 19:32:44 +0100 Subject: [PATCH 09/11] delete test file --- utils/test_move.ts | 266 --------------------------------------------- 1 file changed, 266 deletions(-) delete mode 100644 utils/test_move.ts diff --git a/utils/test_move.ts b/utils/test_move.ts deleted file mode 100644 index 68c0ed17..00000000 --- a/utils/test_move.ts +++ /dev/null @@ -1,266 +0,0 @@ -/* eslint-disable functional/immutable-data */ -/* eslint-disable @typescript-eslint/no-unused-expressions */ -import { ApiManagementClient } from "@azure/arm-apimanagement"; -import { - SubscriptionGetResponse, - UserContract, - UserGetResponse -} from "@azure/arm-apimanagement/esm/models"; -import { Either, isLeft, toError } from "fp-ts/lib/Either"; -import { pipe } from "fp-ts/lib/function"; -import * as TE from "fp-ts/lib/TaskEither"; -import * as E from "fp-ts/lib/Either"; -import * as T from "fp-ts/lib/Task"; -import * as AR from "fp-ts/lib/Array"; -import { groupContractToApiGroup } from "./conversions"; -import { - chainApimMappedError, - getApiClient, - getUserGroups, - IAzureApimConfig, - IServicePrincipalCreds -} from "./apim"; -import { azureApimConfig, servicePrincipalCreds } from "./test_config"; - -const getUserByEmail = ( - apimClient: ApiManagementClient, - userEmail: string -): TE.TaskEither => - pipe( - TE.tryCatch( - () => - apimClient.user.listByService( - azureApimConfig.apimResourceGroup, - azureApimConfig.apim, - { - filter: `email eq '${userEmail}'` - } - ), - toError - ), - TE.chain( - TE.fromPredicate( - results => results.length > 0, - () => new Error("Cannot find user by email") - ) - ), - TE.map(_ => _[0]) - ); -const getUserById = ( - apimClient: ApiManagementClient, - userId: string -): TE.TaskEither => - pipe( - TE.tryCatch( - () => - apimClient.user.get( - azureApimConfig.apimResourceGroup, - azureApimConfig.apim, - userId - ), - toError - ) - ); - -const getSubscription = ( - apimClient: ApiManagementClient, - subscriptionId: string -): TE.TaskEither => - pipe( - TE.tryCatch( - () => - apimClient.subscription.get( - azureApimConfig.apimResourceGroup, - azureApimConfig.apim, - subscriptionId - ), - toError - ) - ); - -const getSubscriptionUserId = ( - apimClient: ApiManagementClient, - subscriptionId: string -): TE.TaskEither => - pipe( - getSubscription(apimClient, subscriptionId), - TE.map(_ => _.ownerId.substring(_.ownerId.lastIndexOf("/") + 1)) - ); - -const updateSubscriptionOwner = ( - apimClient: ApiManagementClient, - subscription: SubscriptionGetResponse, - destinationOwnerId: string -): TE.TaskEither => - pipe( - TE.tryCatch( - () => - apimClient.subscription.createOrUpdate( - azureApimConfig.apimResourceGroup, - azureApimConfig.apim, - subscription.name, - { - displayName: subscription.displayName, - ownerId: destinationOwnerId, - scope: subscription.scope - } - ), - toError - ), - TE.map( - () => - `Update subscription ${subscription.name} with ownerId ${destinationOwnerId}` - ) - ); - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -const mergeFn = (te: T.Task>>) => - T.ApplicativePar.map(te, e => - e.reduce( - (acc, cur) => { - // our reducer is still pure, as we pass fresh object literal as initial value - isLeft(cur) - ? acc.errors.push(cur.left.message) - : acc.results.push(cur.right); - return acc; - }, - { errors: [], results: [] } - ) - ); - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -const testBySubId = ( - servicePrincipalCred: IServicePrincipalCreds, - azureApimCfg: IAzureApimConfig, - subscriptionId: string -) => - pipe( - getApiClient(servicePrincipalCred, azureApimCfg.subscriptionId), - TE.chain(apimClient => - pipe( - getSubscriptionUserId(apimClient, subscriptionId), - TE.chain(subscriptionOwnerId => - getUserById(apimClient, subscriptionOwnerId) - ), - - TE.chain(user => - pipe( - getUserGroups( - apimClient, - azureApimCfg.apimResourceGroup, - azureApimCfg.apim, - user.name - ), - TE.chain(groupContracts => - TE.fromEither( - AR.traverse(E.Applicative)(groupContractToApiGroup)([ - ...groupContracts - ]) - ) - ), - TE.map(groups => ({ - email: user.email, - groups, - id: user.id - })) - ) - ) - ) - ) - ); - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -const testBySubId2 = ( - servicePrincipalCred: IServicePrincipalCreds, - azureApimCfg: IAzureApimConfig, - subscriptionId: string -) => - pipe( - getApiClient(servicePrincipalCred, azureApimCfg.subscriptionId), - TE.chain(apimClient => - pipe( - getSubscriptionUserId(apimClient, subscriptionId), - TE.chain(subscriptionOwnerId => - getUserById(apimClient, subscriptionOwnerId) - ) - ) - ), - chainApimMappedError - ); - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -const test = ( - servicePrincipalCred: IServicePrincipalCreds, - azureApimCfg: IAzureApimConfig, - servicesToMigrate: ReadonlyArray, - orig_email: string = "l.franceschin@gmail.com", - dest_email: string = "postaforum@gmail.com" -) => - pipe( - getApiClient(servicePrincipalCred, azureApimCfg.subscriptionId), - TE.chain(apimClient => - pipe( - getUserByEmail(apimClient, orig_email), - TE.chain(origine => - pipe( - getUserByEmail(apimClient, dest_email), - TE.map(destinazione => ({ destinazione, origine })) - ) - ), - TE.chain(destOrig => - pipe( - AR.sequence(T.ApplicativePar)( - servicesToMigrate.map(serviceId => - pipe( - getSubscription(apimClient, serviceId), - TE.mapLeft( - e => - new Error( - `ERROR|${e.message} SubscriptionId = ${serviceId}` - ) - ), - TE.map(subscription => ({ ...destOrig, subscription })), - TE.chain( - TE.fromPredicate( - result => - result.origine.id === result.subscription.ownerId, - res => - new Error( - `ERROR|Subscription ${res.subscription.name} is not owned by ${res.origine.email}` - ) - ) - ), - TE.chain(_ => - updateSubscriptionOwner( - apimClient, - _.subscription, - _.destinazione.id - ) - ) - ) - ) - ), - mergeFn, - TE.fromTask - ) - ) - ) - ) - ); - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -const run = () => { - console.log(new Date()); - testBySubId2( - servicePrincipalCreds, - azureApimConfig, - "01F3YTAWFFKVVQXS8G4RM4J69M" - )() - .then(_ => { - console.log(new Date()); - console.log(_); - }) - .catch(console.log); -}; - -run(); From 724a52019653b0de85a93dfc7c53c2983afe8ccb Mon Sep 17 00:00:00 2001 From: AleDore Date: Mon, 20 Dec 2021 09:39:34 +0100 Subject: [PATCH 10/11] update dependencies --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 92a24103..3b3af3dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -945,10 +945,10 @@ resolved "https://registry.yarnpkg.com/@pagopa/express-azure-functions/-/express-azure-functions-2.0.0.tgz#eb52a0b997d931c1509372e2a9bea22a8ca85c17" integrity sha512-IFZqtk0e2sfkMZIxYqPORzxcKRkbIrVJesR6eMLNwzh1rA4bl2uh9ZHk1m55LNq4ZmaxREDu+1JcGlIaZQgKNQ== -"@pagopa/io-functions-commons@^22.0.1": - version "22.0.1" - resolved "https://registry.yarnpkg.com/@pagopa/io-functions-commons/-/io-functions-commons-22.0.1.tgz#332243e405c29db4862780135525f968c667da47" - integrity sha512-9ZQ1N8PEZkSsceiP2SG2lTsO+sQxJG0QZpCPVjK/ulpkF0RSqiaFufTzUaSXxiNkaUMd+mYaeo92vfg2GWqxaA== +"@pagopa/io-functions-commons@^22.5.0": + version "22.5.0" + resolved "https://registry.yarnpkg.com/@pagopa/io-functions-commons/-/io-functions-commons-22.5.0.tgz#89491deb779f20772d8d5b4b72602bc805940baf" + integrity sha512-rQy9Il6YIONam5GtzgEb2s8YqT3fAlXrSTGwjKonRsD0r6MtWS7yq4zGFFQd5QzZ03o+aWzEyvP27L32rwfIwA== dependencies: "@azure/cosmos" "^3.11.5" "@pagopa/ts-commons" "^10.0.1" From cb5b0cc8dc89fada47d29b8593d9fb806cc8a81f Mon Sep 17 00:00:00 2001 From: AleDore Date: Mon, 20 Dec 2021 10:24:43 +0100 Subject: [PATCH 11/11] fix typo --- GetImpersonateService/function.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GetImpersonateService/function.json b/GetImpersonateService/function.json index 9adab5dd..e75ad282 100644 --- a/GetImpersonateService/function.json +++ b/GetImpersonateService/function.json @@ -16,5 +16,5 @@ "name": "res" } ], - "scri++ptFile": "../dist/GetImperonateService/index.js" + "scri++ptFile": "../dist/GetImpersonateService/index.js" }