diff --git a/.github/workflows/test-and-release.yaml b/.github/workflows/test-and-release.yaml index e3da252f4..4a925b880 100644 --- a/.github/workflows/test-and-release.yaml +++ b/.github/workflows/test-and-release.yaml @@ -73,10 +73,10 @@ jobs: run: | npx cdk --app "npx tsx --no-warnings cdk/test-resources.ts" deploy --require-approval never MOCK_HTTP_API_ENDPOINT=`aws cloudformation describe-stacks --stack-name ${{ env.STACK_NAME }}-test | jq -r '.Stacks[0].Outputs[] | select(.OutputKey == "apiURL") | .OutputValue' | sed -E 's/\/$//g'` - ./cli.sh configure thirdParty/acme/apiEndpoint ${MOCK_HTTP_API_ENDPOINT} - ./cli.sh configure thirdParty/acme/apiKey apiKey_Acme - ./cli.sh configure thirdParty/elite/apiEndpoint ${MOCK_HTTP_API_ENDPOINT} - ./cli.sh configure thirdParty/elite/apiKey apiKey_Elite + ./cli.sh configure-nrfcloud-account acme apiEndpoint ${MOCK_HTTP_API_ENDPOINT} + ./cli.sh configure-nrfcloud-account acme apiKey apiKey_Acme + ./cli.sh configure-nrfcloud-account elite apiEndpoint ${MOCK_HTTP_API_ENDPOINT} + ./cli.sh configure-nrfcloud-account elite apiKey apiKey_Elite - name: Deploy solution stack env: @@ -119,11 +119,11 @@ jobs: npx cdk destroy -f npx cdk --app 'npx tsx --no-warnings cdk/test-resources.ts' destroy -f ./cli.sh fake-nrfcloud-account-device acme --remove - ./cli.sh configure thirdParty/acme/apiEndpoint -X - ./cli.sh configure thirdParty/acme/apiKey -X + ./cli.sh configure-nrfcloud-account acme apiEndpoint -X + ./cli.sh configure-nrfcloud-account acme apiKey -X ./cli.sh fake-nrfcloud-account-device elite --remove - ./cli.sh configure thirdParty/elite/apiEndpoint -X - ./cli.sh configure thirdParty/elite/apiKey -X + ./cli.sh configure-nrfcloud-account elite apiEndpoint -X + ./cli.sh configure-nrfcloud-account elite apiKey -X ./cli.sh clean-backup-certificates release: diff --git a/README.md b/README.md index 43e3da185..d826dfaef 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ certificate used by MQTT broker to connect nRF Cloud under your account. So, you need to prepare nRF Cloud API key. ```bash -./cli.sh configure thirdParty//apiKey +./cli.sh configure-nrfcloud-account apiKey ./cli.sh initialize-nrfcloud-account ./cli.sh create-health-check-device ``` diff --git a/cdk/resources/ConfigureDevice.ts b/cdk/resources/ConfigureDevice.ts index 685e91e8a..aa403380b 100644 --- a/cdk/resources/ConfigureDevice.ts +++ b/cdk/resources/ConfigureDevice.ts @@ -12,6 +12,7 @@ import type { PackedLambda } from '../helpers/lambdas/packLambda.js' import { LambdaSource } from './LambdaSource.js' import type { WebsocketAPI } from './WebsocketAPI.js' import { Context } from '@hello.nrfcloud.com/proto/hello' +import { Scope } from '../../util/settings.js' /** * Handles device configuration requests @@ -56,10 +57,14 @@ export class ConfigureDevice extends Construct { resources: [ `arn:aws:ssm:${Stack.of(this).region}:${ Stack.of(this).account - }:parameter/${Stack.of(this).stackName}/thirdParty`, + }:parameter/${Stack.of(this).stackName}/${ + Scope.NRFCLOUD_ACCOUNT_PREFIX + }`, `arn:aws:ssm:${Stack.of(this).region}:${ Stack.of(this).account - }:parameter/${Stack.of(this).stackName}/thirdParty/*`, + }:parameter/${Stack.of(this).stackName}/${ + Scope.NRFCLOUD_ACCOUNT_PREFIX + }/*`, `arn:aws:ssm:${Stack.of(this).region}:${ Stack.of(this).account }:parameter/${Stack.of(this).stackName}/nRFCloud/accounts`, diff --git a/cdk/resources/DeviceShadow.ts b/cdk/resources/DeviceShadow.ts index 8d14980c9..4f56233dc 100644 --- a/cdk/resources/DeviceShadow.ts +++ b/cdk/resources/DeviceShadow.ts @@ -15,6 +15,7 @@ import { Construct } from 'constructs' import type { PackedLambda } from '../helpers/lambdas/packLambda' import { LambdaSource } from './LambdaSource.js' import type { WebsocketAPI } from './WebsocketAPI.js' +import { Scope } from '../../util/settings.js' export class DeviceShadow extends Construct { public constructor( @@ -120,10 +121,14 @@ export class DeviceShadow extends Construct { resources: [ `arn:aws:ssm:${Stack.of(this).region}:${ Stack.of(this).account - }:parameter/${Stack.of(this).stackName}/thirdParty`, + }:parameter/${Stack.of(this).stackName}/${ + Scope.NRFCLOUD_ACCOUNT_PREFIX + }`, `arn:aws:ssm:${Stack.of(this).region}:${ Stack.of(this).account - }:parameter/${Stack.of(this).stackName}/thirdParty/*`, + }:parameter/${Stack.of(this).stackName}/${ + Scope.NRFCLOUD_ACCOUNT_PREFIX + }/*`, `arn:aws:ssm:${Stack.of(this).region}:${ Stack.of(this).account }:parameter/${Stack.of(this).stackName}/nRFCloud/accounts`, diff --git a/cdk/resources/HealthCheckMqttBridge.ts b/cdk/resources/HealthCheckMqttBridge.ts index 3b8ca2f97..db6a195cb 100644 --- a/cdk/resources/HealthCheckMqttBridge.ts +++ b/cdk/resources/HealthCheckMqttBridge.ts @@ -13,6 +13,7 @@ import type { PackedLambda } from '../helpers/lambdas/packLambda.js' import type { DeviceStorage } from './DeviceStorage.js' import type { WebsocketAPI } from './WebsocketAPI.js' import { LambdaSource } from './LambdaSource.js' +import { Scope } from '../../util/settings.js' export type BridgeImageSettings = BridgeSettings @@ -64,10 +65,14 @@ export class HealthCheckMqttBridge extends Construct { resources: [ `arn:aws:ssm:${Stack.of(this).region}:${ Stack.of(this).account - }:parameter/${Stack.of(this).stackName}/thirdParty`, + }:parameter/${Stack.of(this).stackName}/${ + Scope.NRFCLOUD_ACCOUNT_PREFIX + }`, `arn:aws:ssm:${Stack.of(this).region}:${ Stack.of(this).account - }:parameter/${Stack.of(this).stackName}/thirdParty/*`, + }:parameter/${Stack.of(this).stackName}/${ + Scope.NRFCLOUD_ACCOUNT_PREFIX + }/*`, `arn:aws:ssm:${Stack.of(this).region}:${ Stack.of(this).account }:parameter/${Stack.of(this).stackName}/nRFCloud/accounts`, diff --git a/cdk/resources/Integration.ts b/cdk/resources/Integration.ts index 7354756dc..faece64d2 100644 --- a/cdk/resources/Integration.ts +++ b/cdk/resources/Integration.ts @@ -221,7 +221,7 @@ export class Integration extends Construct { const { environment, secrets } = Object.entries(nRFCloudAccounts).reduce( (result, [account], index) => { const bridgeNo = String(index + 2).padStart(2, '0') - const scope = `thirdParty/${account}` + const scope = `${Scope.NRFCLOUD_ACCOUNT_PREFIX}/${account}` const bridgePrefix = `MOSQUITTO__BRIDGE${bridgeNo}` result.environment[ diff --git a/cdk/resources/SingleCellGeoLocation.ts b/cdk/resources/SingleCellGeoLocation.ts index 6a1d2d21f..131fb3892 100644 --- a/cdk/resources/SingleCellGeoLocation.ts +++ b/cdk/resources/SingleCellGeoLocation.ts @@ -12,6 +12,7 @@ import { LambdaSource } from './LambdaSource.js' import type { WebsocketAPI } from './WebsocketAPI.js' import { IoTActionRole } from './IoTActionRole.js' import type { DeviceStorage } from './DeviceStorage.js' +import { Scope } from '../../util/settings.js' /** * Resolve device geo location based on network information @@ -61,10 +62,14 @@ export class SingleCellGeoLocation extends Construct { resources: [ `arn:aws:ssm:${Stack.of(this).region}:${ Stack.of(this).account - }:parameter/${Stack.of(this).stackName}/thirdParty`, + }:parameter/${Stack.of(this).stackName}/${ + Scope.NRFCLOUD_ACCOUNT_PREFIX + }`, `arn:aws:ssm:${Stack.of(this).region}:${ Stack.of(this).account - }:parameter/${Stack.of(this).stackName}/thirdParty/*`, + }:parameter/${Stack.of(this).stackName}/${ + Scope.NRFCLOUD_ACCOUNT_PREFIX + }/*`, `arn:aws:ssm:${Stack.of(this).region}:${ Stack.of(this).account }:parameter/${Stack.of(this).stackName}/nRFCloud/accounts`, diff --git a/cli/cli.ts b/cli/cli.ts index 301936a24..c002bd360 100644 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -13,7 +13,6 @@ import { STACK_NAME } from '../cdk/stacks/stackConfig.js' import psjon from '../package.json' import type { CommandDefinition } from './commands/CommandDefinition' import { configureDeviceCommand } from './commands/configure-device.js' -import { configureCommand } from './commands/configure.js' import { createFakeNrfCloudAccountDeviceCredentials } from './commands/create-fake-nrfcloud-account-device-credentials.js' import { createFakeNrfCloudHealthCheckDevice } from './commands/create-fake-nrfcloud-health-check-device.js' import { createHealthCheckDevice } from './commands/create-health-check-device.js' @@ -30,6 +29,7 @@ import { showNRFCloudAccount } from './commands/show-nrfcloud-account.js' import { simulateDeviceCommand } from './commands/simulate-device.js' import { cleanBackupCertificates } from './commands/clean-backup-certificates.js' import { listnRFCloudAccountsCommand } from './commands/list-nrfcloud-accounts.js' +import { configureRFCloudAccountCommand } from './commands/configure-nrfcloud-account.js' const ssm = new SSMClient({}) const iot = new IoTClient({}) @@ -60,7 +60,7 @@ const CLI = async ({ isCI }: { isCI: boolean }) => { program.version(psjon.version) const commands: CommandDefinition[] = [ - configureCommand({ ssm }), + configureRFCloudAccountCommand({ ssm }), setShadowFetcherCommand({ ssm }), logsCommand({ stackName: STACK_NAME, cf, logs }), cleanBackupCertificates({ ssm }), diff --git a/cli/commands/configure.ts b/cli/commands/configure-nrfcloud-account.ts similarity index 72% rename from cli/commands/configure.ts rename to cli/commands/configure-nrfcloud-account.ts index 5171f97c5..d1706b1ef 100644 --- a/cli/commands/configure.ts +++ b/cli/commands/configure-nrfcloud-account.ts @@ -2,15 +2,19 @@ import { SSMClient } from '@aws-sdk/client-ssm' import chalk from 'chalk' import fs from 'fs' import { STACK_NAME } from '../../cdk/stacks/stackConfig.js' -import { deleteSettings, putSettings } from '../../util/settings.js' import type { CommandDefinition } from './CommandDefinition.js' +import { + deleteSettings, + putSetting, + type Settings, +} from '../../nrfcloud/settings.js' -export const configureCommand = ({ +export const configureRFCloudAccountCommand = ({ ssm, }: { ssm: SSMClient }): CommandDefinition => ({ - command: 'configure [value]', + command: 'configure-nrfcloud-account [value]', options: [ { flags: '-d, --deleteBeforeUpdate', @@ -22,27 +26,18 @@ export const configureCommand = ({ }, ], action: async ( - path: string, + account: string, + property: string, value: string | undefined, { deleteBeforeUpdate, deleteParameter }, ) => { - const parts = path.split('/') - const property = parts.pop() - - const scope = parts.join('/') - - if (property === undefined || property.length === 0) - throw new Error(`Must specify a parameter.`) - if (deleteParameter !== undefined) { // Delete const { name } = await deleteSettings({ ssm, stackName: STACK_NAME, - scope, - })({ - property, - }) + account, + })(property) console.log() console.log( chalk.green('Deleted the parameters from'), @@ -56,15 +51,11 @@ export const configureCommand = ({ throw new Error(`Must provide value either as argument or via stdin!`) } - const { name } = await putSettings({ + const { name } = await putSetting({ ssm, stackName: STACK_NAME, - scope, - })({ - property, - value: v, - deleteBeforeUpdate, - }) + account, + })(property as keyof Settings, v, deleteBeforeUpdate) console.log() console.log( diff --git a/cli/commands/create-fake-nrfcloud-account-device-credentials.ts b/cli/commands/create-fake-nrfcloud-account-device-credentials.ts index 534a6fcdf..47767468b 100644 --- a/cli/commands/create-fake-nrfcloud-account-device-credentials.ts +++ b/cli/commands/create-fake-nrfcloud-account-device-credentials.ts @@ -22,14 +22,9 @@ import { chunk } from 'lodash-es' import { randomUUID } from 'node:crypto' import { getIoTEndpoint } from '../../aws/getIoTEndpoint.js' import { STACK_NAME } from '../../cdk/stacks/stackConfig.js' -import { updateSettings, type Settings } from '../../nrfcloud/settings.js' +import { putSettings, type Settings } from '../../nrfcloud/settings.js' import { isString } from '../../util/isString.js' -import { - Scope, - deleteSettings, - putSettings, - settingsPath, -} from '../../util/settings.js' +import { Scope, settingsPath } from '../../util/settings.js' import type { CommandDefinition } from './CommandDefinition.js' export const createFakeNrfCloudAccountDeviceCredentials = ({ @@ -47,7 +42,7 @@ export const createFakeNrfCloudAccountDeviceCredentials = ({ }, ], action: async (account, { remove }) => { - const scope = `thirdParty/${account}` + const scope = `${Scope.NRFCLOUD_ACCOUNT_PREFIX}/${account}` const fakeTenantParameter = `/${STACK_NAME}/${account}/fakeTenant` if (remove === true) { // check if has fake device @@ -131,12 +126,6 @@ export const createFakeNrfCloudAccountDeviceCredentials = ({ ) } - await deleteSettings({ - ssm, - stackName: STACK_NAME, - scope: Scope.NRFCLOUD_ACCOUNT, - })({ property: account }) - return } @@ -177,14 +166,6 @@ export const createFakeNrfCloudAccountDeviceCredentials = ({ throw new Error(`Failed to create certificate!`) } - await putSettings({ - ssm, - stackName: STACK_NAME, - scope: Scope.NRFCLOUD_ACCOUNT, - })({ - property: account, - value: account, - }) const settings: Partial = { accountDeviceClientCert: credentials.certificatePem, accountDevicePrivateKey: pk, @@ -192,10 +173,10 @@ export const createFakeNrfCloudAccountDeviceCredentials = ({ mqttEndpoint: await getIoTEndpoint({ iot })(), mqttTopicPrefix: `prod/${tenantId}/`, } - await updateSettings({ + await putSettings({ ssm, stackName: STACK_NAME, - scope, + account, })(settings) console.debug(chalk.white(`nRF Cloud settings:`)) diff --git a/lambda/ws/AuthorizedEvent.ts b/lambda/ws/AuthorizedEvent.ts index eba2b460c..ba848d9a3 100644 --- a/lambda/ws/AuthorizedEvent.ts +++ b/lambda/ws/AuthorizedEvent.ts @@ -7,7 +7,7 @@ export type AuthorizedEvent = APIGatewayProxyWebsocketEventV2 & { model: string //e.g. "PCA20035+solar", integrationLatency: 1043 deviceId: string // e.g. 'oob-352656108602296' - account: string // e.g. 'exeger' + account: string // e.g. 'elite' } } } diff --git a/nrfcloud/allAccounts.ts b/nrfcloud/allAccounts.ts index c2a5f3b85..aa1e16b8a 100644 --- a/nrfcloud/allAccounts.ts +++ b/nrfcloud/allAccounts.ts @@ -19,14 +19,17 @@ export const getAllnRFCloudAccounts = async ({ }: { ssm: SSMClient stackName: string -}): Promise => - Object.keys( - await getSettings({ - ssm, - stackName, - scope: Scope.NRFCLOUD_ACCOUNT, - })(), - ) +}): Promise => [ + ...new Set( + Object.keys( + await getSettings({ + ssm, + stackName, + scope: Scope.NRFCLOUD_ACCOUNT_PREFIX, + })(), + ).map((key) => key.split('/')[0] as string), + ), +] export const getAllAccountsSettings = ({ ssm, stackName }: { ssm: SSMClient; stackName: string }) => diff --git a/nrfcloud/healthCheckSettings.ts b/nrfcloud/healthCheckSettings.ts index 446c1e6ab..bcb68aa1e 100644 --- a/nrfcloud/healthCheckSettings.ts +++ b/nrfcloud/healthCheckSettings.ts @@ -1,5 +1,9 @@ import type { SSMClient } from '@aws-sdk/client-ssm' -import { getSettings as getSSMSettings, putSettings } from '../util/settings.js' +import { + Scope, + getSettings as getSSMSettings, + putSettings, +} from '../util/settings.js' export type Settings = { healthCheckClientCert: string @@ -21,7 +25,7 @@ export const updateSettings = ({ const settingsWriter = putSettings({ ssm, stackName, - scope: `thirdParty/${account}`, + scope: `${Scope.NRFCLOUD_ACCOUNT_PREFIX}/${account}`, }) return async (settings): Promise => { await Promise.all( @@ -44,7 +48,7 @@ export const getSettings = ({ stackName: string account: string }): (() => Promise) => { - const scope = `thirdParty/${account}` + const scope = `${Scope.NRFCLOUD_ACCOUNT_PREFIX}/${account}` const settingsReader = getSSMSettings({ ssm, stackName, diff --git a/nrfcloud/initializeAccount.ts b/nrfcloud/initializeAccount.ts index c7b6790fc..30652abbd 100644 --- a/nrfcloud/initializeAccount.ts +++ b/nrfcloud/initializeAccount.ts @@ -3,14 +3,14 @@ import { SSMClient } from '@aws-sdk/client-ssm' import chalk from 'chalk' import { getIoTEndpoint } from '../aws/getIoTEndpoint.js' import { STACK_NAME } from '../cdk/stacks/stackConfig.js' -import { Scope, getSettings, putSettings } from '../util/settings.js' +import { Scope, getSettings } from '../util/settings.js' import { createAccountDevice } from './createAccountDevice.js' import { deleteAccountDevice } from './deleteNrfcloudCredentials.js' import { getAccountInfo } from './getAccountInfo.js' import { defaultApiEndpoint, getSettings as getNRFCloudSettings, - updateSettings, + putSettings as putNRFCloudSettings, type Settings, } from './settings.js' @@ -30,7 +30,7 @@ export const initializeAccount = account: string }) => async (reset = false): Promise => { - const scope = `thirdParty/${account}` + const scope = `${Scope.NRFCLOUD_ACCOUNT_PREFIX}/${account}` const settingsReader = getSettings({ ssm, stackName, @@ -98,13 +98,7 @@ export const initializeAccount = } console.log(chalk.green(`Account device created.`)) - await putSettings({ - ssm, - stackName: STACK_NAME, - scope: Scope.NRFCLOUD_ACCOUNT, - })({ property: account, value: account }) - - await updateSettings({ ssm, stackName: STACK_NAME, scope })({ + await putNRFCloudSettings({ ssm, stackName: STACK_NAME, account })({ accountDeviceClientCert: clientCert, accountDevicePrivateKey: privateKey, accountDeviceClientId: `account-${accountInfo.tenantId}`, diff --git a/nrfcloud/settings.ts b/nrfcloud/settings.ts index 856a09673..3d3e31d7f 100644 --- a/nrfcloud/settings.ts +++ b/nrfcloud/settings.ts @@ -1,8 +1,10 @@ import type { SSMClient } from '@aws-sdk/client-ssm' import { getSettings as getSSMSettings, - putSettings, + putSettings as putSSMSettings, settingsPath, + deleteSettings as deleteSSMSettings, + Scope, } from '../util/settings.js' export const defaultApiEndpoint = new URL('https://api.nrfcloud.com') @@ -26,7 +28,7 @@ export const getSettings = ({ stackName: string account: string }): (() => Promise) => { - const scope = `thirdParty/${account}` + const scope = `${Scope.NRFCLOUD_ACCOUNT_PREFIX}/${account}` const settingsReader = getSSMSettings({ ssm, stackName, @@ -81,7 +83,7 @@ export const getAPISettings = ({ const settingsReader = getSSMSettings({ ssm, stackName, - scope: `thirdParty/${account}`, + scope: `${Scope.NRFCLOUD_ACCOUNT_PREFIX}/${account}`, }) return async (): Promise> => { const p = await settingsReader() @@ -97,19 +99,19 @@ export const getAPISettings = ({ } } -export const updateSettings = ({ +export const putSettings = ({ ssm, stackName, - scope, + account, }: { ssm: SSMClient stackName: string - scope: string + account: string }): ((settings: Partial) => Promise) => { - const settingsWriter = putSettings({ + const settingsWriter = putSSMSettings({ ssm, stackName, - scope, + scope: `${Scope.NRFCLOUD_ACCOUNT_PREFIX}/${account}`, }) return async (settings): Promise => { await Promise.all( @@ -123,6 +125,32 @@ export const updateSettings = ({ } } +export const putSetting = ({ + ssm, + stackName, + account, +}: { + ssm: SSMClient + stackName: string + account: string +}): (( + property: keyof Settings, + value: string, + deleteBeforeUpdate: boolean, +) => ReturnType) => { + const settingsWriter = putSSMSettings({ + ssm, + stackName, + scope: `${Scope.NRFCLOUD_ACCOUNT_PREFIX}/${account}`, + }) + return async (property, value, deleteBeforeUpdate) => + settingsWriter({ + property, + value, + deleteBeforeUpdate, + }) +} + export const parameterName = ( stackName: string, scope: string, @@ -133,3 +161,20 @@ export const parameterName = ( scope, property: parameterName, }) + +export const deleteSettings = ({ + ssm, + stackName, + account, +}: { + ssm: SSMClient + stackName: string + account: string +}): ((property: string) => ReturnType) => { + const settingsDeleter = deleteSSMSettings({ + ssm, + stackName, + scope: `${Scope.NRFCLOUD_ACCOUNT_PREFIX}/${account}`, + }) + return async (property) => settingsDeleter({ property }) +} diff --git a/util/settings.spec.ts b/util/settings.spec.ts index 0801b99bd..952752f3b 100644 --- a/util/settings.spec.ts +++ b/util/settings.spec.ts @@ -37,11 +37,11 @@ describe('settingsPath()', () => { it('should produce a fully qualified parameter name for valid string scope', () => expect( settingsPath({ - scope: 'thirdParty/exeger', + scope: 'thirdParty/elite', stackName: 'hello-nrfcloud', property: 'someProperty', }), - ).toEqual('/hello-nrfcloud/thirdParty/exeger/someProperty')) + ).toEqual('/hello-nrfcloud/thirdParty/elite/someProperty')) it('should error for invalid string scope', () => { expect(() => diff --git a/util/settings.ts b/util/settings.ts index fa6877cac..42d130755 100644 --- a/util/settings.ts +++ b/util/settings.ts @@ -12,13 +12,13 @@ export enum Scope { STACK_MQTT_BRIDGE = 'stack/mqttBridge', NRFCLOUD_BRIDGE_CERTIFICATE_MQTT = 'nRFCloudBridgeCertificate/MQTT', NRFCLOUD_BRIDGE_CERTIFICATE_CA = 'nRFCloudBridgeCertificate/CA', - NRFCLOUD_ACCOUNT = 'nRFCloud/accounts', + NRFCLOUD_ACCOUNT_PREFIX = 'thirdParty', } const validScope = (scope: string): boolean => { return ( Object.values(Scope).map(String).includes(scope) || - /^thirdParty\/[a-zA-Z0-9_.-]+$/.test(scope) + new RegExp(`^${Scope.NRFCLOUD_ACCOUNT_PREFIX}/[a-zA-Z0-9_.-]+$`).test(scope) ) }