Skip to content

Commit

Permalink
Add secret store imports and exports
Browse files Browse the repository at this point in the history
  • Loading branch information
phalestrivir committed Nov 14, 2024
1 parent 814e650 commit f9c4a17
Show file tree
Hide file tree
Showing 9 changed files with 2,484 additions and 0 deletions.
173 changes: 173 additions & 0 deletions src/api/classic/SecretStoreApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import util from 'util';

import { State } from '../../shared/State';
import { getConfigPath, getRealmPathGlobal } from '../../utils/ForgeRockUtils';
import {
AmConfigEntityInterface,
IdObjectSkeletonInterface,
PagedResult,
} from '../ApiTypes';
import { generateAmApi } from '../BaseApi';

const secretStoreURLTemplate = '%s/json%s/%s/secrets/stores/%s/%s';
const secretStoresURLTemplate =
'%s/json%s/%s/secrets/stores?_action=nextdescendents';
const secretStoreMappingURLTemplate = secretStoreURLTemplate + '/mappings/%s';
const secretStoreMappingsURLTemplate =
secretStoreURLTemplate + '/mappings?_queryFilter=true';

const secretTypesThatIgnoreId = ['EnvironmentAndSystemPropertySecretStore'];

const apiVersion = 'protocol=2.1,resource=%s';
const globalVersion = '1.0';
const realmVersion = '2.0';
const getApiConfig = (globalConfig) => {
return {
apiVersion: util.format(
apiVersion,
globalConfig ? globalVersion : realmVersion
),
};
};

export type SecretStoreSkeleton = AmConfigEntityInterface;

export type SecretStoreMappingSkeleton = IdObjectSkeletonInterface & {
secretId: string;
aliases: string[];
};

/**
* Get all secret stores
* @param {boolean} globalConfig true if the secret store is global, false otherwise. Default: false.
* @returns {Promise<PagedResult<SecretStoreSkeleton>>} a promise that resolves to an array of secret store objects
*/
export async function getSecretStores({
globalConfig = false,
state,
}: {
globalConfig: boolean;
state: State;
}): Promise<PagedResult<SecretStoreSkeleton>> {
const urlString = util.format(
secretStoresURLTemplate,
state.getHost(),
getRealmPathGlobal(globalConfig, state),
getConfigPath(globalConfig)
);
const { data } = await generateAmApi({
resource: getApiConfig(globalConfig),
state,
}).post(urlString, undefined, {
withCredentials: true,
});
return data;
}

/**
* Get secret store mappings
* @param {string} secretStoreId Secret store id
* @param {string} secretStoreTypeId Secret store type id
* @param {boolean} globalConfig true if the secret store is global, false otherwise. Default: false.
* @returns {Promise<SecretStoreMappingSkeleton[]>} a promise that resolves to an array of secret store mapping objects
*/
export async function getSecretStoreMappings({
secretStoreId,
secretStoreTypeId,
globalConfig = false,
state,
}: {
secretStoreId: string;
secretStoreTypeId: string;
globalConfig: boolean;
state: State;
}): Promise<PagedResult<SecretStoreMappingSkeleton>> {
const urlString = util.format(
secretStoreMappingsURLTemplate,
state.getHost(),
getRealmPathGlobal(globalConfig, state),
getConfigPath(globalConfig),
secretStoreTypeId,
secretStoreId
);
const { data } = await generateAmApi({
resource: getApiConfig(globalConfig),
state,
}).get(urlString, {
withCredentials: true,
});
return data;
}

/**
* Put secret store
* @param {SecretStoreSkeleton} secretStoreData secret store to import
* @param {boolean} globalConfig true if the secret store is global, false otherwise. Default: false.
* @returns {Promise<SecretStoreSkeleton>} a promise that resolves to a secret store object
*/
export async function putSecretStore({
secretStoreData,
globalConfig = false,
state,
}: {
secretStoreData: SecretStoreSkeleton;
globalConfig: boolean;
state: State;
}): Promise<SecretStoreSkeleton> {
const urlString = util.format(
secretStoreURLTemplate,
state.getHost(),
getRealmPathGlobal(globalConfig, state),
getConfigPath(globalConfig),
secretStoreData._type._id,
secretTypesThatIgnoreId.includes(secretStoreData._type._id)
? ''
: secretStoreData._id
);
const { data } = await generateAmApi({
resource: getApiConfig(globalConfig),
state,
}).put(urlString, secretStoreData, {
withCredentials: true,
});
return data;
}

/**
* Put secret store mapping
* @param {string} secretStoreId Secret store id
* @param {string} secretStoreTypeId Secret store type id
* @param {SecretStoreMappingSkeleton} secretStoreMappingData secret store mapping to import
* @param {boolean} globalConfig true if the secret store mapping is global, false otherwise. Default: false.
* @returns {Promise<SecretStoreMappingSkeleton>} a promise that resolves to a secret store mapping object
*/
export async function putSecretStoreMapping({
secretStoreId,
secretStoreTypeId,
secretStoreMappingData,
globalConfig = false,
state,
}: {
secretStoreId: string;
secretStoreTypeId: string;
secretStoreMappingData: SecretStoreMappingSkeleton;
globalConfig: boolean;
state: State;
}): Promise<SecretStoreMappingSkeleton> {
const urlString = util.format(
secretStoreMappingURLTemplate,
state.getHost(),
getRealmPathGlobal(globalConfig, state),
getConfigPath(globalConfig),
secretStoreTypeId,
secretStoreId,
secretStoreMappingData._id
);
const { data } = await generateAmApi({
resource: getApiConfig(globalConfig),
state,
}).put(urlString, secretStoreMappingData, {
withCredentials: true,
});
return data;
}
3 changes: 3 additions & 0 deletions src/lib/FrodoLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import AuthenticationSettingsOps, {
AuthenticationSettings,
} from '../ops/AuthenticationSettingsOps';
import CirclesOfTrustOps, { CirclesOfTrust } from '../ops/CirclesOfTrustOps';
import SecretStoreOps, { SecretStore } from '../ops/classic/SecretStoreOps';
import ServerOps, { Server } from '../ops/classic/ServerOps';
import AdminFederationOps, {
AdminFederation,
Expand Down Expand Up @@ -186,6 +187,7 @@ export type Frodo = {

script: Script;
server: Server;
secretStore: SecretStore;
service: Service;
session: Session;

Expand Down Expand Up @@ -354,6 +356,7 @@ const FrodoLib = (config: StateInterface = {}): Frodo => {

script: ScriptOps(state),
server: ServerOps(state),
secretStore: SecretStoreOps(state),
service: ServiceOps(state),
session: SessionOps(state),

Expand Down
142 changes: 142 additions & 0 deletions src/ops/classic/SecretStoreOps.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* To record and update snapshots, you must perform 3 steps in order:
*
* 1. Record API responses
*
* Recording requires an available classic deployment, since secret stores
* can only be accessed in classic. Set FRODO_HOST and FRODO_REALM
* environment variables or alternatively FRODO_DEPLOY=classic
* in order to appropriately record requests to the classic deployment.
*
* To record API responses, you must call the test:record script and
* override all the connection state required to connect to the
* env to record from:
*
* ATTENTION: For the recording to succeed, you MUST make sure to use a
* user account, not a service account.
*
* FRODO_DEBUG=1 FRODO_HOST=frodo-dev npm run test:record SecretStoreOps
*
* The above command assumes that you have a connection profile for
* 'frodo-dev' on your development machine.
*
* 2. Update snapshots
*
* After recording API responses, you must manually update/create snapshots
* by running:
*
* FRODO_DEBUG=1 npm run test:update SecretStoreOps
*
* 3. Test your changes
*
* If 1 and 2 didn't produce any errors, you are ready to run the tests in
* replay mode and make sure they all succeed as well:
*
* FRODO_DEBUG=1 npm run test:only SecretStoreOps
*
* Note: FRODO_DEBUG=1 is optional and enables debug logging for some output
* in case things don't function as expected
*/
import { autoSetupPolly, setDefaultState } from "../../utils/AutoSetupPolly";
import { filterRecording } from "../../utils/PollyUtils";
import * as SecretStoreOps from "./SecretStoreOps";
import { state } from "../../lib/FrodoLib";
import Constants from "../../shared/Constants";

const ctx = autoSetupPolly();

describe('SecretStoreOps', () => {
beforeEach(async () => {
if (process.env.FRODO_POLLY_MODE === 'record') {
ctx.polly.server.any().on('beforePersist', (_req, recording) => {
filterRecording(recording);
});
}
setDefaultState(Constants.CLASSIC_DEPLOYMENT_TYPE_KEY);
});

describe('createSecretStoreExportTemplate()', () => {
test('0: Method is implemented', async () => {
expect(SecretStoreOps.createSecretStoreExportTemplate).toBeDefined();
});

test('1: Create SecretStore Export Template', async () => {
const response = SecretStoreOps.createSecretStoreExportTemplate({ state });
expect(response).toMatchSnapshot({
meta: expect.any(Object),
});
});
});

describe('readSecretStore()', () => {
test('0: Method is implemented', async () => {
expect(SecretStoreOps.readSecretStore).toBeDefined();
});
//TODO: create tests
});

describe('readSecretStores()', () => {
test('0: Method is implemented', async () => {
expect(SecretStoreOps.readSecretStores).toBeDefined();
});

test('1: Read realm SecretStores', async () => {
const response = await SecretStoreOps.readSecretStores({ globalConfig: false, state });
expect(response).toMatchSnapshot();
});

test('2: Read global SecretStores', async () => {
const response = await SecretStoreOps.readSecretStores({ globalConfig: true, state });
expect(response).toMatchSnapshot();
});
});

describe('exportSecretStore()', () => {
test('0: Method is implemented', async () => {
expect(SecretStoreOps.exportSecretStore).toBeDefined();
});
//TODO: create tests
});

describe('exportSecretStores()', () => {
test('0: Method is implemented', async () => {
expect(SecretStoreOps.exportSecretStores).toBeDefined();
});

test('1: Export realm SecretStores', async () => {
const response = await SecretStoreOps.exportSecretStores({ globalConfig: false, state });
expect(response).toMatchSnapshot({
meta: expect.any(Object),
});
});

test('2: Export global SecretStores', async () => {
const response = await SecretStoreOps.exportSecretStores({ globalConfig: true, state });
expect(response).toMatchSnapshot({
meta: expect.any(Object),
});
});
});

describe('updateSecretStore()', () => {
test('0: Method is implemented', async () => {
expect(SecretStoreOps.updateSecretStore).toBeDefined();
});
//TODO: create tests
});

describe('updateSecretStoreMapping()', () => {
test('0: Method is implemented', async () => {
expect(SecretStoreOps.updateSecretStoreMapping).toBeDefined();
});
//TODO: create tests
});

describe('importSecretStores()', () => {
test('0: Method is implemented', async () => {
expect(SecretStoreOps.importSecretStores).toBeDefined();
});
//TODO: create tests
});

});
Loading

0 comments on commit f9c4a17

Please sign in to comment.