From 105b3ff730f8ee0d47db0ea96163bef427d29d7d Mon Sep 17 00:00:00 2001 From: Giuseppe Di Pinto Date: Fri, 10 Mar 2023 11:46:58 +0100 Subject: [PATCH 1/8] Update GetSubscription and Add UpdateSubscriptionCidrs --- GetSubscription/__tests__/handler.test.ts | 100 +++++++- GetSubscription/handler.ts | 56 ++++- GetSubscription/index.ts | 19 +- .../__tests__/handler.test.ts | 217 ++++++++++++++++++ UpdateSubscriptionCidrs/function.json | 20 ++ UpdateSubscriptionCidrs/handler.ts | 126 ++++++++++ UpdateSubscriptionCidrs/index.ts | 72 ++++++ openapi/index.yaml | 46 ++++ package.json | 4 +- yarn.lock | 76 ++++-- 10 files changed, 695 insertions(+), 41 deletions(-) create mode 100644 UpdateSubscriptionCidrs/__tests__/handler.test.ts create mode 100644 UpdateSubscriptionCidrs/function.json create mode 100644 UpdateSubscriptionCidrs/handler.ts create mode 100644 UpdateSubscriptionCidrs/index.ts diff --git a/GetSubscription/__tests__/handler.test.ts b/GetSubscription/__tests__/handler.test.ts index f20eecf8..87d6ce09 100644 --- a/GetSubscription/__tests__/handler.test.ts +++ b/GetSubscription/__tests__/handler.test.ts @@ -2,6 +2,7 @@ import { ApiManagementClient } from "@azure/arm-apimanagement"; import { SubscriptionContract } from "@azure/arm-apimanagement/esm/models"; import * as E from "fp-ts/lib/Either"; +import * as O from "fp-ts/lib/Option"; import * as TE from "fp-ts/lib/TaskEither"; import * as ApimUtils from "../../utils/apim"; import { @@ -13,6 +14,12 @@ import { GetSubscriptionHandler } from "../handler"; import { SubscriptionWithoutKeys } from "../../generated/definitions/SubscriptionWithoutKeys"; import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; import { RestError } from "@azure/ms-rest-js"; +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"; jest.mock("@azure/arm-apimanagement"); jest.mock("@azure/graph"); @@ -78,10 +85,16 @@ describe("GetSubscription", () => { spyOnGetApiClient.mockImplementationOnce(() => TE.left(Error("Error from ApiManagementClient constructor")) ); + const mockSubscriptionCIDRsModel = { + findLastVersionByModelId: jest.fn(() => { + return TE.right(none); + }) + }; const getSubscriptionHandler = GetSubscriptionHandler( fakeServicePrincipalCredentials, - fakeApimConfig + fakeApimConfig, + (mockSubscriptionCIDRsModel as any) as SubscriptionCIDRsModel ); const response = await getSubscriptionHandler( @@ -91,16 +104,56 @@ describe("GetSubscription", () => { ); expect(response.kind).toEqual("IResponseErrorInternal"); + expect( + mockSubscriptionCIDRsModel.findLastVersionByModelId + ).not.toBeCalled(); }); it("should return a not found error response if the API management client doesn't retrieve a subscription", async () => { mockSubscription.mockImplementation(() => Promise.reject(new RestError("not found", "Not Found", 404)) ); + const mockSubscriptionCIDRsModel = { + findLastVersionByModelId: jest.fn(() => { + return TE.right(none); + }) + }; + + const getSubscriptionHandler = GetSubscriptionHandler( + fakeServicePrincipalCredentials, + fakeApimConfig, + (mockSubscriptionCIDRsModel as any) as SubscriptionCIDRsModel + ); + + const response = await getSubscriptionHandler( + mockedContext as any, + undefined as any, + undefined as any + ); + + expect(response.kind).toEqual("IResponseErrorNotFound"); + expect( + mockSubscriptionCIDRsModel.findLastVersionByModelId + ).not.toBeCalled(); + }); + + it("should return an internal server error response if the Subscription CIDRs model return a CosmosError", async () => { + mockSubscription.mockImplementationOnce(() => { + const apimResponse = aValidSubscription; + return Promise.resolve(apimResponse); + }); + const mockSubscriptionCIDRsModel = { + findLastVersionByModelId: jest.fn(() => + TE.left( + Promise.reject(toCosmosErrorResponse("db error") as CosmosErrors) + ) + ) + }; const getSubscriptionHandler = GetSubscriptionHandler( fakeServicePrincipalCredentials, - fakeApimConfig + fakeApimConfig, + (mockSubscriptionCIDRsModel as any) as SubscriptionCIDRsModel ); const response = await getSubscriptionHandler( @@ -109,18 +162,54 @@ describe("GetSubscription", () => { undefined as any ); + expect(mockSubscriptionCIDRsModel.findLastVersionByModelId).toBeCalledTimes( + 1 + ); + expect(response.kind).toEqual("IResponseErrorInternal"); + }); + + it("should return a not found error response if the Subscription CIDRs model return a None", async () => { + mockSubscription.mockImplementationOnce(() => { + const apimResponse = aValidSubscription; + return Promise.resolve(apimResponse); + }); + const mockSubscriptionCIDRsModel = { + findLastVersionByModelId: jest.fn(() => TE.of(O.none)) + }; + + const getSubscriptionHandler = GetSubscriptionHandler( + fakeServicePrincipalCredentials, + fakeApimConfig, + (mockSubscriptionCIDRsModel as any) as SubscriptionCIDRsModel + ); + + const response = await getSubscriptionHandler( + mockedContext as any, + undefined as any, + undefined as any + ); + + expect(mockSubscriptionCIDRsModel.findLastVersionByModelId).toBeCalledTimes( + 1 + ); expect(response.kind).toEqual("IResponseErrorNotFound"); }); - it("should return subscription information", async () => { + it("should return subscription information with related cidrs", async () => { mockSubscription.mockImplementationOnce(() => { const apimResponse = aValidSubscription; return Promise.resolve(apimResponse); }); + const mockSubscriptionCIDRsModel = { + findLastVersionByModelId: jest.fn(() => { + return TE.right(O.some({ subscriptionId: "12345", cidrs: [] })); + }) + }; const getSubscriptionHandler = GetSubscriptionHandler( fakeServicePrincipalCredentials, - fakeApimConfig + fakeApimConfig, + (mockSubscriptionCIDRsModel as any) as SubscriptionCIDRsModel ); const response = await getSubscriptionHandler( @@ -137,7 +226,8 @@ describe("GetSubscription", () => { owner_id: parseOwnerIdFullPath( aValidSubscription.ownerId as NonEmptyString ), - scope: aValidSubscription.scope + scope: aValidSubscription.scope, + authorized_cidrs: [] } }); expect( diff --git a/GetSubscription/handler.ts b/GetSubscription/handler.ts index 1631fc7c..21086ea2 100644 --- a/GetSubscription/handler.ts +++ b/GetSubscription/handler.ts @@ -22,6 +22,9 @@ import { import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; import { pipe } from "fp-ts/lib/function"; +import { SubscriptionGetResponse } from "@azure/arm-apimanagement/esm/models"; +import { SubscriptionCIDRsModel } from "@pagopa/io-functions-commons/dist/src/models/subscription_cidrs"; +import { isSome } from "fp-ts/lib/Option"; import { SubscriptionWithoutKeys } from "../generated/definitions/SubscriptionWithoutKeys"; import { getApiClient, @@ -40,10 +43,44 @@ type IGetSubscriptionHandler = ( | IResponseErrorNotFound >; +const getSubscriptionCIDRs = ( + subscriptionId: string, + subscription: SubscriptionGetResponse +) => ( + subscriptionCIDRsModel: SubscriptionCIDRsModel +): Promise< + | IResponseSuccessJson + | IResponseErrorInternal + | IResponseErrorNotFound +> => + pipe( + subscriptionCIDRsModel.findLastVersionByModelId([ + subscriptionId as NonEmptyString + ]), + TE.map(subscriptionCIDRs => + isSome(subscriptionCIDRs) + ? ResponseSuccessJson({ + authorized_cidrs: Array.from(subscriptionCIDRs.value.cidrs), + id: subscription.id, + owner_id: parseOwnerIdFullPath( + subscription.ownerId as NonEmptyString + ), + scope: subscription.scope + } as SubscriptionWithoutKeys) + : ResponseErrorNotFound( + "Not found", + "The required document does not exist" + ) + ), + TE.mapLeft(_ => ResponseErrorInternal(`Internal server error - db error`)), + TE.toUnion + )(); + // eslint-disable-next-line prefer-arrow/prefer-arrow-functions export function GetSubscriptionHandler( servicePrincipalCreds: IServicePrincipalCreds, - azureApimConfig: IAzureApimConfig + azureApimConfig: IAzureApimConfig, + subscriptionCIDRsModel: SubscriptionCIDRsModel ): IGetSubscriptionHandler { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type return async (context, _, subscriptionId) => @@ -61,13 +98,10 @@ export function GetSubscriptionHandler( ) ), TE.map(subscription => - ResponseSuccessJson({ - id: subscription.id, - owner_id: parseOwnerIdFullPath( - subscription.ownerId as NonEmptyString - ), - scope: subscription.scope - }) + getSubscriptionCIDRs( + subscriptionId, + subscription + )(subscriptionCIDRsModel) ), TE.mapLeft(error => { context.log.error(error); @@ -91,11 +125,13 @@ export function GetSubscriptionHandler( // eslint-disable-next-line prefer-arrow/prefer-arrow-functions export function GetSubscription( servicePrincipalCreds: IServicePrincipalCreds, - azureApimConfig: IAzureApimConfig + azureApimConfig: IAzureApimConfig, + subscriptionCIDRsModel: SubscriptionCIDRsModel ): express.RequestHandler { const handler = GetSubscriptionHandler( servicePrincipalCreds, - azureApimConfig + azureApimConfig, + subscriptionCIDRsModel ); const middlewaresWrap = withRequestMiddlewares( diff --git a/GetSubscription/index.ts b/GetSubscription/index.ts index 61a3e875..50280b23 100644 --- a/GetSubscription/index.ts +++ b/GetSubscription/index.ts @@ -8,8 +8,13 @@ import { AzureContextTransport } from "@pagopa/io-functions-commons/dist/src/uti import { setAppContext } from "@pagopa/io-functions-commons/dist/src/utils/middlewares/context_middleware"; import createAzureFunctionHandler from "@pagopa/express-azure-functions/dist/src/createAzureFunctionsHandler"; +import { + SUBSCRIPTION_CIDRS_COLLECTION_NAME, + SubscriptionCIDRsModel +} from "@pagopa/io-functions-commons/dist/src/models/subscription_cidrs"; import { getConfigOrThrow } from "../utils/config"; +import { cosmosdbClient } from "../utils/cosmosdb"; import { GetSubscription } from "./handler"; const config = getConfigOrThrow(); @@ -25,6 +30,14 @@ const azureApimConfig = { subscriptionId: config.AZURE_SUBSCRIPTION_ID }; +const subscriptionCIDRsContainer = cosmosdbClient + .database(config.COSMOSDB_NAME) + .container(SUBSCRIPTION_CIDRS_COLLECTION_NAME); + +const subscriptionCIDRsModel = new SubscriptionCIDRsModel( + subscriptionCIDRsContainer +); + // eslint-disable-next-line functional/no-let let logger: Context["log"] | undefined; const contextTransport = new AzureContextTransport(() => logger, { @@ -39,7 +52,11 @@ secureExpressApp(app); // Add express route app.get( "/adm/subscriptions/:subscriptionid", - GetSubscription(servicePrincipalCreds, azureApimConfig) + GetSubscription( + servicePrincipalCreds, + azureApimConfig, + subscriptionCIDRsModel + ) ); const azureFunctionHandler = createAzureFunctionHandler(app); diff --git a/UpdateSubscriptionCidrs/__tests__/handler.test.ts b/UpdateSubscriptionCidrs/__tests__/handler.test.ts new file mode 100644 index 00000000..efe3f5fa --- /dev/null +++ b/UpdateSubscriptionCidrs/__tests__/handler.test.ts @@ -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 an internal 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("IResponseErrorInternal"); + 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(); + }); +}); diff --git a/UpdateSubscriptionCidrs/function.json b/UpdateSubscriptionCidrs/function.json new file mode 100644 index 00000000..1b811bf2 --- /dev/null +++ b/UpdateSubscriptionCidrs/function.json @@ -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" +} \ No newline at end of file diff --git a/UpdateSubscriptionCidrs/handler.ts b/UpdateSubscriptionCidrs/handler.ts new file mode 100644 index 00000000..0ad4fe57 --- /dev/null +++ b/UpdateSubscriptionCidrs/handler.ts @@ -0,0 +1,126 @@ +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, + 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 { + getApiClient, + IAzureApimConfig, + IServicePrincipalCreds +} from "../utils/apim"; +import { CIDRsPayload } from "../generated/definitions/CIDRsPayload"; + +type IUpdateSubscriptionCidrsHandler = ( + context: Context, + auth: IAzureApiAuthorization, + subscriptionid: NonEmptyString, + cidrsPayload: CIDRsPayload +) => Promise< + | IResponseSuccessJson + | IResponseErrorInternal + | IResponseErrorNotFound + | IResponseErrorQuery +>; + +// 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 errorOrApiManagementClient = await getApiClient( + servicePrincipalCreds, + azureApimConfig.subscriptionId + )(); + if (E.isLeft(errorOrApiManagementClient)) { + return ResponseErrorInternal("Error trying to get Api Management Client"); + } + + const apiClient = errorOrApiManagementClient.right; + const errorOrSubscriptionResponse = await TE.tryCatch( + () => + apiClient.subscription.get( + azureApimConfig.apimResourceGroup, + azureApimConfig.apim, + subscriptionId + ), + E.toError + )(); + if (E.isLeft(errorOrSubscriptionResponse)) { + return ResponseErrorInternal("Error trying to get APIM Subscription"); + } + + const errorOrMaybeUpdatedSubscriptionCIDRs = await subscriptionCIDRsModel.upsert( + { + cidrs: toAuthorizedCIDRs(Array.from(cidrs)), + kind: "INewSubscriptionCIDRs", + subscriptionId + } + )(); + 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. + */ +// 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)); +} diff --git a/UpdateSubscriptionCidrs/index.ts b/UpdateSubscriptionCidrs/index.ts new file mode 100644 index 00000000..2eb74724 --- /dev/null +++ b/UpdateSubscriptionCidrs/index.ts @@ -0,0 +1,72 @@ +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 { + SUBSCRIPTION_CIDRS_COLLECTION_NAME, + SubscriptionCIDRsModel +} from "@pagopa/io-functions-commons/dist/src/models/subscription_cidrs"; + +import { getConfigOrThrow } from "../utils/config"; +import { cosmosdbClient } from "../utils/cosmosdb"; +import { UpdateSubscriptionCidrs } 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 +}; + +const subscriptionCIDRsContainer = cosmosdbClient + .database(config.COSMOSDB_NAME) + .container(SUBSCRIPTION_CIDRS_COLLECTION_NAME); + +const subscriptionCIDRsModel = new SubscriptionCIDRsModel( + subscriptionCIDRsContainer +); + +// 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.put( + "/adm/subscriptions/:subscriptionid/cidrs", + UpdateSubscriptionCidrs( + servicePrincipalCreds, + azureApimConfig, + subscriptionCIDRsModel + ) +); + +const azureFunctionHandler = createAzureFunctionHandler(app); + +// Binds the express app to an Azure Function handler +// eslint-disable-next-line prefer-arrow/prefer-arrow-functions +function httpStart(context: Context): void { + logger = context.log; + setAppContext(app, context); + azureFunctionHandler(context); +} + +export default httpStart; diff --git a/openapi/index.yaml b/openapi/index.yaml index 3a027086..d76794b3 100644 --- a/openapi/index.yaml +++ b/openapi/index.yaml @@ -542,6 +542,40 @@ paths: description: Subscription not found "500": description: Internal server error + /subscriptions/{subscriptionId}/cidrs: + put: + summary: Update Subscription CIDRs + description: Update authorized cidrs for a Subscription + operationId: updateSubscriptionCidrs + parameters: + - name: subscriptionId + in: path + type: string + required: true + description: The id of the Subscription + - name: body + in: body + required: true + schema: + $ref: "#/definitions/CIDRsPayload" + responses: + "200": + description: The subscription CIDRs updated + schema: + $ref: "#/definitions/CIDRsPayload" + "400": + description: Bad request + "401": + description: Unauthorized + "403": + description: Forbidden + "404": + description: Resource (User or Product) not found + "429": + description: Too Many Requests + "500": + description: Internal server error + definitions: DevelopmentProfile: type: object @@ -802,8 +836,20 @@ definitions: type: string owner_id: type: string + authorized_cidrs: + description: |- + Allowed source IPs or CIDRs for this subscription. + When empty, every IP address is authorized. + type: array + items: + $ref: "#/definitions/CIDR" required: - scope + - authorized_cidrs + CIDRsPayload: + type: array + items: + $ref: "#/definitions/CIDR" SubscriptionState: type: string x-extensible-enum: diff --git a/package.json b/package.json index 676dc6c2..c7382aee 100644 --- a/package.json +++ b/package.json @@ -54,13 +54,13 @@ }, "dependencies": { "@azure/arm-apimanagement": "^5.1.1", - "@azure/cosmos": "^3.15.1", + "@azure/cosmos": "^3.17.1", "@azure/graph": "^4.0.1", "@azure/ms-rest-js": "^2.6.4", "@azure/ms-rest-nodeauth": "^2.0.6", "@pagopa/express-azure-functions": "^2.0.0", "@pagopa/io-backend-session-sdk": "x", - "@pagopa/io-functions-commons": "^25.5.2", + "@pagopa/io-functions-commons": "^27.4.0", "@pagopa/ts-commons": "^10.9.0", "@types/archiver": "^3.1.1", "@types/randomstring": "^1.1.6", diff --git a/yarn.lock b/yarn.lock index cca6537b..26f6ea54 100644 --- a/yarn.lock +++ b/yarn.lock @@ -127,13 +127,22 @@ "@opentelemetry/api" "^1.0.1" tslib "^2.2.0" -"@azure/cosmos@^3.15.1": - version "3.15.1" - resolved "https://registry.yarnpkg.com/@azure/cosmos/-/cosmos-3.15.1.tgz#5b206f061cdf0bb387cf92ac13e25c9190f929c4" - integrity sha512-Pjn9CcoipUSsm/r9ZqAeyrqIQnh0AOeIDwYs/3pcLumK9ezCVFrfeLoOfas4Dm2X8bwL+/9puKAM55LJEaQwWg== +"@azure/core-tracing@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@azure/core-tracing/-/core-tracing-1.0.1.tgz#352a38cbea438c4a83c86b314f48017d70ba9503" + integrity sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw== + dependencies: + tslib "^2.2.0" + +"@azure/cosmos@^3.17.1": + version "3.17.3" + resolved "https://registry.yarnpkg.com/@azure/cosmos/-/cosmos-3.17.3.tgz#380398496af8ef3473ae0a9ad8cdbab32d91eb08" + integrity sha512-wBglkQ6Irjv5Vo2iw8fd6eYj60WYRSSg4/0DBkeOP6BwQ4RA91znsOHd6s3qG6UAbNgYuzC9Nnq07vlFFZkHEw== dependencies: + "@azure/abort-controller" "^1.0.0" "@azure/core-auth" "^1.3.0" "@azure/core-rest-pipeline" "^1.2.0" + "@azure/core-tracing" "^1.0.0" debug "^4.1.1" fast-json-stable-stringify "^2.1.0" jsbi "^3.1.3" @@ -863,13 +872,13 @@ fp-ts "^2.10.5" io-ts "^2.2.16" -"@pagopa/io-functions-commons@^25.5.2": - version "25.5.2" - resolved "https://registry.yarnpkg.com/@pagopa/io-functions-commons/-/io-functions-commons-25.5.2.tgz#5bb04ded9551100ba2477ee6207735e82362368e" - integrity sha512-+nXvYuK0Oc/KA3TBsXGDudNTycWH/5dzFwmd2QlBsv6lDc02h5ILw3R3RsqUrdx3ts03RfDfKejoBu9Misr9YQ== +"@pagopa/io-functions-commons@^27.4.0": + version "27.4.0" + resolved "https://registry.yarnpkg.com/@pagopa/io-functions-commons/-/io-functions-commons-27.4.0.tgz#fe0fa128954286a7aec665aeeb21e73d6e54d02b" + integrity sha512-M8g89OM1w4KB7VLPntyc+vRO+tuZRXlKK6GtI8+4G2H3XihiidlXdia+CUv3sqKs4FhwWfnY8GFrc75nt1GcYQ== dependencies: - "@azure/cosmos" "^3.15.1" - "@pagopa/ts-commons" "^10.5.0" + "@azure/cosmos" "^3.17.1" + "@pagopa/ts-commons" "^10.13.0" applicationinsights "^1.8.10" azure-storage "^2.10.5" cidr-matcher "^2.1.1" @@ -885,7 +894,7 @@ remark-frontmatter "^2.0.0" remark-parse "^5.0.0" remark-rehype "^3.0.0" - request-ip "^2.1.3" + request-ip "^3.3.0" ulid "^2.3.0" unified "^9.2.2" winston "^3.1.0" @@ -918,7 +927,23 @@ node-fetch "^2.6.0" validator "^10.1.0" -"@pagopa/ts-commons@^10.5.0", "@pagopa/ts-commons@^10.9.0": +"@pagopa/ts-commons@^10.13.0": + version "10.14.2" + resolved "https://registry.yarnpkg.com/@pagopa/ts-commons/-/ts-commons-10.14.2.tgz#a27bdbe00a376d0f71e9d2859ebaedc1d16b6a10" + integrity sha512-rRSAbAnIZidEsM8Vto1TK0jOKOOs8MUFora72OSM0VM18RWp1gsVd3WC7JbPaw8vztX+wB7PINBJUdq66j+aow== + dependencies: + abort-controller "^3.0.0" + agentkeepalive "^4.1.4" + applicationinsights "^1.8.10" + fp-ts "^2.11.0" + io-ts "^2.2.16" + jose "^4.11.2" + json-set-map "^1.1.2" + node-fetch "^2.6.0" + semver "^7.3.7" + validator "^13.7.0" + +"@pagopa/ts-commons@^10.9.0": version "10.9.0" resolved "https://registry.yarnpkg.com/@pagopa/ts-commons/-/ts-commons-10.9.0.tgz#ca50f8f9b09499667b6f3532c897453918ed7c76" integrity sha512-hnWpA8Le8ylxKGGj+IALFBhOQDgLDl8vb3opKr3MHVdkcvm4GONZkotGnnSQ3Bh/tasgmPbeOa4wAHJSLTRmXQ== @@ -5109,11 +5134,6 @@ is-wsl@^2.1.1: dependencies: is-docker "^2.0.0" -is_js@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/is_js/-/is_js-0.9.0.tgz#0ab94540502ba7afa24c856aa985561669e9c52d" - integrity sha1-CrlFQFArp6+iTIVqqYVWFmnpxS0= - isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -5822,6 +5842,11 @@ jest@^24.9.0: import-local "^2.0.0" jest-cli "^24.9.0" +jose@^4.11.2: + version "4.13.1" + resolved "https://registry.yarnpkg.com/jose/-/jose-4.13.1.tgz#449111bb5ab171db85c03f1bd2cb1647ca06db1c" + integrity sha512-MSJQC5vXco5Br38mzaQKiq9mwt7lwj2eXpgpRyQYNHYt2lq1PjkWa7DLXX0WVcQLE9HhMh3jPiufS7fhJf+CLQ== + js-base64@^2.4.5: version "2.6.4" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" @@ -8166,12 +8191,10 @@ req-all@^0.1.0: resolved "https://registry.yarnpkg.com/req-all/-/req-all-0.1.0.tgz#130051e2ace58a02eacbfc9d448577a736a9273a" integrity sha1-EwBR4qzligLqy/ydRIV3pzapJzo= -request-ip@^2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/request-ip/-/request-ip-2.1.3.tgz#99ab2bafdeaf2002626e28083cb10597511d9e14" - integrity sha512-J3qdE/IhVM3BXkwMIVO4yFrvhJlU3H7JH16+6yHucadT4fePnR8dyh+vEs6FIx0S2x5TCt2ptiPfHcn0sqhbYQ== - dependencies: - is_js "^0.9.0" +request-ip@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/request-ip/-/request-ip-3.3.0.tgz#863451e8fec03847d44f223e30a5d63e369fa611" + integrity sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA== request-promise-core@1.1.4: version "1.1.4" @@ -8432,6 +8455,13 @@ semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: dependencies: lru-cache "^6.0.0" +semver@^7.3.7: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + dependencies: + lru-cache "^6.0.0" + send@0.17.1: version "0.17.1" resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" From 1ef4e2bf089e20f3a91108db7e22677b944fcf31 Mon Sep 17 00:00:00 2001 From: Giuseppe Di Pinto Date: Fri, 10 Mar 2023 12:39:47 +0100 Subject: [PATCH 2/8] fix MessageStatus build error in userData util --- utils/userData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/userData.ts b/utils/userData.ts index 6f0dde89..9e96ff6e 100644 --- a/utils/userData.ts +++ b/utils/userData.ts @@ -45,7 +45,7 @@ export const AllUserData = t.interface({ t.exact(MessageContentWithId), "MessageContentList" ), - messageStatuses: t.readonlyArray(t.exact(MessageStatus), "MessageStatusList"), + messageStatuses: t.readonlyArray(MessageStatus, "MessageStatusList"), messages: t.readonlyArray(t.exact(MessageWithoutContent), "MessageList"), messagesView: t.readonlyArray(t.exact(MessageView), "MessageViewList"), notificationStatuses: t.readonlyArray( From 25ccb7c0c7e6dd4f606a0b1abad3e6c21867320d Mon Sep 17 00:00:00 2001 From: Giuseppe Di Pinto Date: Fri, 10 Mar 2023 12:57:49 +0100 Subject: [PATCH 3/8] correct mock MessageStatusValueEnum with NotRejectedMessageStatusValueEnum --- __mocks__/mocks.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/__mocks__/mocks.ts b/__mocks__/mocks.ts index a1294a1b..69aae3b7 100644 --- a/__mocks__/mocks.ts +++ b/__mocks__/mocks.ts @@ -43,7 +43,7 @@ import { import { MessageBodyMarkdown } from "@pagopa/io-functions-commons/dist/generated/definitions/MessageBodyMarkdown"; import { MessageContent } from "@pagopa/io-functions-commons/dist/generated/definitions/MessageContent"; -import { MessageStatusValueEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/MessageStatusValue"; +import { NotRejectedMessageStatusValueEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/NotRejectedMessageStatusValue"; import { MessageSubject } from "@pagopa/io-functions-commons/dist/generated/definitions/MessageSubject"; import { NotificationChannelEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/NotificationChannel"; import { NotificationChannelStatusValueEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/NotificationChannelStatusValue"; @@ -243,7 +243,7 @@ const aComponents: Components = { const aStatus: Status = { archived: false, - processing: MessageStatusValueEnum.PROCESSED, + processing: NotRejectedMessageStatusValueEnum.PROCESSED, read: false }; @@ -333,7 +333,7 @@ export const aRetrievedNotificationStatus = { export const aSerializedMessageStatus = { messageId: aMessageId, - status: MessageStatusValueEnum.ACCEPTED, + status: NotRejectedMessageStatusValueEnum.ACCEPTED, updatedAt: new Date().toISOString() }; From d3495a86ba30d7a23f14d9b83076dfbb55166d35 Mon Sep 17 00:00:00 2001 From: Giuseppe Di Pinto Date: Mon, 13 Mar 2023 17:12:09 +0100 Subject: [PATCH 4/8] pr fixes --- GetSubscription/handler.ts | 14 ++++---- UpdateSubscriptionCidrs/handler.ts | 53 ++++++++++++++++++++---------- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/GetSubscription/handler.ts b/GetSubscription/handler.ts index 21086ea2..4d6b5a42 100644 --- a/GetSubscription/handler.ts +++ b/GetSubscription/handler.ts @@ -48,10 +48,9 @@ const getSubscriptionCIDRs = ( subscription: SubscriptionGetResponse ) => ( subscriptionCIDRsModel: SubscriptionCIDRsModel -): Promise< - | IResponseSuccessJson - | IResponseErrorInternal - | IResponseErrorNotFound +): TE.TaskEither< + IResponseErrorInternal, + IResponseErrorNotFound | IResponseSuccessJson > => pipe( subscriptionCIDRsModel.findLastVersionByModelId([ @@ -72,9 +71,8 @@ const getSubscriptionCIDRs = ( "The required document does not exist" ) ), - TE.mapLeft(_ => ResponseErrorInternal(`Internal server error - db error`)), - TE.toUnion - )(); + TE.mapLeft(_ => ResponseErrorInternal(`Internal server error - db error`)) + ); // eslint-disable-next-line prefer-arrow/prefer-arrow-functions export function GetSubscriptionHandler( @@ -97,7 +95,7 @@ export function GetSubscriptionHandler( E.toError ) ), - TE.map(subscription => + TE.chainW(subscription => getSubscriptionCIDRs( subscriptionId, subscription diff --git a/UpdateSubscriptionCidrs/handler.ts b/UpdateSubscriptionCidrs/handler.ts index 0ad4fe57..48ec72c9 100644 --- a/UpdateSubscriptionCidrs/handler.ts +++ b/UpdateSubscriptionCidrs/handler.ts @@ -27,6 +27,7 @@ import { } 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, @@ -46,6 +47,35 @@ type IUpdateSubscriptionCidrsHandler = ( | IResponseErrorQuery >; +const subscriptionExists = ( + servicePrincipalCreds: IServicePrincipalCreds, + azureApimConfig: IAzureApimConfig, + subscriptionId: NonEmptyString +): TE.TaskEither => + 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(_ => + ResponseErrorInternal("Error trying to get APIM Subscription") + ), + TE.map(_ => true) + ) + ) + ); + // eslint-disable-next-line prefer-arrow/prefer-arrow-functions export function UpdateSubscriptionCidrsHandler( servicePrincipalCreds: IServicePrincipalCreds, @@ -54,26 +84,13 @@ export function UpdateSubscriptionCidrsHandler( ): IUpdateSubscriptionCidrsHandler { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type return async (context, _, subscriptionId, cidrs) => { - const errorOrApiManagementClient = await getApiClient( + const maybeSubscriptionExists = await subscriptionExists( servicePrincipalCreds, - azureApimConfig.subscriptionId - )(); - if (E.isLeft(errorOrApiManagementClient)) { - return ResponseErrorInternal("Error trying to get Api Management Client"); - } - - const apiClient = errorOrApiManagementClient.right; - const errorOrSubscriptionResponse = await TE.tryCatch( - () => - apiClient.subscription.get( - azureApimConfig.apimResourceGroup, - azureApimConfig.apim, - subscriptionId - ), - E.toError + azureApimConfig, + subscriptionId )(); - if (E.isLeft(errorOrSubscriptionResponse)) { - return ResponseErrorInternal("Error trying to get APIM Subscription"); + if (E.isLeft(maybeSubscriptionExists)) { + return maybeSubscriptionExists.left; } const errorOrMaybeUpdatedSubscriptionCIDRs = await subscriptionCIDRsModel.upsert( From c0c7b9d49a76c4e112fddb2ff8bfdd9a4c49c036 Mon Sep 17 00:00:00 2001 From: Giuseppe Di Pinto Date: Tue, 14 Mar 2023 11:15:16 +0100 Subject: [PATCH 5/8] change UpdateSubscriptionCidrs ErrorNotFound --- UpdateSubscriptionCidrs/__tests__/handler.test.ts | 4 ++-- UpdateSubscriptionCidrs/handler.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/UpdateSubscriptionCidrs/__tests__/handler.test.ts b/UpdateSubscriptionCidrs/__tests__/handler.test.ts index efe3f5fa..38aae500 100644 --- a/UpdateSubscriptionCidrs/__tests__/handler.test.ts +++ b/UpdateSubscriptionCidrs/__tests__/handler.test.ts @@ -103,7 +103,7 @@ describe("UpdateSubscriptionCidrs", () => { expect(mockSubscriptionCIDRsModel.upsert).not.toBeCalled(); }); - it("should return an internal error response if the apiclient get subscription returns an error", async () => { + it("should return a not found error response if the apiclient get subscription returns an error", async () => { mockApiManagementClient.mockImplementation(() => ({ subscription: { get: jest.fn(() => { @@ -131,7 +131,7 @@ describe("UpdateSubscriptionCidrs", () => { undefined as any ); - expect(response.kind).toEqual("IResponseErrorInternal"); + expect(response.kind).toEqual("IResponseErrorNotFound"); expect(mockSubscriptionCIDRsModel.upsert).not.toBeCalled(); }); diff --git a/UpdateSubscriptionCidrs/handler.ts b/UpdateSubscriptionCidrs/handler.ts index 48ec72c9..df497653 100644 --- a/UpdateSubscriptionCidrs/handler.ts +++ b/UpdateSubscriptionCidrs/handler.ts @@ -16,6 +16,7 @@ import { IResponseErrorNotFound, IResponseSuccessJson, ResponseErrorInternal, + ResponseErrorNotFound, ResponseSuccessJson } from "@pagopa/ts-commons/lib/responses"; import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; @@ -51,7 +52,7 @@ const subscriptionExists = ( servicePrincipalCreds: IServicePrincipalCreds, azureApimConfig: IAzureApimConfig, subscriptionId: NonEmptyString -): TE.TaskEither => +): TE.TaskEither => pipe( getApiClient(servicePrincipalCreds, azureApimConfig.subscriptionId), TE.mapLeft(_ => @@ -69,7 +70,10 @@ const subscriptionExists = ( E.toError ), TE.mapLeft(_ => - ResponseErrorInternal("Error trying to get APIM Subscription") + ResponseErrorNotFound( + "Subscription not found", + "Error trying to get APIM Subscription" + ) ), TE.map(_ => true) ) From e62096f606686609dd6e443cccdb6be954e1f66b Mon Sep 17 00:00:00 2001 From: Giuseppe Di Pinto Date: Tue, 14 Mar 2023 11:21:09 +0100 Subject: [PATCH 6/8] Add comments on UpdateSubscriptionCidrs API --- UpdateSubscriptionCidrs/handler.ts | 2 ++ openapi/index.yaml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/UpdateSubscriptionCidrs/handler.ts b/UpdateSubscriptionCidrs/handler.ts index df497653..513b980e 100644 --- a/UpdateSubscriptionCidrs/handler.ts +++ b/UpdateSubscriptionCidrs/handler.ts @@ -119,6 +119,8 @@ export function UpdateSubscriptionCidrsHandler( /** * 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( diff --git a/openapi/index.yaml b/openapi/index.yaml index d76794b3..0bcf77ae 100644 --- a/openapi/index.yaml +++ b/openapi/index.yaml @@ -545,7 +545,7 @@ paths: /subscriptions/{subscriptionId}/cidrs: put: summary: Update Subscription CIDRs - description: Update authorized cidrs for a Subscription + description: Update authorized cidrs for a Subscription. **IMPORTANT:** This API should be used only for *MANAGE Flow*. operationId: updateSubscriptionCidrs parameters: - name: subscriptionId From f7c53ca1b05373dc43ba22bc5c7f97c4e2358818 Mon Sep 17 00:00:00 2001 From: Giuseppe Di Pinto Date: Tue, 14 Mar 2023 11:42:29 +0100 Subject: [PATCH 7/8] Rollback GetSubscription to remove cidrs management --- GetSubscription/__tests__/handler.test.ts | 100 ++-------------------- GetSubscription/handler.ts | 56 +++--------- GetSubscription/index.ts | 19 +--- openapi/index.yaml | 8 -- 4 files changed, 17 insertions(+), 166 deletions(-) diff --git a/GetSubscription/__tests__/handler.test.ts b/GetSubscription/__tests__/handler.test.ts index 87d6ce09..f20eecf8 100644 --- a/GetSubscription/__tests__/handler.test.ts +++ b/GetSubscription/__tests__/handler.test.ts @@ -2,7 +2,6 @@ import { ApiManagementClient } from "@azure/arm-apimanagement"; import { SubscriptionContract } from "@azure/arm-apimanagement/esm/models"; import * as E from "fp-ts/lib/Either"; -import * as O from "fp-ts/lib/Option"; import * as TE from "fp-ts/lib/TaskEither"; import * as ApimUtils from "../../utils/apim"; import { @@ -14,12 +13,6 @@ import { GetSubscriptionHandler } from "../handler"; import { SubscriptionWithoutKeys } from "../../generated/definitions/SubscriptionWithoutKeys"; import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; import { RestError } from "@azure/ms-rest-js"; -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"; jest.mock("@azure/arm-apimanagement"); jest.mock("@azure/graph"); @@ -85,16 +78,10 @@ describe("GetSubscription", () => { spyOnGetApiClient.mockImplementationOnce(() => TE.left(Error("Error from ApiManagementClient constructor")) ); - const mockSubscriptionCIDRsModel = { - findLastVersionByModelId: jest.fn(() => { - return TE.right(none); - }) - }; const getSubscriptionHandler = GetSubscriptionHandler( fakeServicePrincipalCredentials, - fakeApimConfig, - (mockSubscriptionCIDRsModel as any) as SubscriptionCIDRsModel + fakeApimConfig ); const response = await getSubscriptionHandler( @@ -104,56 +91,16 @@ describe("GetSubscription", () => { ); expect(response.kind).toEqual("IResponseErrorInternal"); - expect( - mockSubscriptionCIDRsModel.findLastVersionByModelId - ).not.toBeCalled(); }); it("should return a not found error response if the API management client doesn't retrieve a subscription", async () => { mockSubscription.mockImplementation(() => Promise.reject(new RestError("not found", "Not Found", 404)) ); - const mockSubscriptionCIDRsModel = { - findLastVersionByModelId: jest.fn(() => { - return TE.right(none); - }) - }; - - const getSubscriptionHandler = GetSubscriptionHandler( - fakeServicePrincipalCredentials, - fakeApimConfig, - (mockSubscriptionCIDRsModel as any) as SubscriptionCIDRsModel - ); - - const response = await getSubscriptionHandler( - mockedContext as any, - undefined as any, - undefined as any - ); - - expect(response.kind).toEqual("IResponseErrorNotFound"); - expect( - mockSubscriptionCIDRsModel.findLastVersionByModelId - ).not.toBeCalled(); - }); - - it("should return an internal server error response if the Subscription CIDRs model return a CosmosError", async () => { - mockSubscription.mockImplementationOnce(() => { - const apimResponse = aValidSubscription; - return Promise.resolve(apimResponse); - }); - const mockSubscriptionCIDRsModel = { - findLastVersionByModelId: jest.fn(() => - TE.left( - Promise.reject(toCosmosErrorResponse("db error") as CosmosErrors) - ) - ) - }; const getSubscriptionHandler = GetSubscriptionHandler( fakeServicePrincipalCredentials, - fakeApimConfig, - (mockSubscriptionCIDRsModel as any) as SubscriptionCIDRsModel + fakeApimConfig ); const response = await getSubscriptionHandler( @@ -162,54 +109,18 @@ describe("GetSubscription", () => { undefined as any ); - expect(mockSubscriptionCIDRsModel.findLastVersionByModelId).toBeCalledTimes( - 1 - ); - expect(response.kind).toEqual("IResponseErrorInternal"); - }); - - it("should return a not found error response if the Subscription CIDRs model return a None", async () => { - mockSubscription.mockImplementationOnce(() => { - const apimResponse = aValidSubscription; - return Promise.resolve(apimResponse); - }); - const mockSubscriptionCIDRsModel = { - findLastVersionByModelId: jest.fn(() => TE.of(O.none)) - }; - - const getSubscriptionHandler = GetSubscriptionHandler( - fakeServicePrincipalCredentials, - fakeApimConfig, - (mockSubscriptionCIDRsModel as any) as SubscriptionCIDRsModel - ); - - const response = await getSubscriptionHandler( - mockedContext as any, - undefined as any, - undefined as any - ); - - expect(mockSubscriptionCIDRsModel.findLastVersionByModelId).toBeCalledTimes( - 1 - ); expect(response.kind).toEqual("IResponseErrorNotFound"); }); - it("should return subscription information with related cidrs", async () => { + it("should return subscription information", async () => { mockSubscription.mockImplementationOnce(() => { const apimResponse = aValidSubscription; return Promise.resolve(apimResponse); }); - const mockSubscriptionCIDRsModel = { - findLastVersionByModelId: jest.fn(() => { - return TE.right(O.some({ subscriptionId: "12345", cidrs: [] })); - }) - }; const getSubscriptionHandler = GetSubscriptionHandler( fakeServicePrincipalCredentials, - fakeApimConfig, - (mockSubscriptionCIDRsModel as any) as SubscriptionCIDRsModel + fakeApimConfig ); const response = await getSubscriptionHandler( @@ -226,8 +137,7 @@ describe("GetSubscription", () => { owner_id: parseOwnerIdFullPath( aValidSubscription.ownerId as NonEmptyString ), - scope: aValidSubscription.scope, - authorized_cidrs: [] + scope: aValidSubscription.scope } }); expect( diff --git a/GetSubscription/handler.ts b/GetSubscription/handler.ts index 4d6b5a42..1631fc7c 100644 --- a/GetSubscription/handler.ts +++ b/GetSubscription/handler.ts @@ -22,9 +22,6 @@ import { import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; import { pipe } from "fp-ts/lib/function"; -import { SubscriptionGetResponse } from "@azure/arm-apimanagement/esm/models"; -import { SubscriptionCIDRsModel } from "@pagopa/io-functions-commons/dist/src/models/subscription_cidrs"; -import { isSome } from "fp-ts/lib/Option"; import { SubscriptionWithoutKeys } from "../generated/definitions/SubscriptionWithoutKeys"; import { getApiClient, @@ -43,42 +40,10 @@ type IGetSubscriptionHandler = ( | IResponseErrorNotFound >; -const getSubscriptionCIDRs = ( - subscriptionId: string, - subscription: SubscriptionGetResponse -) => ( - subscriptionCIDRsModel: SubscriptionCIDRsModel -): TE.TaskEither< - IResponseErrorInternal, - IResponseErrorNotFound | IResponseSuccessJson -> => - pipe( - subscriptionCIDRsModel.findLastVersionByModelId([ - subscriptionId as NonEmptyString - ]), - TE.map(subscriptionCIDRs => - isSome(subscriptionCIDRs) - ? ResponseSuccessJson({ - authorized_cidrs: Array.from(subscriptionCIDRs.value.cidrs), - id: subscription.id, - owner_id: parseOwnerIdFullPath( - subscription.ownerId as NonEmptyString - ), - scope: subscription.scope - } as SubscriptionWithoutKeys) - : ResponseErrorNotFound( - "Not found", - "The required document does not exist" - ) - ), - TE.mapLeft(_ => ResponseErrorInternal(`Internal server error - db error`)) - ); - // eslint-disable-next-line prefer-arrow/prefer-arrow-functions export function GetSubscriptionHandler( servicePrincipalCreds: IServicePrincipalCreds, - azureApimConfig: IAzureApimConfig, - subscriptionCIDRsModel: SubscriptionCIDRsModel + azureApimConfig: IAzureApimConfig ): IGetSubscriptionHandler { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type return async (context, _, subscriptionId) => @@ -95,11 +60,14 @@ export function GetSubscriptionHandler( E.toError ) ), - TE.chainW(subscription => - getSubscriptionCIDRs( - subscriptionId, - subscription - )(subscriptionCIDRsModel) + TE.map(subscription => + ResponseSuccessJson({ + id: subscription.id, + owner_id: parseOwnerIdFullPath( + subscription.ownerId as NonEmptyString + ), + scope: subscription.scope + }) ), TE.mapLeft(error => { context.log.error(error); @@ -123,13 +91,11 @@ export function GetSubscriptionHandler( // eslint-disable-next-line prefer-arrow/prefer-arrow-functions export function GetSubscription( servicePrincipalCreds: IServicePrincipalCreds, - azureApimConfig: IAzureApimConfig, - subscriptionCIDRsModel: SubscriptionCIDRsModel + azureApimConfig: IAzureApimConfig ): express.RequestHandler { const handler = GetSubscriptionHandler( servicePrincipalCreds, - azureApimConfig, - subscriptionCIDRsModel + azureApimConfig ); const middlewaresWrap = withRequestMiddlewares( diff --git a/GetSubscription/index.ts b/GetSubscription/index.ts index 50280b23..61a3e875 100644 --- a/GetSubscription/index.ts +++ b/GetSubscription/index.ts @@ -8,13 +8,8 @@ import { AzureContextTransport } from "@pagopa/io-functions-commons/dist/src/uti import { setAppContext } from "@pagopa/io-functions-commons/dist/src/utils/middlewares/context_middleware"; import createAzureFunctionHandler from "@pagopa/express-azure-functions/dist/src/createAzureFunctionsHandler"; -import { - SUBSCRIPTION_CIDRS_COLLECTION_NAME, - SubscriptionCIDRsModel -} from "@pagopa/io-functions-commons/dist/src/models/subscription_cidrs"; import { getConfigOrThrow } from "../utils/config"; -import { cosmosdbClient } from "../utils/cosmosdb"; import { GetSubscription } from "./handler"; const config = getConfigOrThrow(); @@ -30,14 +25,6 @@ const azureApimConfig = { subscriptionId: config.AZURE_SUBSCRIPTION_ID }; -const subscriptionCIDRsContainer = cosmosdbClient - .database(config.COSMOSDB_NAME) - .container(SUBSCRIPTION_CIDRS_COLLECTION_NAME); - -const subscriptionCIDRsModel = new SubscriptionCIDRsModel( - subscriptionCIDRsContainer -); - // eslint-disable-next-line functional/no-let let logger: Context["log"] | undefined; const contextTransport = new AzureContextTransport(() => logger, { @@ -52,11 +39,7 @@ secureExpressApp(app); // Add express route app.get( "/adm/subscriptions/:subscriptionid", - GetSubscription( - servicePrincipalCreds, - azureApimConfig, - subscriptionCIDRsModel - ) + GetSubscription(servicePrincipalCreds, azureApimConfig) ); const azureFunctionHandler = createAzureFunctionHandler(app); diff --git a/openapi/index.yaml b/openapi/index.yaml index 0bcf77ae..79281d41 100644 --- a/openapi/index.yaml +++ b/openapi/index.yaml @@ -836,16 +836,8 @@ definitions: type: string owner_id: type: string - authorized_cidrs: - description: |- - Allowed source IPs or CIDRs for this subscription. - When empty, every IP address is authorized. - type: array - items: - $ref: "#/definitions/CIDR" required: - scope - - authorized_cidrs CIDRsPayload: type: array items: From f90ada4126d5cae5f371bdc5cef9e0e46e19e69c Mon Sep 17 00:00:00 2001 From: Giuseppe Di Pinto <118166285+giuseppedipinto@users.noreply.github.com> Date: Tue, 14 Mar 2023 15:27:43 +0100 Subject: [PATCH 8/8] Add GetSubscriptionCidrs API (#219) --- .../__tests__/handler.test.ts | 98 +++++++++++++++++++ GetSubscriptionCidrs/function.json | 20 ++++ GetSubscriptionCidrs/handler.ts | 84 ++++++++++++++++ GetSubscriptionCidrs/index.ts | 57 +++++++++++ openapi/index.yaml | 40 ++++++++ 5 files changed, 299 insertions(+) create mode 100644 GetSubscriptionCidrs/__tests__/handler.test.ts create mode 100644 GetSubscriptionCidrs/function.json create mode 100644 GetSubscriptionCidrs/handler.ts create mode 100644 GetSubscriptionCidrs/index.ts diff --git a/GetSubscriptionCidrs/__tests__/handler.test.ts b/GetSubscriptionCidrs/__tests__/handler.test.ts new file mode 100644 index 00000000..06fe9190 --- /dev/null +++ b/GetSubscriptionCidrs/__tests__/handler.test.ts @@ -0,0 +1,98 @@ +// eslint-disable @typescript-eslint/no-explicit-any +import * as E from "fp-ts/lib/Either"; +import * as O from "fp-ts/lib/Option"; +import * as TE from "fp-ts/lib/TaskEither"; +import { GetSubscriptionCidrsHandler } from "../handler"; +import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; +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 { SubscriptionCIDRs } from "../../generated/definitions/SubscriptionCIDRs"; + +const fakeSubscriptionId = "a-non-empty-string"; + +const mockLog = jest.fn(); +const mockedContext = { log: { error: mockLog } }; + +// eslint-disable-next-line sonar/sonar-max-lines-per-function +describe("GetSubscriptionCidrs", () => { + it("should return an internal server error response if the Subscription CIDRs model return a CosmosError", async () => { + const mockSubscriptionCIDRsModel = { + findLastVersionByModelId: jest.fn(() => + TE.left( + Promise.reject(toCosmosErrorResponse("db error") as CosmosErrors) + ) + ) + }; + + const getSubscriptionCidrsHandler = GetSubscriptionCidrsHandler( + (mockSubscriptionCIDRsModel as any) as SubscriptionCIDRsModel + ); + + const response = await getSubscriptionCidrsHandler( + mockedContext as any, + undefined as any, + undefined as any + ); + + expect(mockSubscriptionCIDRsModel.findLastVersionByModelId).toBeCalledTimes( + 1 + ); + expect(response.kind).toEqual("IResponseErrorInternal"); + }); + + it("should return a not found error response if the Subscription CIDRs model return a None", async () => { + const mockSubscriptionCIDRsModel = { + findLastVersionByModelId: jest.fn(() => TE.of(O.none)) + }; + + const getSubscriptionCidrsHandler = GetSubscriptionCidrsHandler( + (mockSubscriptionCIDRsModel as any) as SubscriptionCIDRsModel + ); + + const response = await getSubscriptionCidrsHandler( + mockedContext as any, + undefined as any, + undefined as any + ); + + expect(mockSubscriptionCIDRsModel.findLastVersionByModelId).toBeCalledTimes( + 1 + ); + expect(response.kind).toEqual("IResponseErrorNotFound"); + }); + + it("should return subscription cidrs", async () => { + const mockSubscriptionCIDRsModel = { + findLastVersionByModelId: jest.fn(() => { + return TE.right( + O.some({ subscriptionId: fakeSubscriptionId, cidrs: [] }) + ); + }) + }; + + const getSubscriptionCidrsHandler = GetSubscriptionCidrsHandler( + (mockSubscriptionCIDRsModel as any) as SubscriptionCIDRsModel + ); + + const response = await getSubscriptionCidrsHandler( + mockedContext as any, + undefined as any, + fakeSubscriptionId as NonEmptyString + ); + + expect(response).toEqual({ + apply: expect.any(Function), + kind: "IResponseSuccessJson", + value: { + id: fakeSubscriptionId, + cidrs: [] + } + }); + expect( + E.isRight(SubscriptionCIDRs.decode((response as any).value)) + ).toBeTruthy(); + }); +}); diff --git a/GetSubscriptionCidrs/function.json b/GetSubscriptionCidrs/function.json new file mode 100644 index 00000000..7d6cbd6f --- /dev/null +++ b/GetSubscriptionCidrs/function.json @@ -0,0 +1,20 @@ +{ + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "route": "adm/subscriptions/{subscriptionid}/cidrs", + "methods": [ + "get" + ] + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ], + "scriptFile": "../dist/GetSubscriptionCidrs/index.js" +} \ No newline at end of file diff --git a/GetSubscriptionCidrs/handler.ts b/GetSubscriptionCidrs/handler.ts new file mode 100644 index 00000000..c03f67ac --- /dev/null +++ b/GetSubscriptionCidrs/handler.ts @@ -0,0 +1,84 @@ +import { Context } from "@azure/functions"; +import * as express from "express"; +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 { pipe } from "fp-ts/lib/function"; + +import { SubscriptionCIDRsModel } from "@pagopa/io-functions-commons/dist/src/models/subscription_cidrs"; +import { isSome } from "fp-ts/lib/Option"; +import { SubscriptionCIDRs } from "../generated/definitions/SubscriptionCIDRs"; + +type IGetSubscriptionCidrsHandler = ( + context: Context, + auth: IAzureApiAuthorization, + subscriptionid: NonEmptyString +) => Promise< + | IResponseSuccessJson + | IResponseErrorInternal + | IResponseErrorNotFound +>; + +// eslint-disable-next-line prefer-arrow/prefer-arrow-functions +export function GetSubscriptionCidrsHandler( + subscriptionCIDRsModel: SubscriptionCIDRsModel +): IGetSubscriptionCidrsHandler { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + return async (context, _, subscriptionId) => + pipe( + subscriptionCIDRsModel.findLastVersionByModelId([subscriptionId]), + TE.map(subscriptionCIDRs => + isSome(subscriptionCIDRs) + ? ResponseSuccessJson({ + cidrs: Array.from(subscriptionCIDRs.value.cidrs), + id: subscriptionId + }) + : ResponseErrorNotFound( + "Not found", + "The required document does not exist" + ) + ), + TE.mapLeft(error => { + context.log.error(error); + return ResponseErrorInternal(`Internal server error - db error`); + }), + TE.toUnion + )(); +} + +/** + * Wraps a GetSubscriptionCidrs handler inside an Express request handler. + */ +// eslint-disable-next-line prefer-arrow/prefer-arrow-functions +export function GetSubscriptionCidrs( + subscriptionCIDRsModel: SubscriptionCIDRsModel +): express.RequestHandler { + const handler = GetSubscriptionCidrsHandler(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) + ); + + return wrapRequestHandler(middlewaresWrap(handler)); +} diff --git a/GetSubscriptionCidrs/index.ts b/GetSubscriptionCidrs/index.ts new file mode 100644 index 00000000..a35d5fe1 --- /dev/null +++ b/GetSubscriptionCidrs/index.ts @@ -0,0 +1,57 @@ +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 { + SUBSCRIPTION_CIDRS_COLLECTION_NAME, + SubscriptionCIDRsModel +} from "@pagopa/io-functions-commons/dist/src/models/subscription_cidrs"; + +import { getConfigOrThrow } from "../utils/config"; +import { cosmosdbClient } from "../utils/cosmosdb"; +import { GetSubscriptionCidrs } from "./handler"; + +const config = getConfigOrThrow(); + +const subscriptionCIDRsContainer = cosmosdbClient + .database(config.COSMOSDB_NAME) + .container(SUBSCRIPTION_CIDRS_COLLECTION_NAME); + +const subscriptionCIDRsModel = new SubscriptionCIDRsModel( + subscriptionCIDRsContainer +); + +// 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/subscriptions/:subscriptionid/cidrs", + GetSubscriptionCidrs(subscriptionCIDRsModel) +); + +const azureFunctionHandler = createAzureFunctionHandler(app); + +// Binds the express app to an Azure Function handler +// eslint-disable-next-line prefer-arrow/prefer-arrow-functions +function httpStart(context: Context): void { + logger = context.log; + setAppContext(app, context); + azureFunctionHandler(context); +} + +export default httpStart; diff --git a/openapi/index.yaml b/openapi/index.yaml index 79281d41..cfc6ec7f 100644 --- a/openapi/index.yaml +++ b/openapi/index.yaml @@ -543,6 +543,31 @@ paths: "500": description: Internal server error /subscriptions/{subscriptionId}/cidrs: + get: + summary: Get subscription cidrs + description: Get a specific subscription cidrs + operationId: getSubscriptionCidrs + parameters: + - name: subscriptionId + in: path + type: string + required: true + description: The id of the Subscription + responses: + "200": + description: Retrieved Subscription cidrs data + schema: + $ref: "#/definitions/SubscriptionCIDRs" + "400": + description: Bad request + "401": + description: Unauthorized + "403": + description: Forbidden + "404": + description: Subscription not found + "500": + description: Internal server error put: summary: Update Subscription CIDRs description: Update authorized cidrs for a Subscription. **IMPORTANT:** This API should be used only for *MANAGE Flow*. @@ -838,6 +863,21 @@ definitions: type: string required: - scope + SubscriptionCIDRs: + type: object + properties: + id: + type: string + cidrs: + description: |- + Allowed source IPs or CIDRs for this subscription. + When empty, every IP address is authorized. + type: array + items: + $ref: "#/definitions/CIDR" + required: + - id + - cidrs CIDRsPayload: type: array items: