diff --git a/package-lock.json b/package-lock.json index b8a781e..edcd626 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "akamai-edgeworkers-cli", - "version": "1.7.3", + "version": "1.7.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "akamai-edgeworkers-cli", - "version": "1.7.3", + "version": "1.7.4", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index eaf656e..8951aa5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "akamai-edgeworkers-cli", - "version": "1.7.3", + "version": "1.7.4", "description": "A tool that makes it easier to manage Akamai EdgeWorkers code bundles and EdgeKV databases. Call the EdgeWorkers and EdgeKV API from the command line.", "repository": "https://github.com/akamai/cli-edgeworkers", "scripts": { diff --git a/src/edgekv/ekv-cli-main.ts b/src/edgekv/ekv-cli-main.ts index 32a0ae9..ba04258 100644 --- a/src/edgekv/ekv-cli-main.ts +++ b/src/edgekv/ekv-cli-main.ts @@ -2,17 +2,17 @@ import * as envUtils from '../utils/env-utils'; import * as cliUtils from '../utils/cli-utils'; import * as configUtils from '../utils/config-utils'; -import { - ORDER_BY, - MAX_ITEMS, - GROUP_ID, - RETENTION, - GEO_LOCATION, - STAGING, - PRODUCTION, - EW_IDS, - EXPIRY, - NAMESPACE, +import { + ORDER_BY, + MAX_ITEMS, + GROUP_ID, + RETENTION, + GEO_LOCATION, + STAGING, + PRODUCTION, + EW_IDS, + EXPIRY, + NAMESPACE, SAVE_PATH } from './../utils/constants'; import { SANDBOX_ID } from '../utils/constants'; import * as kvCliHandler from './ekv-handler'; @@ -20,6 +20,7 @@ import { ekvJsonOutput } from './client-manager'; import * as httpEdge from '../cli-httpRequest'; import * as pkginfo from '../../package.json'; import { Command } from 'commander'; +import {validateNamespaceDataAccessPolicy} from './ekv-helper'; const program = new Command(); const currentYear = new Date().getFullYear(); @@ -118,10 +119,14 @@ program program .command('initialize') .description('Initialize EdgeKV for the first time') + .option( + '--dataAccessPolicy ', + '`dataAccessPolicy` option must be of the form `restrictDataAccess=,allowNamespacePolicyOverride=` where can be true or false.' + ) .alias('init') - .action(async function () { + .action(async function (options) { try { - await kvCliHandler.initializeEdgeKv(); + await kvCliHandler.initializeEdgeKv(options['dataAccessPolicy']); } catch (e) { cliUtils.logAndExit(1, e); } @@ -413,11 +418,16 @@ create '--geoLocation ', 'Specifies the persistent storage location for data when creating a namespace on the production network. This can help optimize performance by storing data where most or all of your users are located. The value defaults to `US` on the `STAGING` and `PRODUCTION` networks.' ) + .option( + '--dataAccessPolicy ', + '`dataAccessPolicy` option must be of the form `restrictDataAccess=` where can be true or false.' + ) .description('Creates an EdgeKV namespace') .action(async function (environment, namespace, options) { options['retention'] = options.retention || configUtils.searchProperty(RETENTION); options['groupId'] = options.groupId || configUtils.searchProperty(GROUP_ID); options['geolocation'] = options.geolocation || configUtils.searchProperty(GEO_LOCATION); + options['dataAccessPolicy'] = options.dataAccessPolicy ? validateNamespaceDataAccessPolicy(options.dataAccessPolicy) : undefined; try { await kvCliHandler.createNamespace( @@ -425,7 +435,8 @@ create namespace, options.retention, options.groupId, - options.geoLocation + options.geoLocation, + options.dataAccessPolicy ); } catch (e) { cliUtils.logAndExit(1, e); @@ -559,6 +570,24 @@ modify cliUtils.logAndExit(0, copywrite); }); +modify + .command('db') + .requiredOption( + '--dataAccessPolicy ', + '`dataAccessPolicy` option must be of the form `restrictDataAccess=,allowNamespacePolicyOverride=` where can be true or false.' + ) + .description('Modify the database data access policy') + .action(async function (options) { + try { + await kvCliHandler.updateDatabase(options['dataAccessPolicy']); + } catch (e) { + cliUtils.logAndExit(1, e); + } + }) + .on('--help', function () { + cliUtils.logAndExit(0, copywrite); + }); + const download = program .command('download') .alias('dnld') diff --git a/src/edgekv/ekv-handler.ts b/src/edgekv/ekv-handler.ts index 1137705..3aa787b 100644 --- a/src/edgekv/ekv-handler.ts +++ b/src/edgekv/ekv-handler.ts @@ -4,6 +4,7 @@ import * as response from './ekv-response'; import * as ekvhelper from './ekv-helper'; import * as edgeWorkersSvc from '../edgeworkers/ew-service'; import { ekvJsonOutput } from './client-manager'; +import {validateDataAccessPolicy} from './ekv-helper'; export async function listNameSpaces( environment: string, @@ -33,6 +34,7 @@ export async function listNameSpaces( RetentionPeriod: retentionPeriod, GeoLocation: value['geoLocation'], AccessGroupId: groupId, + 'Namespace dataAccessPolicy': value['dataAccessPolicy'] ? 'restrictDataAccess=' + value['dataAccessPolicy']['restrictDataAccess'] + ', policyType=' + value['dataAccessPolicy']['policyType'] : 'N/A', }); } else { nsListResp.push({ NamespaceId: value['namespace'] }); @@ -84,7 +86,8 @@ export async function createNamespace( nameSpace: string, retention: number, groupId: number, - geoLocation: string + geoLocation: string, + dataAccessPolicy: object = undefined ) { if (!groupId) { cliUtils.logAndExit( @@ -102,7 +105,8 @@ export async function createNamespace( nameSpace, retentionPeriod, groupId, - geoLocation + geoLocation, + dataAccessPolicy ), `Creating namespace for environment ${environment}` ); @@ -196,9 +200,13 @@ export async function updateNameSpace( } } -export async function initializeEdgeKv() { +export async function initializeEdgeKv(dataAccessPolicyStr: string) { + let dataAccessPolicy; + if (dataAccessPolicyStr) { + dataAccessPolicy = validateDataAccessPolicy(dataAccessPolicyStr); + } const initializedEdgeKv = await cliUtils.spinner( - edgekvSvc.initializeEdgeKV(), + edgekvSvc.initializeEdgeKV(dataAccessPolicy), 'Initializing EdgeKV...' ); @@ -289,6 +297,41 @@ export async function getInitializationStatus() { } } +export async function updateDatabase(dataAccessPolicyStr: string) { + const dataAccessPolicy = validateDataAccessPolicy(dataAccessPolicyStr); + const updateDataAccessPolicy = await cliUtils.spinner( + edgekvSvc.updateDatabase(dataAccessPolicy), + 'Updating database data access policy...' + ); + + if (updateDataAccessPolicy.data != undefined && !updateDataAccessPolicy.isError) { + const updateRespBody = updateDataAccessPolicy.data; + + const status = updateDataAccessPolicy.status; + let msg; + if (Object.prototype.hasOwnProperty.call(updateRespBody, 'accountStatus')) { + const accountStatus = updateRespBody['accountStatus']; + if (status == 200 && accountStatus == 'INITIALIZED') { + msg = 'EdgeKV database data access policy successfully modified'; + } else { + msg = 'EdgeKV database data access policy was not modified'; + } + } + if (ekvJsonOutput.isJSONOutputMode()) { + ekvJsonOutput.writeJSONOutput(0, msg, updateRespBody); + } else { + cliUtils.logWithBorder(msg); + response.logInitialize(updateRespBody); + } + } else { + const errorReason = `${updateDataAccessPolicy.error_reason}`; + response.logError( + updateDataAccessPolicy, + `ERROR: EdgeKV database update failed (${errorReason}) [TraceId: ${updateDataAccessPolicy.traceId}]` + ); + } +} + export async function writeItemToEdgeKV( environment: string, nameSpace: string, @@ -652,7 +695,7 @@ export async function listAuthGroups(options: { ewGroups = await getEwGroups(options.groupIds); } const msg = `User has the following permission access for group: ${groupId}`; - + if (ekvJsonOutput.isJSONOutputMode()) { const obj = { authGroups, diff --git a/src/edgekv/ekv-helper.ts b/src/edgekv/ekv-helper.ts index 19bb6cc..6e2b709 100644 --- a/src/edgekv/ekv-helper.ts +++ b/src/edgekv/ekv-helper.ts @@ -14,7 +14,7 @@ const tkn_export = '\n}\nexport { edgekv_access_tokens };'; /** * converts seconds to years, months and days - * @param seconds + * @param seconds */ export function convertRetentionPeriod(seconds) { if (seconds == 0) { @@ -51,13 +51,70 @@ export function validateNetwork(network: string,sandboxId?: string) { if (network.toUpperCase() !== cliUtils.staging && network.toUpperCase() !== cliUtils.production) { cliUtils.logAndExit(1, `ERROR: Environment parameter must be either staging or production - was: ${network}`); } - } + } +} + +/** + * Formats a database data access policy option string + * and returns as an object for use in requests + * @param dataAccessPolicyStr + */ +export function formatDataAccessPolicy(dataAccessPolicyStr: string) { + const dataAccessPolicy = {}; + if (dataAccessPolicyStr) { + const dataAccessPolicyArr = dataAccessPolicyStr.split(','); + dataAccessPolicyArr.filter(item => item.includes('=')).forEach(item => { + const property = item.split('='); + if (property.length !== 2) { + console.error(`Warning: cannot parse invalid item ['${item}'], skip it.`); + } else { + const boolProp = property[1].trim(); + // Avoid setting improper string value to false + if (boolProp === 'true' || boolProp === 'false') { + dataAccessPolicy[property[0].trim()] = boolProp === 'true'; + } + } + }); + } + return dataAccessPolicy; +} + +/** + * Validates a database data access policy option string + * and returns as a formatted object for use in requests + * @param dataAccessPolicyStr + */ +export function validateDataAccessPolicy(dataAccessPolicyStr: string) { + const dataAccessPolicy = formatDataAccessPolicy(dataAccessPolicyStr); + if (dataAccessPolicy['restrictDataAccess'] == undefined || dataAccessPolicy['allowNamespacePolicyOverride'] == undefined) { + cliUtils.logAndExit( + 1, + 'ERROR: `dataAccessPolicy` option must be of the form `restrictDataAccess=,allowNamespacePolicyOverride=` where can be true or false.' + ); + } + return dataAccessPolicy; +} + +/** + * Validates a database data access policy option string + * and returns as a formatted object for use in requests + * @param dataAccessPolicyStr + */ +export function validateNamespaceDataAccessPolicy(dataAccessPolicyStr: string) { + const dataAccessPolicy = formatDataAccessPolicy(dataAccessPolicyStr); + if (dataAccessPolicy['restrictDataAccess'] == undefined) { + cliUtils.logAndExit( + 1, + 'ERROR: `dataAccessPolicy` option must be of the form `restrictDataAccess=` where can be true or false.' + ); + } + return dataAccessPolicy; } /** * Validates if json file exists in the specified location * and validates json content of the file - * @param items + * @param items */ export function validateInputFile(itemFilePath) { @@ -75,7 +132,7 @@ export function validateInputFile(itemFilePath) { /** * Validates the json file content - * @param itemFilePath + * @param itemFilePath */ function validateJson(itemFilePath) { try { @@ -89,7 +146,7 @@ function validateJson(itemFilePath) { } } -// converts jwt token to date format +// converts jwt token to date format export function convertTokenDate(seconds) { const convertedDate = new Date(seconds * 1000); return convertedDate; @@ -142,11 +199,11 @@ export function isValidDate(dateString) { * if no token file exists, new file is created.Else the existing file is updated * If overwrite option is specified token content will be overwritte else token will be displayed * for users to copy to their file - * @param savePath - * @param overWrite - * @param createdToken - * @param decodedToken - * @param nameSpaceList + * @param savePath + * @param overWrite + * @param createdToken + * @param decodedToken + * @param nameSpaceList */ export function saveTokenToBundle(savePath, overWrite, createdToken, decodedToken, nameSpaceList) { let tokenContent = []; @@ -209,7 +266,7 @@ export function saveTokenToBundle(savePath, overWrite, createdToken, decodedToke const newTarBallStream = fs.createWriteStream(savePath);// create writestream at the last stage pack.pipe(zlib.createGzip()).pipe(newTarBallStream);// gzips and writes the contents to the new tarball - + if (ekvJsonOutput.isJSONOutputMode()) { ekvJsonOutput.writeJSONOutput( 0, @@ -225,7 +282,7 @@ export function saveTokenToBundle(savePath, overWrite, createdToken, decodedToke /** * Constructs the token file with static constants - * @param tokenContent + * @param tokenContent */ function constructTokenFile(tokenContent) { const token = []; @@ -240,11 +297,11 @@ function constructTokenFile(tokenContent) { /** * If only directory is specified without tgz, we create new token file and place it in the save path. * If token file already exists, token will be updated. Users can place this token file when they place it in th bundle - * @param savePath - * @param overWrite - * @param createdToken - * @param decodedToken - * @param nameSpaceList + * @param savePath + * @param overWrite + * @param createdToken + * @param decodedToken + * @param nameSpaceList */ export function createTokenFileWithoutBundle(savePath, overWrite, createdToken, decodedToken, nameSpaceList) { @@ -296,16 +353,16 @@ export function createTokenFileWithoutBundle(savePath, overWrite, createdToken, cliUtils.logWithBorder(msg); response.logToken(createdToken['name'], createdToken['value'], decodedToken, nameSpaceList, true); } - + } /** - * Validates the static content of the token + * Validates the static content of the token * If valid parses the content and returns it - * @param data - * @param createdToken - * @param decodedToken - * @param nameSpaceList + * @param data + * @param createdToken + * @param decodedToken + * @param nameSpaceList */ function validateAndGetExistingTokenContent(data, createdToken, decodedToken, nameSpaceList) { const tokenList = data.split('='); @@ -327,7 +384,7 @@ function validateAndGetExistingTokenContent(data, createdToken, decodedToken, na } tokenList[1] = tokenList[1].replace('export { edgekv_access_tokens };', ''); - + // Parse token content from the existing file try { tokenContent = JSON.parse(tokenList[1]); @@ -341,11 +398,11 @@ function validateAndGetExistingTokenContent(data, createdToken, decodedToken, na /** * Add or update token to the existing token content - * @param tokenContent - * @param nameSpaceList - * @param createdToken - * @param decodedToken - * @param overWrite + * @param tokenContent + * @param nameSpaceList + * @param createdToken + * @param decodedToken + * @param overWrite * @returns updated token content value */ function updateTokenContent(tokenContent, nameSpaceList, createdToken, decodedToken, overWrite) { @@ -355,7 +412,7 @@ function updateTokenContent(tokenContent, nameSpaceList, createdToken, decodedTo const nameSpaceContent = { 'name': createdToken['name'], 'value': createdToken['value'] }; tokenContent[ns] = nameSpaceContent; } - // if namespace already exists, if overwrite option is specified overwrite token value in file else display token + // if namespace already exists, if overwrite option is specified overwrite token value in file else display token else if (Object.prototype.hasOwnProperty.call(tokenContent, ns)) { const tokenName = tokenContent[ns]['name']; if (tokenName === createdToken['name']) { @@ -384,9 +441,9 @@ function updateTokenContent(tokenContent, nameSpaceList, createdToken, decodedTo export function getDateDifference(date) { const Difference_In_Time = date.getTime() - new Date().getTime(); - - // To calculate the no. of days between two dates - const Difference_In_Days = Difference_In_Time / (1000 * 3600 * 24); + + // To calculate the no. of days between two dates + const Difference_In_Days = Difference_In_Time / (1000 * 3600 * 24); return Difference_In_Days; } @@ -400,4 +457,4 @@ export function convertDaysToSeconds(days: number) { } else { cliUtils.logAndExit(1, 'ERROR: Retention period specified is invalid. Please specify the retention in number of days.'); } -} \ No newline at end of file +} diff --git a/src/edgekv/ekv-metricFactory.ts b/src/edgekv/ekv-metricFactory.ts index 1a5cecf..f54a5b9 100644 --- a/src/edgekv/ekv-metricFactory.ts +++ b/src/edgekv/ekv-metricFactory.ts @@ -5,6 +5,7 @@ export const ekvMetrics = { updateNamespace: 'Update/ns', initialize: 'Init/ekv', showInitStatus: 'Show/status', + updateDatabase: 'Update/db', writeItem: 'Write/item', readItem: 'Read/item', deleteItem: 'Delete/item', diff --git a/src/edgekv/ekv-response.ts b/src/edgekv/ekv-response.ts index c5dcc11..25e8460 100644 --- a/src/edgekv/ekv-response.ts +++ b/src/edgekv/ekv-response.ts @@ -14,7 +14,8 @@ export function logNamespace(nameSpaceId: string, createdNameSpace) { Namespace: nameSpaceId, RetentionPeriod: retentionPeriod, GeoLocation: createdNameSpace['geoLocation'], - GroupId: groupId + GroupId: groupId, + 'Namespace dataAccessPolicy': createdNameSpace['dataAccessPolicy'] ? 'restrictDataAccess=' + createdNameSpace['dataAccessPolicy']['restrictDataAccess'] + ', policyType=' + createdNameSpace['dataAccessPolicy']['policyType'] : 'N/A' }; console.table([createNameSpace]); } @@ -59,7 +60,8 @@ export function logInitialize(initializedEdgekv) { AccountStatus: initializedEdgekv['accountStatus'], ProductionStatus: initializedEdgekv['productionStatus'], StagingStatus: initializedEdgekv['stagingStatus'], - Cpcode: initializedEdgekv['cpcode'] + Cpcode: initializedEdgekv['cpcode'], + DataAccessPolicy: initializedEdgekv['dataAccessPolicy'] ? 'restrictDataAccess=' + initializedEdgekv['dataAccessPolicy']['restrictDataAccess'] + ', allowNamespacePolicyOverride=' + initializedEdgekv['dataAccessPolicy']['allowNamespacePolicyOverride'] : 'N/A' }; console.table([initializeStatus]); } diff --git a/src/edgekv/ekv-service.ts b/src/edgekv/ekv-service.ts index 6040c63..3d29ca5 100644 --- a/src/edgekv/ekv-service.ts +++ b/src/edgekv/ekv-service.ts @@ -39,7 +39,8 @@ export function createNameSpace( namespace: string, retention, groupId, - geoLocation + geoLocation, + dataAccessPolicy: object = undefined ) { const body = { namespace: namespace, @@ -47,6 +48,9 @@ export function createNameSpace( groupId: groupId, geoLocation: geoLocation, }; + if (dataAccessPolicy !== undefined) { + body['dataAccessPolicy'] = dataAccessPolicy; + } return httpEdge .postJson( `${EDGEKV_API_BASE}/networks/${network}/namespaces`, @@ -93,11 +97,12 @@ export function updateNameSpace( .catch((err) => error.handleError(err)); } -export function initializeEdgeKV() { +export function initializeEdgeKV(dataAccessPolicy) { + const body = dataAccessPolicy ? { dataAccessPolicy } : undefined; return httpEdge .putJson( `${EDGEKV_API_BASE}/initialize`, - '', + body, cliUtils.getTimeout(INIT_EKV_TIMEOUT), ekvMetrics.initialize ) @@ -116,6 +121,21 @@ export function getInitializedEdgeKV() { .catch((err) => error.handleError(err)); } +export function updateDatabase(dataAccessPolicy) { + const body = { + dataAccessPolicy + }; + return httpEdge + .putJson( + `${EDGEKV_API_BASE}/auth/database`, + body, + cliUtils.getTimeout(INIT_EKV_TIMEOUT), + ekvMetrics.updateDatabase + ) + .then((r) => r.response) + .catch((err) => error.handleError(err)); +} + export function writeItems( network: string, namespace: string, @@ -209,7 +229,7 @@ export function readItem( .then((r) => { // manually parse body if needed return typeof r.body === 'string' ? r.body : cliUtils.parseIfJSON(r.body); - }) + }) .catch((err) => error.handleError(err)); } diff --git a/tests/edgekv/ekv-services.test.ts b/tests/edgekv/ekv-services.test.ts index fbe5ea3..843ee1d 100644 --- a/tests/edgekv/ekv-services.test.ts +++ b/tests/edgekv/ekv-services.test.ts @@ -148,11 +148,15 @@ describe('ekv-services tests', () => { const retention = 1000; const groupId = 'mockGroup'; const geoLocation = 'mocklocation'; + const dataAccessPolicy = { + restrictDataAccess: true + }; const mockReqBody = { namespace: mockNamespace, retentionInSeconds: retention, groupId: groupId, geoLocation: geoLocation, + dataAccessPolicy: dataAccessPolicy }; const mockResBody = { mesaage: 'success' }; @@ -178,7 +182,8 @@ describe('ekv-services tests', () => { mockNamespace, retention, groupId, - geoLocation + geoLocation, + dataAccessPolicy ); expect(createNsSpy).toHaveBeenCalled(); @@ -199,7 +204,8 @@ describe('ekv-services tests', () => { mockNamespace, retention, groupId, - geoLocation + geoLocation, + dataAccessPolicy ); expect(createNsSpy).toHaveBeenCalled(); @@ -328,7 +334,13 @@ describe('ekv-services tests', () => { }); describe('testing initializeEdgeKV', () => { - const mockReqBody = ''; + const dataAccessPolicy = { + restrictDataAccess: true, + allowNamespacePolicyOverride: false + }; + const mockReqBody = { + dataAccessPolicy: dataAccessPolicy + }; const mockResponse = { statusCode: 200, body: 'success', @@ -349,7 +361,7 @@ describe('ekv-services tests', () => { }); const initializeSpy = jest.spyOn(ekvService, 'initializeEdgeKV'); - const res = await ekvService.initializeEdgeKV(); + const res = await ekvService.initializeEdgeKV(dataAccessPolicy); expect(initializeSpy).toHaveBeenCalled(); expect(res).toEqual(mockResponse); @@ -364,7 +376,7 @@ describe('ekv-services tests', () => { }); const initializeSpy = jest.spyOn(ekvService, 'initializeEdgeKV'); - const error = await ekvService.initializeEdgeKV(); + const error = await ekvService.initializeEdgeKV(dataAccessPolicy); expect(initializeSpy).toHaveBeenCalled(); // Check the details of error object @@ -423,6 +435,63 @@ describe('ekv-services tests', () => { }); }); + describe('testing updateDatabase', () => { + const dataAccessPolicy = { + dataAccessPolicy: { + restrictDataAccess: true, + allowNamespacePolicyOverride: false + } + }; + const mockReqBody = { + dataAccessPolicy + }; + const mockResponse = { + statusCode: 200, + body: 'success', + }; + + test('response should return update success', async () => { + // Mock putJson() method + const putJsonSpy = jest.spyOn(httpEdge, 'putJson'); + putJsonSpy.mockImplementation((path, body, timeout, metricType) => { + expect(path).toContain(`${ekvService.EDGEKV_API_BASE}/auth/database`); + expect(body).toEqual(mockReqBody); + expect(timeout).toEqual(initTimeout); + expect(metricType).toEqual(ekvMetrics.updateDatabase); + + return Promise.resolve({ + response: mockResponse, + }); + }); + + const updateDatabaseSpy = jest.spyOn(ekvService, 'updateDatabase'); + const res = await ekvService.updateDatabase(dataAccessPolicy); + + expect(updateDatabaseSpy).toHaveBeenCalled(); + expect(res).toEqual(mockResponse); + }); + + test('function should handle errors properly', async () => { + // Mock putJson() method + const putJsonSpy = jest.spyOn(httpEdge, 'putJson'); + putJsonSpy.mockImplementation(() => { + // The normal error object will be returned as a string + return Promise.reject(JSON.stringify(mockError)); + }); + + const updateDatabaseSpy = jest.spyOn(ekvService, 'updateDatabase'); + const error = await ekvService.updateDatabase(dataAccessPolicy); + + expect(updateDatabaseSpy).toHaveBeenCalled(); + // Check the details of error object + expect(error).not.toBeUndefined; + expect(error.isError).toEqual(true); + expect(error.status).toEqual(mockError.status); + expect(error.error_reason).toEqual(mockError.detail); + expect(error.traceId).toEqual(mockError.traceId); + }); + }); + describe('testing writeItems', () => { const textItem = 'mockItem'; const jsonItems = {