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

[#IOPAE-203] Add UpdateSubscriptionCidrs #218

Merged
merged 8 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from 7 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
217 changes: 217 additions & 0 deletions UpdateSubscriptionCidrs/__tests__/handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// eslint-disable @typescript-eslint/no-explicit-any
import { ApiManagementClient } from "@azure/arm-apimanagement";
import { SubscriptionContract } from "@azure/arm-apimanagement/esm/models";
import * as E from "fp-ts/lib/Either";
import * as TE from "fp-ts/lib/TaskEither";
import * as ApimUtils from "../../utils/apim";
import { IAzureApimConfig, IServicePrincipalCreds } from "../../utils/apim";
import { UpdateSubscriptionCidrsHandler } from "../handler";
import { CIDR } from "@pagopa/ts-commons/lib/strings";
import { none } from "fp-ts/lib/Option";
import { SubscriptionCIDRsModel } from "@pagopa/io-functions-commons/dist/src/models/subscription_cidrs";
import {
CosmosErrors,
toCosmosErrorResponse
} from "@pagopa/io-functions-commons/dist/src/utils/cosmosdb_model";
import { CIDRsPayload } from "../../generated/definitions/CIDRsPayload";
import { toAuthorizedCIDRs } from "@pagopa/io-functions-commons/dist/src/models/service";

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 fakeSubscriptionOwnerId = "5931a75ae4bbd512a88c680b";
const fakeFullPathSubscriptionOwnerId =
"/subscriptions/subid/resourceGroups/{resourceGroup}/providers/Microsoft.ApiManagement/service/{apimService}/users/" +
fakeSubscriptionOwnerId;

const aValidSubscription: SubscriptionContract = {
allowTracing: false,
createdDate: new Date(),
displayName: undefined,
endDate: undefined,
expirationDate: undefined,
id: "12345",
name: undefined,
notificationDate: undefined,
ownerId: fakeFullPathSubscriptionOwnerId,
primaryKey: "a-primary-key",
scope: "/apis",
secondaryKey: "a-secondary-key",
startDate: new Date(),
state: "active",
stateComment: undefined,
type: undefined
};

const aCIDRsPayload = [("1.2.3.4/5" as any) as CIDR] as any;

const mockSubscription = jest.fn();

const mockApiManagementClient = ApiManagementClient as jest.Mock;
mockApiManagementClient.mockImplementation(() => ({
subscription: {
get: mockSubscription
}
}));

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("UpdateSubscriptionCidrs", () => {
it("should return an internal error response if the API management client can not be got", async () => {
spyOnGetApiClient.mockImplementationOnce(() =>
TE.left(Error("Error on APIM client creation"))
);
const mockSubscriptionCIDRsModel = {
upsert: jest.fn(() => {
return TE.right(none);
})
};

const updateSubscriptionCidrs = UpdateSubscriptionCidrsHandler(
fakeServicePrincipalCredentials,
fakeApimConfig,
(mockSubscriptionCIDRsModel as any) as SubscriptionCIDRsModel
);

const response = await updateSubscriptionCidrs(
mockedContext as any,
undefined as any,
undefined as any,
undefined as any
);

expect(response.kind).toEqual("IResponseErrorInternal");
expect(mockSubscriptionCIDRsModel.upsert).not.toBeCalled();
});

it("should return a not found error response if the apiclient get subscription returns an error", async () => {
mockApiManagementClient.mockImplementation(() => ({
subscription: {
get: jest.fn(() => {
return Promise.reject(new Error("error"));
})
}
}));

const mockSubscriptionCIDRsModel = {
upsert: jest.fn(() => {
return TE.right(none);
})
};

const updateSubscriptionCidrs = UpdateSubscriptionCidrsHandler(
fakeServicePrincipalCredentials,
fakeApimConfig,
(mockSubscriptionCIDRsModel as any) as SubscriptionCIDRsModel
);

const response = await updateSubscriptionCidrs(
mockedContext as any,
undefined as any,
undefined as any,
undefined as any
);

expect(response.kind).toEqual("IResponseErrorNotFound");
expect(mockSubscriptionCIDRsModel.upsert).not.toBeCalled();
});

it("should return an error query response if cosmos returns an error", async () => {
mockApiManagementClient.mockImplementation(() => ({
subscription: {
get: jest.fn(() =>
Promise.resolve({
...((aValidSubscription as any) as SubscriptionContract)
})
)
}
}));

const mockSubscriptionCIDRsModel = {
upsert: jest.fn(() => {
return TE.left(
Promise.reject(toCosmosErrorResponse("db error") as CosmosErrors)
);
})
};

const updateSubscriptionCidrs = UpdateSubscriptionCidrsHandler(
fakeServicePrincipalCredentials,
fakeApimConfig,
(mockSubscriptionCIDRsModel as any) as SubscriptionCIDRsModel
);

const response = await updateSubscriptionCidrs(
mockedContext as any,
undefined as any,
undefined as any,
aCIDRsPayload
);

expect(response.kind).toEqual("IResponseErrorQuery");
expect(mockSubscriptionCIDRsModel.upsert).toBeCalledTimes(1);
});

it("should return an updated CIDRsPayload", async () => {
mockApiManagementClient.mockImplementation(() => ({
subscription: {
get: jest.fn(() =>
Promise.resolve({
...((aValidSubscription as any) as SubscriptionContract)
})
)
}
}));

const mockSubscriptionCIDRsModel = {
upsert: jest.fn(() => {
return TE.right({
cidrs: (["1.2.3.4/5"] as unknown) as CIDR[],
subscriptionId: "aSubscriptionId"
});
})
};

const updateSubscriptionCidrs = UpdateSubscriptionCidrsHandler(
fakeServicePrincipalCredentials,
fakeApimConfig,
(mockSubscriptionCIDRsModel as any) as SubscriptionCIDRsModel
);

const response = await updateSubscriptionCidrs(
mockedContext as any,
undefined as any,
undefined as any,
aCIDRsPayload
);

expect(mockSubscriptionCIDRsModel.upsert).toBeCalledTimes(1);
expect(response).toEqual({
apply: expect.any(Function),
kind: "IResponseSuccessJson",
value: aCIDRsPayload
});
expect(
E.isRight(CIDRsPayload.decode((response as any).value))
).toBeTruthy();
});
});
20 changes: 20 additions & 0 deletions UpdateSubscriptionCidrs/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"bindings": [
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"route": "adm/subscriptions/{subscriptionid}/cidrs",
"methods": [
"put"
]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
],
"scriptFile": "../dist/UpdateSubscriptionCidrs/index.js"
}
149 changes: 149 additions & 0 deletions UpdateSubscriptionCidrs/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { Context } from "@azure/functions";
import * as express from "express";
import * as E from "fp-ts/lib/Either";
import * as TE from "fp-ts/lib/TaskEither";
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,
ResponseErrorInternal,
ResponseErrorNotFound,
ResponseSuccessJson
} from "@pagopa/ts-commons/lib/responses";
import { NonEmptyString } from "@pagopa/ts-commons/lib/strings";

import { RequiredBodyPayloadMiddleware } from "@pagopa/io-functions-commons/dist/src/utils/middlewares/required_body_payload";
import {
IResponseErrorQuery,
ResponseErrorQuery
} from "@pagopa/io-functions-commons/dist/src/utils/response";
import { toAuthorizedCIDRs } from "@pagopa/io-functions-commons/dist/src/models/service";
import { SubscriptionCIDRsModel } from "@pagopa/io-functions-commons/dist/src/models/subscription_cidrs";
import { pipe } from "fp-ts/lib/function";
import {
getApiClient,
IAzureApimConfig,
IServicePrincipalCreds
} from "../utils/apim";
import { CIDRsPayload } from "../generated/definitions/CIDRsPayload";

type IUpdateSubscriptionCidrsHandler = (
context: Context,
auth: IAzureApiAuthorization,
subscriptionid: NonEmptyString,
cidrsPayload: CIDRsPayload
) => Promise<
| IResponseSuccessJson<CIDRsPayload>
| IResponseErrorInternal
| IResponseErrorNotFound
| IResponseErrorQuery
>;

const subscriptionExists = (
servicePrincipalCreds: IServicePrincipalCreds,
azureApimConfig: IAzureApimConfig,
subscriptionId: NonEmptyString
): TE.TaskEither<IResponseErrorInternal | IResponseErrorNotFound, true> =>
pipe(
getApiClient(servicePrincipalCreds, azureApimConfig.subscriptionId),
TE.mapLeft(_ =>
ResponseErrorInternal("Error trying to get Api Management Client")
),
TE.chainW(apiClient =>
pipe(
TE.tryCatch(
() =>
apiClient.subscription.get(
azureApimConfig.apimResourceGroup,
azureApimConfig.apim,
subscriptionId
),
E.toError
),
TE.mapLeft(_ =>
ResponseErrorNotFound(
"Subscription not found",
"Error trying to get APIM Subscription"
)
),
TE.map(_ => true)
)
)
);

// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
export function UpdateSubscriptionCidrsHandler(
servicePrincipalCreds: IServicePrincipalCreds,
azureApimConfig: IAzureApimConfig,
subscriptionCIDRsModel: SubscriptionCIDRsModel
): IUpdateSubscriptionCidrsHandler {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
return async (context, _, subscriptionId, cidrs) => {
const maybeSubscriptionExists = await subscriptionExists(
servicePrincipalCreds,
azureApimConfig,
subscriptionId
)();
if (E.isLeft(maybeSubscriptionExists)) {
return maybeSubscriptionExists.left;
}

const errorOrMaybeUpdatedSubscriptionCIDRs = await subscriptionCIDRsModel.upsert(
{
cidrs: toAuthorizedCIDRs(Array.from(cidrs)),
kind: "INewSubscriptionCIDRs",
subscriptionId
}
)();
giuseppedipinto marked this conversation as resolved.
Show resolved Hide resolved
if (E.isLeft(errorOrMaybeUpdatedSubscriptionCIDRs)) {
return ResponseErrorQuery(
"Error trying to update subscription cidrs",
errorOrMaybeUpdatedSubscriptionCIDRs.left
);
}

const updatedSubscriptionCIDRs = errorOrMaybeUpdatedSubscriptionCIDRs.right;

return ResponseSuccessJson(Array.from(updatedSubscriptionCIDRs.cidrs));
};
}

/**
* Wraps an UpdateSubscriptionCidrs handler inside an Express request handler.
*
* **IMPORTANT:** This handler should be used only for *MANAGE Flow*
*/
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
export function UpdateSubscriptionCidrs(
servicePrincipalCreds: IServicePrincipalCreds,
azureApimConfig: IAzureApimConfig,
subscriptionCIDRsModel: SubscriptionCIDRsModel
): express.RequestHandler {
const handler = UpdateSubscriptionCidrsHandler(
servicePrincipalCreds,
azureApimConfig,
subscriptionCIDRsModel
);

const middlewaresWrap = withRequestMiddlewares(
// Extract Azure Functions bindings
ContextMiddleware(),
// Allow only users in the ApiUserAdmin group
AzureApiAuthMiddleware(new Set([UserGroup.ApiUserAdmin])),
// Extract the subscription id value from the request
RequiredParamMiddleware("subscriptionid", NonEmptyString),
// Extract the body payload from the request
RequiredBodyPayloadMiddleware(CIDRsPayload)
);

return wrapRequestHandler(middlewaresWrap(handler));
}
Loading