diff --git a/packages/cactus-plugin-keychain-azure-kv/package.json b/packages/cactus-plugin-keychain-azure-kv/package.json index 23ca8968c9..df4465cbef 100644 --- a/packages/cactus-plugin-keychain-azure-kv/package.json +++ b/packages/cactus-plugin-keychain-azure-kv/package.json @@ -33,6 +33,11 @@ "name": "Peter Somogyvari", "email": "peter.somogyvari@accenture.com", "url": "https://accenture.com" + }, + { + "name": "Jeffrey Ushry II", + "email": "ushryaspirant@gmail.com", + "url": "https://www.linkedin.com/in/jeffrey-ushry-ii-aa7ab8183" } ], "main": "dist/lib/main/typescript/index.js", diff --git a/packages/cactus-plugin-keychain-azure-kv/src/main/json/openapi.json b/packages/cactus-plugin-keychain-azure-kv/src/main/json/openapi.json index ed4bea6a21..3e4837709d 100644 --- a/packages/cactus-plugin-keychain-azure-kv/src/main/json/openapi.json +++ b/packages/cactus-plugin-keychain-azure-kv/src/main/json/openapi.json @@ -1,18 +1,26 @@ { - "openapi": "3.0.3", - "info": { - "title": "Hyperledger Cactus - Keychain API", - "description": "Contains/describes the Keychain API types/paths for Hyperledger Cactus.", - "version": "0.3.0", - "license": { - "name": "Apache 2.0", - "url": "https://www.apache.org/licenses/LICENSE-2.0.html" - } - }, - "components": { - "schemas": { - } - }, + "openapi": "3.0.3", + "info": { + "title": "Hyperledger Cactus - Keychain API", + "description": "Contains/describes the Keychain API types/paths for Hyperledger Cactus.", + "version": "0.3.0", + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "components": { + "schemas": { + "GetSecretRequest": { + "type": "string", + "nullable": false + }, + "GetSecretResponse": { + "type": "string", + "nullable": false + } + } + }, "paths": { "/api/v1/plugins/@hyperledger/cactus-plugin-keychain-azure-kv/get-keychain-entry": { "post": { @@ -76,6 +84,67 @@ } } } + }, + "/api/v1/plugins/@hyperledger/cactus-plugin-keychain-azure-kv/delete-keychain-entry": { + "post": { + "x-hyperledger-cactus": { + "http": { + "path": "/api/v1/plugins/@hyperledger/cactus-plugin-keychain-azure-kv/delete-keychain-entry", + "verbLowerCase": "post" + } + }, + "operationId": "deleteKeychainEntryV1", + "summary": "Deletes a value under a key on the keychain backend.", + "parameters": [], + "requestBody": { + "$ref": "https://raw.githubusercontent.com/hyperledger/cactus/v0.9.0/packages/cactus-core-api/src/main/json/openapi.json#/components/requestBodies/keychain_delete_entry_request_body" + }, + "responses": { + "200": { + "$ref": "https://raw.githubusercontent.com/hyperledger/cactus/v0.9.0/packages/cactus-core-api/src/main/json/openapi.json#/components/responses/keychain_delete_entry_200" + }, + "400": { + "$ref": "https://raw.githubusercontent.com/hyperledger/cactus/v0.9.0/packages/cactus-core-api/src/main/json/openapi.json#/components/responses/keychain_delete_entry_400" + }, + "401": { + "$ref": "https://raw.githubusercontent.com/hyperledger/cactus/v0.9.0/packages/cactus-core-api/src/main/json/openapi.json#/components/responses/keychain_delete_entry_401" + }, + "500": { + "$ref": "https://raw.githubusercontent.com/hyperledger/cactus/v0.9.0/packages/cactus-core-api/src/main/json/openapi.json#/components/responses/keychain_delete_entry_500" + } + } + } + }, + "/api/v1/plugins/@hyperledger/cactus-plugin-keychain-azure-kv/has-keychain-entry": { + "post": { + "x-hyperledger-cactus": { + "http": { + "path": "/api/v1/plugins/@hyperledger/cactus-plugin-keychain-azure-kv/has-keychain-entry", + "verbLowerCase": "post" + } + }, + "operationId": "hasKeychainEntryV1", + "summary": "Checks that an entry exists under a key on the keychain backend", + "parameters": [], + "requestBody": { + "$ref": "https://raw.githubusercontent.com/hyperledger/cactus/v0.9.0/packages/cactus-core-api/src/main/json/openapi.json#/components/requestBodies/keychain_has_entry_request_body" + }, + "responses": { + "200": { + "$ref": "https://raw.githubusercontent.com/hyperledger/cactus/v0.9.0/packages/cactus-core-api/src/main/json/openapi.json#/components/responses/keychain_has_entry_200" + }, + "400": { + "$ref": "https://raw.githubusercontent.com/hyperledger/cactus/v0.9.0/packages/cactus-core-api/src/main/json/openapi.json#/components/responses/keychain_has_entry_400" + }, + "401": { + "$ref": "https://raw.githubusercontent.com/hyperledger/cactus/v0.9.0/packages/cactus-core-api/src/main/json/openapi.json#/components/responses/keychain_has_entry_401" + }, + "500": { + "$ref": "https://raw.githubusercontent.com/hyperledger/cactus/v0.9.0/packages/cactus-core-api/src/main/json/openapi.json#/components/responses/keychain_has_entry_500" + } + } + } } } -} \ No newline at end of file +} + diff --git a/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/generated/openapi/typescript-axios/api.ts index 1d325d0f6d..dc2dc08bc0 100644 --- a/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -21,6 +21,32 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from './base'; +/** + * + * @export + * @interface DeleteKeychainEntryRequestV1 + */ +export interface DeleteKeychainEntryRequestV1 { + /** + * The key for the entry to check the presence of on the keychain. + * @type {string} + * @memberof DeleteKeychainEntryRequestV1 + */ + key: string; +} +/** + * + * @export + * @interface DeleteKeychainEntryResponseV1 + */ +export interface DeleteKeychainEntryResponseV1 { + /** + * The key that was deleted from the keychain. + * @type {string} + * @memberof DeleteKeychainEntryResponseV1 + */ + key: string; +} /** * * @export @@ -53,6 +79,44 @@ export interface GetKeychainEntryResponse { */ value: string; } +/** + * + * @export + * @interface HasKeychainEntryRequestV1 + */ +export interface HasKeychainEntryRequestV1 { + /** + * The key to check for presence in the keychain. + * @type {string} + * @memberof HasKeychainEntryRequestV1 + */ + key: string; +} +/** + * + * @export + * @interface HasKeychainEntryResponseV1 + */ +export interface HasKeychainEntryResponseV1 { + /** + * The key that was used to check the presence of the value in the entry store. + * @type {string} + * @memberof HasKeychainEntryResponseV1 + */ + key: string; + /** + * Date and time encoded as JSON when the presence check was performed by the plugin backend. + * @type {string} + * @memberof HasKeychainEntryResponseV1 + */ + checkedAt: string; + /** + * The boolean true or false indicating the presence or absence of an entry under \'key\'. + * @type {boolean} + * @memberof HasKeychainEntryResponseV1 + */ + isPresent: boolean; +} /** * * @export @@ -92,6 +156,42 @@ export interface SetKeychainEntryResponse { */ export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * + * @summary Deletes a value under a key on the keychain backend. + * @param {DeleteKeychainEntryRequestV1} deleteKeychainEntryRequestV1 Request body to delete a keychain entry via its key + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteKeychainEntryV1: async (deleteKeychainEntryRequestV1: DeleteKeychainEntryRequestV1, options: any = {}): Promise => { + // verify required parameter 'deleteKeychainEntryRequestV1' is not null or undefined + assertParamExists('deleteKeychainEntryV1', 'deleteKeychainEntryRequestV1', deleteKeychainEntryRequestV1) + const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-keychain-azure-kv/delete-keychain-entry`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(deleteKeychainEntryRequestV1, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary Retrieves the contents of a keychain entry from the backend. @@ -128,6 +228,42 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati options: localVarRequestOptions, }; }, + /** + * + * @summary Checks that an entry exists under a key on the keychain backend + * @param {HasKeychainEntryRequestV1} hasKeychainEntryRequestV1 Request body for checking a keychain entry via its key + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + hasKeychainEntryV1: async (hasKeychainEntryRequestV1: HasKeychainEntryRequestV1, options: any = {}): Promise => { + // verify required parameter 'hasKeychainEntryRequestV1' is not null or undefined + assertParamExists('hasKeychainEntryV1', 'hasKeychainEntryRequestV1', hasKeychainEntryRequestV1) + const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-keychain-azure-kv/has-keychain-entry`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(hasKeychainEntryRequestV1, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary Sets a value under a key on the keychain backend. @@ -174,6 +310,17 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati export const DefaultApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration) return { + /** + * + * @summary Deletes a value under a key on the keychain backend. + * @param {DeleteKeychainEntryRequestV1} deleteKeychainEntryRequestV1 Request body to delete a keychain entry via its key + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteKeychainEntryV1(deleteKeychainEntryRequestV1: DeleteKeychainEntryRequestV1, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteKeychainEntryV1(deleteKeychainEntryRequestV1, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @summary Retrieves the contents of a keychain entry from the backend. @@ -185,6 +332,17 @@ export const DefaultApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getKeychainEntryV1(getKeychainEntryRequest, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @summary Checks that an entry exists under a key on the keychain backend + * @param {HasKeychainEntryRequestV1} hasKeychainEntryRequestV1 Request body for checking a keychain entry via its key + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async hasKeychainEntryV1(hasKeychainEntryRequestV1: HasKeychainEntryRequestV1, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.hasKeychainEntryV1(hasKeychainEntryRequestV1, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @summary Sets a value under a key on the keychain backend. @@ -206,6 +364,16 @@ export const DefaultApiFp = function(configuration?: Configuration) { export const DefaultApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = DefaultApiFp(configuration) return { + /** + * + * @summary Deletes a value under a key on the keychain backend. + * @param {DeleteKeychainEntryRequestV1} deleteKeychainEntryRequestV1 Request body to delete a keychain entry via its key + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteKeychainEntryV1(deleteKeychainEntryRequestV1: DeleteKeychainEntryRequestV1, options?: any): AxiosPromise { + return localVarFp.deleteKeychainEntryV1(deleteKeychainEntryRequestV1, options).then((request) => request(axios, basePath)); + }, /** * * @summary Retrieves the contents of a keychain entry from the backend. @@ -216,6 +384,16 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa getKeychainEntryV1(getKeychainEntryRequest: GetKeychainEntryRequest, options?: any): AxiosPromise { return localVarFp.getKeychainEntryV1(getKeychainEntryRequest, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary Checks that an entry exists under a key on the keychain backend + * @param {HasKeychainEntryRequestV1} hasKeychainEntryRequestV1 Request body for checking a keychain entry via its key + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + hasKeychainEntryV1(hasKeychainEntryRequestV1: HasKeychainEntryRequestV1, options?: any): AxiosPromise { + return localVarFp.hasKeychainEntryV1(hasKeychainEntryRequestV1, options).then((request) => request(axios, basePath)); + }, /** * * @summary Sets a value under a key on the keychain backend. @@ -236,6 +414,18 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa * @extends {BaseAPI} */ export class DefaultApi extends BaseAPI { + /** + * + * @summary Deletes a value under a key on the keychain backend. + * @param {DeleteKeychainEntryRequestV1} deleteKeychainEntryRequestV1 Request body to delete a keychain entry via its key + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public deleteKeychainEntryV1(deleteKeychainEntryRequestV1: DeleteKeychainEntryRequestV1, options?: any) { + return DefaultApiFp(this.configuration).deleteKeychainEntryV1(deleteKeychainEntryRequestV1, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary Retrieves the contents of a keychain entry from the backend. @@ -248,6 +438,18 @@ export class DefaultApi extends BaseAPI { return DefaultApiFp(this.configuration).getKeychainEntryV1(getKeychainEntryRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary Checks that an entry exists under a key on the keychain backend + * @param {HasKeychainEntryRequestV1} hasKeychainEntryRequestV1 Request body for checking a keychain entry via its key + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public hasKeychainEntryV1(hasKeychainEntryRequestV1: HasKeychainEntryRequestV1, options?: any) { + return DefaultApiFp(this.configuration).hasKeychainEntryV1(hasKeychainEntryRequestV1, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary Sets a value under a key on the keychain backend. diff --git a/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/plugin-factory-keychain.ts b/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/plugin-factory-keychain.ts index b9edb78dcc..4173789c42 100644 --- a/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/plugin-factory-keychain.ts +++ b/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/plugin-factory-keychain.ts @@ -1,27 +1,29 @@ -import { v4 as uuidv4 } from "uuid"; +import { Checks } from "@hyperledger/cactus-common"; import { IPluginFactoryOptions, + IPluginKeychain, PluginFactory, + PluginImportType, } from "@hyperledger/cactus-core-api"; - import { IPluginKeychainAzureKvOptions, PluginKeychainAzureKv, } from "./plugin-keychain-azure-kv"; export class PluginFactoryKeychain extends PluginFactory< - PluginKeychainAzureKv, + IPluginKeychain, IPluginKeychainAzureKvOptions, IPluginFactoryOptions > { - async create( - pluginOptions: IPluginKeychainAzureKvOptions = { - instanceId: uuidv4(), - keychainId: uuidv4(), - azureEndpoint: "", - logLevel: "TRACE", - }, - ): Promise { - return new PluginKeychainAzureKv(pluginOptions); + async create(options: any): Promise { + const fnTag = "PluginFactoryKeychain#create()"; + + const { pluginImportType } = this.options; + Checks.truthy(options, `${fnTag}:options`); + if (pluginImportType === PluginImportType.Local) { + return new PluginKeychainAzureKv(options); + } else { + throw new Error(`${fnTag} No PluginImportType: ${pluginImportType}`); + } } } diff --git a/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/plugin-keychain-azure-kv.ts b/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/plugin-keychain-azure-kv.ts index 772d250b3f..34ddd67df2 100644 --- a/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/plugin-keychain-azure-kv.ts +++ b/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/plugin-keychain-azure-kv.ts @@ -16,16 +16,18 @@ import { IWebServiceEndpoint, } from "@hyperledger/cactus-core-api"; +import { SetKeychainEntryEndpoint } from "./web-services/set-keychain-entry-endpoint"; +import { GetKeychainEntryEndpoint } from "./web-services/get-keychain-entry-endpoint"; +import { DeleteKeychainEntryEndpoint } from "./web-services/delete-keychain-entry-endpoint"; + import { KeyVaultSecret, SecretClient } from "@azure/keyvault-secrets"; import { UsernamePasswordCredential, DefaultAzureCredential, } from "@azure/identity"; +import { HasKeychainEntryEndpoint } from "./web-services/has-keychain-entry-endpoint"; // TODO: Writing the getExpressRequestHandler() method for -// GetKeychainEntryEndpointV1 and SetKeychainEntryEndpointV1 -// import { GetKeychainEntryEndpointV1 } from "./web-services/get-keychain-entry-endpoint-v1"; -// import { SetKeychainEntryEndpointV1 } from "./web-services/set-keychain-entry-endpoint-v1"; export enum AzureCredentialType { LocalFile = "LOCAL_FILE", @@ -136,7 +138,24 @@ export class PluginKeychainAzureKv if (Array.isArray(this.endpoints)) { return this.endpoints; } - const endpoints: IWebServiceEndpoint[] = []; + const endpoints: IWebServiceEndpoint[] = [ + new SetKeychainEntryEndpoint({ + connector: this, + logLevel: this.opts.logLevel, + }), + new GetKeychainEntryEndpoint({ + connector: this, + logLevel: this.opts.logLevel, + }), + new DeleteKeychainEntryEndpoint({ + connector: this, + logLevel: this.opts.logLevel, + }), + new HasKeychainEntryEndpoint({ + connector: this, + logLevel: this.opts.logLevel, + }), + ]; // TODO: Writing the getExpressRequestHandler() method for // GetKeychainEntryEndpointV1 and SetKeychainEntryEndpointV1 diff --git a/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/web-services/delete-keychain-entry-endpoint.ts b/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/web-services/delete-keychain-entry-endpoint.ts new file mode 100644 index 0000000000..9b12f93b74 --- /dev/null +++ b/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/web-services/delete-keychain-entry-endpoint.ts @@ -0,0 +1,102 @@ +import { Express, Request, Response } from "express"; + +import { + Logger, + Checks, + LogLevelDesc, + LoggerProvider, + IAsyncProvider, +} from "@hyperledger/cactus-common"; +import { + IEndpointAuthzOptions, + IExpressRequestHandler, + IWebServiceEndpoint, +} from "@hyperledger/cactus-core-api"; +import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; + +import { PluginKeychainAzureKv } from "../plugin-keychain-azure-kv"; + +import OAS from "../../json/openapi.json"; +import { DeleteKeychainEntryRequestV1 } from "../generated/openapi/typescript-axios"; + +export interface IDeleteKeychainEntryEndpointOptions { + logLevel?: LogLevelDesc; + connector: PluginKeychainAzureKv; +} + +export class DeleteKeychainEntryEndpoint implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "DeleteKeychainEntryEndpoint"; + + private readonly log: Logger; + + public get className(): string { + return DeleteKeychainEntryEndpoint.CLASS_NAME; + } + + constructor(public readonly options: IDeleteKeychainEntryEndpointOptions) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + Checks.truthy(options.connector, `${fnTag} arg options.connector`); + + const level = this.options.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + public getOasPath() { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-keychain-azure-kv/delete-keychain-entry" + ]; + } + + public getPath(): string { + const apiPath = this.getOasPath(); + return apiPath.post["x-hyperledger-cactus"].http.path; + } + + public getVerbLowerCase(): string { + const apiPath = this.getOasPath(); + return apiPath.post["x-hyperledger-cactus"].http.verbLowerCase; + } + + public getOperationId(): string { + return this.getOasPath().post.operationId; + } + + getAuthorizationOptionsProvider(): IAsyncProvider { + // TODO: make this an injectable dependency in the constructor + return { + get: async () => ({ + isProtected: true, + requiredRoles: [], + }), + }; + } + + public async registerExpress( + expressApp: Express, + ): Promise { + await registerWebServiceEndpoint(expressApp, this); + return this; + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public async handleRequest(req: Request, res: Response): Promise { + const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; + this.log.debug(reqTag); + try { + const { key } = req.body as DeleteKeychainEntryRequestV1; + const resBody = await this.options.connector.delete(key); + res.json(resBody); + } catch (ex) { + this.log.error(`Crash while serving ${reqTag}`, ex); + res.status(500).json({ + message: "Internal Server Error", + error: ex?.stack || ex?.message, + }); + } + } +} diff --git a/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/web-services/get-keychain-entry-endpoint.ts b/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/web-services/get-keychain-entry-endpoint.ts new file mode 100644 index 0000000000..e20e4ea417 --- /dev/null +++ b/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/web-services/get-keychain-entry-endpoint.ts @@ -0,0 +1,112 @@ +import { Express, Request, Response } from "express"; + +import { + Logger, + Checks, + LogLevelDesc, + LoggerProvider, + IAsyncProvider, +} from "@hyperledger/cactus-common"; +import { + IEndpointAuthzOptions, + IExpressRequestHandler, + IWebServiceEndpoint, +} from "@hyperledger/cactus-core-api"; +import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; + +import { PluginKeychainAzureKv } from "../plugin-keychain-azure-kv"; + +import OAS from "../../json/openapi.json"; +import { GetKeychainEntryRequest } from "../generated/openapi/typescript-axios"; + +export interface IGetKeychainEntryEndpointOptions { + logLevel?: LogLevelDesc; + connector: PluginKeychainAzureKv; +} + +export class GetKeychainEntryEndpoint implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "GetKeychainEntryEndpoint"; + + private readonly log: Logger; + + public get className(): string { + return GetKeychainEntryEndpoint.CLASS_NAME; + } + + constructor(public readonly options: IGetKeychainEntryEndpointOptions) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + Checks.truthy(options.connector, `${fnTag} arg options.connector`); + + const level = this.options.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + public getOasPath() { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-keychain-azure-kv/get-keychain-entry" + ]; + } + + public getPath(): string { + const apiPath = this.getOasPath(); + return apiPath.post["x-hyperledger-cactus"].http.path; + } + + public getVerbLowerCase(): string { + const apiPath = this.getOasPath(); + return apiPath.post["x-hyperledger-cactus"].http.verbLowerCase; + } + + public getOperationId(): string { + return this.getOasPath().post.operationId; + } + + getAuthorizationOptionsProvider(): IAsyncProvider { + // TODO: make this an injectable dependency in the constructor + return { + get: async () => ({ + isProtected: true, + requiredRoles: [], + }), + }; + } + + public async registerExpress( + expressApp: Express, + ): Promise { + await registerWebServiceEndpoint(expressApp, this); + return this; + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public async handleRequest(req: Request, res: Response): Promise { + const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; + this.log.debug(reqTag); + const { key } = req.body as GetKeychainEntryRequest; + try { + const value = await this.options.connector.get(key); + res.json({ + key, + value, + }); + } catch (ex) { + if (ex?.message?.includes(`${key} secret not found`)) { + res.status(404).json({ + key, + error: ex?.stack || ex?.message, + }); + } else { + this.log.error(`Crash while serving ${reqTag}`, ex); + res.status(500).json({ + message: "Internal Server Error", + error: ex?.stack || ex?.message, + }); + } + } + } +} diff --git a/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/web-services/has-keychain-entry-endpoint.ts b/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/web-services/has-keychain-entry-endpoint.ts new file mode 100644 index 0000000000..c5439004d0 --- /dev/null +++ b/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/web-services/has-keychain-entry-endpoint.ts @@ -0,0 +1,110 @@ +import { Express, Response, Request } from "express"; + +import { + Logger, + Checks, + LogLevelDesc, + LoggerProvider, + IAsyncProvider, +} from "@hyperledger/cactus-common"; +import { + HasKeychainEntryRequestV1, + HasKeychainEntryResponseV1, + IEndpointAuthzOptions, + IExpressRequestHandler, + IWebServiceEndpoint, +} from "@hyperledger/cactus-core-api"; +import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; + +import { PluginKeychainAzureKv } from "../plugin-keychain-azure-kv"; + +import OAS from "../../json/openapi.json"; + +export interface IHasKeychainEntryEndpointOptions { + logLevel?: LogLevelDesc; + connector: PluginKeychainAzureKv; +} + +export class HasKeychainEntryEndpoint implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "HasKeychainEntryEndpoint"; + + private readonly log: Logger; + + public get className(): string { + return HasKeychainEntryEndpoint.CLASS_NAME; + } + + constructor(public readonly options: IHasKeychainEntryEndpointOptions) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + Checks.truthy(options.connector, `${fnTag} arg options.connector`); + + const level = this.options.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + public getOasPath() { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-keychain-azure-kv/has-keychain-entry" + ]; + } + + public getPath(): string { + const apiPath = this.getOasPath(); + return apiPath.post["x-hyperledger-cactus"].http.path; + } + + public getVerbLowerCase(): string { + const apiPath = this.getOasPath(); + return apiPath.post["x-hyperledger-cactus"].http.verbLowerCase; + } + + public getOperationId(): string { + return this.getOasPath().post.operationId; + } + + getAuthorizationOptionsProvider(): IAsyncProvider { + // TODO: make this an injectable dependency in the constructor + return { + get: async () => ({ + isProtected: true, + requiredRoles: [], + }), + }; + } + + public async registerExpress( + expressApp: Express, + ): Promise { + await registerWebServiceEndpoint(expressApp, this); + return this; + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public async handleRequest(req: Request, res: Response): Promise { + const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; + this.log.debug(reqTag); + try { + const reqBody = req.body as HasKeychainEntryRequestV1; + const { key } = reqBody; + const checkedAt = new Date().toJSON(); + const isPresent = await this.options.connector.has(key); + const resBody: HasKeychainEntryResponseV1 = { + isPresent, + key, + checkedAt, + }; + res.json(resBody); + } catch (ex) { + this.log.error(`Crash while serving ${reqTag}`, ex); + res.status(500).json({ + message: "Internal Server Error", + error: ex?.stack || ex?.message, + }); + } + } +} diff --git a/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/web-services/set-keychain-entry-endpoint.ts b/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/web-services/set-keychain-entry-endpoint.ts new file mode 100644 index 0000000000..c8ff9c6743 --- /dev/null +++ b/packages/cactus-plugin-keychain-azure-kv/src/main/typescript/web-services/set-keychain-entry-endpoint.ts @@ -0,0 +1,102 @@ +import { Express, Request, Response } from "express"; + +import { + Logger, + Checks, + LogLevelDesc, + LoggerProvider, + IAsyncProvider, +} from "@hyperledger/cactus-common"; +import { + IEndpointAuthzOptions, + IExpressRequestHandler, + IWebServiceEndpoint, +} from "@hyperledger/cactus-core-api"; +import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; + +import { PluginKeychainAzureKv } from "../plugin-keychain-azure-kv"; + +import OAS from "../../json/openapi.json"; +import { SetKeychainEntryRequest } from "../generated/openapi/typescript-axios"; + +export interface ISetKeychainEntryEndpointOptions { + logLevel?: LogLevelDesc; + connector: PluginKeychainAzureKv; +} + +export class SetKeychainEntryEndpoint implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "SetKeychainEntryEndpoint"; + + private readonly log: Logger; + + public get className(): string { + return SetKeychainEntryEndpoint.CLASS_NAME; + } + + constructor(public readonly options: ISetKeychainEntryEndpointOptions) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + Checks.truthy(options.connector, `${fnTag} arg options.connector`); + + const level = this.options.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + public getOasPath() { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-keychain-azure-kv/set-keychain-entry" + ]; + } + + public getPath(): string { + const apiPath = this.getOasPath(); + return apiPath.post["x-hyperledger-cactus"].http.path; + } + + public getVerbLowerCase(): string { + const apiPath = this.getOasPath(); + return apiPath.post["x-hyperledger-cactus"].http.verbLowerCase; + } + + public getOperationId(): string { + return this.getOasPath().post.operationId; + } + + getAuthorizationOptionsProvider(): IAsyncProvider { + // TODO: make this an injectable dependency in the constructor + return { + get: async () => ({ + isProtected: true, + requiredRoles: [], + }), + }; + } + + public async registerExpress( + expressApp: Express, + ): Promise { + await registerWebServiceEndpoint(expressApp, this); + return this; + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public async handleRequest(req: Request, res: Response): Promise { + const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; + this.log.debug(reqTag); + try { + const { key, value } = req.body as SetKeychainEntryRequest; + const resBody = await this.options.connector.set(key, value); + res.json(resBody); + } catch (ex) { + this.log.error(`Crash while serving ${reqTag}`, ex); + res.status(500).json({ + message: "Internal Server Error", + error: ex?.stack || ex?.message, + }); + } + } +} diff --git a/packages/cactus-plugin-keychain-azure-kv/src/test/typescript/integration/plugin-keychain-azure-kv.test.ts b/packages/cactus-plugin-keychain-azure-kv/src/test/typescript/integration/plugin-keychain-azure-kv.test.ts index 3432888f53..00af8f8cdc 100644 --- a/packages/cactus-plugin-keychain-azure-kv/src/test/typescript/integration/plugin-keychain-azure-kv.test.ts +++ b/packages/cactus-plugin-keychain-azure-kv/src/test/typescript/integration/plugin-keychain-azure-kv.test.ts @@ -1,10 +1,18 @@ +import http from "http"; +import type { AddressInfo } from "net"; + import test, { Test } from "tape-promise/tape"; +import express from "express"; +import bodyParser from "body-parser"; import { v4 as uuidv4 } from "uuid"; -import { LogLevelDesc } from "@hyperledger/cactus-common"; +import { IListenOptions } from "@hyperledger/cactus-common"; +import { LogLevelDesc, Servers } from "@hyperledger/cactus-common"; import { + Configuration, + DefaultApi as KeychainAzureKvApi, IPluginKeychainAzureKvOptions, PluginKeychainAzureKv, } from "../../../main/typescript/public-api"; @@ -29,31 +37,109 @@ test("get,set,has,delete alters state as expected for AzureCredentialType.InMemo t.equal(plugin.getKeychainId(), options.keychainId, "Keychain ID set OK"); t.equal(plugin.getInstanceId(), options.instanceId, "Instance ID set OK"); - const key = uuidv4(); - const value = uuidv4(); - - const hasPrior = await plugin.has(key); - - t.false(hasPrior, "hasPrior === false OK"); - - await plugin.set(key, value); - - const hasAfter = await plugin.has(key); - t.true(hasAfter, "hasAfter === true OK"); + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + const server = http.createServer(expressApp); + const listenOptions: IListenOptions = { + hostname: "0.0.0.0", + port: 0, + server, + }; + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + test.onFinish(async () => await Servers.shutdown(server)); + const { address, port } = addressInfo; + const apiHost = `http://${address}:${port}`; - const valueAfter = await plugin.get(key); - t.ok(valueAfter, "valueAfter truthy OK"); - t.equal(valueAfter, value, "valueAfter === value OK"); + const configuration = new Configuration({ basePath: apiHost }); + const apiClient = new KeychainAzureKvApi(configuration); - await plugin.delete(key); + await plugin.registerWebServices(expressApp); - const hasAfterDelete = await plugin.has(key); - t.false(hasAfterDelete, "hasAfterDelete === false OK"); + const key = uuidv4(); + const value = uuidv4(); - const valueAfterDelete = plugin.get(key); - const regExp = new RegExp(/secret not found*/); - const rejectMsg = "valueAfterDelete === throws OK"; - await t.rejects(valueAfterDelete, regExp, rejectMsg); + const res1 = await apiClient.hasKeychainEntryV1({ key }); + t.true(res1.status >= 200, "res1.status >= 200 OK"); + t.true(res1.status < 300, "res1.status < 300 OK"); + + // FIXME: make it so that the hasKeychainEntryV1 endpoint returns the + // response object as defined by the openapi.json in core-api + // (remember that we have a pending pull request for applying those changes + // in the main line so there's a dependency between pull requests here at play) + t.ok(res1.data, "res1.data truthy OK"); + t.false(res1.data.isPresent, "res1.data.isPresent === false OK"); + t.ok(res1.data.checkedAt, "res1.data.checkedAt truthy OK"); + t.equal(res1.data.key, key, "res1.data.key === key OK"); + + const res2 = await apiClient.setKeychainEntryV1({ key, value }); + t.true(res2.status >= 200, "res2.status >= 200 OK"); + t.true(res2.status < 300, "res2.status < 300 OK"); + t.notOk(res2.data, "res2.data truthy OK"); + + // const hasAfter = await plugin.has(key); + // t.true(hasAfter, "hasAfter === true OK"); + + const res3 = await apiClient.hasKeychainEntryV1({ key }); + t.true(res3.status >= 200, "res3.status >= 200 OK"); + t.true(res3.status < 300, "res3.status < 300 OK"); + + // FIXME: make it so that the hasKeychainEntryV1 endpoint returns the + // response object as defined by the openapi.json in core-api + // (remember that we have a pending pull request for applying those changes + // in the main line so there's a dependency between pull requests here at play) + t.ok(res3.data, "res3.data truthy OK"); + t.true(res3.data.isPresent, "res3.data.isPresent === true OK"); + t.ok(res3.data.checkedAt, "res3.data.checkedAt truthy OK"); + t.equal(res3.data.key, key, "res3.data.key === key OK"); + + const res4 = await apiClient.getKeychainEntryV1({ key }); + t.true(res4.status >= 200, "res4.status >= 200 OK"); + t.true(res4.status < 300, "res4.status < 300 OK"); + t.ok(res4.data, "res4.data truthy OK"); + t.equal(res4.data.value, value, "res4.data.value === value OK"); + + // await plugin.delete(key); + + const res5 = await apiClient.deleteKeychainEntryV1({ key }); + t.true(res5.status >= 200, "res5.status >= 200 OK"); + t.true(res5.status < 300, "res5.status < 300 OK"); + t.notOk(res5.data, "res5.data falsy OK"); + + const res6 = await apiClient.hasKeychainEntryV1({ key }); + t.true(res6.status >= 200, "res6.status >= 200 OK"); + t.true(res6.status < 300, "res6.status < 300 OK"); + + // FIXME: make it so that the hasKeychainEntryV1 endpoint returns the + // response object as defined by the openapi.json in core-api + // (remember that we have a pending pull request for applying those changes + // in the main line so there's a dependency between pull requests here at play) + t.ok(res6.data, "res6.data truthy OK"); + t.false(res6.data.isPresent, "res6.data.isPresent === false OK"); + t.ok(res6.data.checkedAt, "res6.data.checkedAt truthy OK"); + t.equal(res6.data.key, key, "res6.data.key === key OK"); + + // const valueAfterDelete = plugin.get(key); + // const regExp = new RegExp(/secret not found*/); + // const rejectMsg = "valueAfterDelete === throws OK"; + // await t.rejects(valueAfterDelete, regExp, rejectMsg); + + try { + await apiClient.getKeychainEntryV1({ key }); + t.fail( + "Failing because getKeychainEntryV1 did not throw when called with non-existent key.", + ); + } catch (ex) { + t.ok(ex, "res7 -> ex truthy"); + const res7 = ex.response; + t.equal(res7.status, 404, "res7.status === 404 OK"); + t.ok(res7.data, "res7.data truthy OK"); + t.ok(res7.data.error, "res7.data.error truthy OK"); + t.equal(typeof res7.data.error, "string", "res7.data.error truthy OK"); + t.true( + res7.data.error.includes(`${key} secret not found`), + "res7.data.error contains legible error message about missing key OK", + ); + } t.end(); });