Migrate from ember-cli-mirage to miragejs (#3088)

* chore: 🤖 import explicit mirage references

* chore: 🤖 add mirage setupMirage helper

* chore: 🤖 working example with application adapter test

* chore: 🤖 use internal setupMirage test helper from api addon

* chore: 🤖 conditionally load mirage

* chore: 🤖 filter mirage folder inclusion in production builds

* chore: 🤖 drop ember-mirage and use our own test helpers

* chore: 🤖 collapse reference to import declaration

* docs: ✏️ add comment better explaining the mirage test env

* chore: 🤖 update configuration and manage mirage-related deps

* fix: 🐛 restore ipc mock and add missing checkOS handler

* chore: 🤖 add comment explaining omitting mirage deps in build

* chore: 🤖 allow mirage.enabled to include deps and handlers

* chore: 🤖 re-enable mirage for core dummy app

* Add missing copyright headers

* chore: 🤖 use embroider macros to conditionally start mirage

* docs: ✏️ update api readme for mirage changes

* chore: 🤖 replace remaining ember-cli-mirage references

* chore: 🤖 add mirage to test builds

* fix: 🐛 ensure that auto mirage start doesn't happen in test env

* chore: 🤖 use const instead of var

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>

* refactor: 💡 use cleaner logic for excluded api build modules

* fix: 🐛 handle issue where importSync was too eager

This was fixed by bumping the @embroider/macros dependency that includes
a fix to this issue:
https://github.com/embroider-build/embroider/pull/2511

* refactor: 💡 move mirage from addon to app directory

* refactor: 💡 update mirage test helper to be a factory

* chore: 🤖 exclude mirage directory when necessary at build time

* chore: 🤖 add miragejs as admin dev dependency

* refactor: 💡 update reference to startMirage

* chore: 🤖 update local mirage test helper

* chore: 🤖 change import path for mirage on desktop

* chore: 🤖 update imports in tests for startMirage

* chore: 🤖 update startMirage imports for api addon tests

* chore: 🤖 add miragejs dev dep to desktop

* docs: ✏️ update README

* chore: 🤖 set up setupMirage for core tests

* chore: 🤖 fix copyright header

* Add missing copyright headers

---------

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
This commit is contained in:
hashiccandZhihe Li authored and GitHub committed 2026-02-03 13:54:32 -05:00
1 parent 72fa6d86c4
commit 63b810d53f
191 files changed
+500 -215

No files matched your search

+988
View File
@@ -0,0 +1,988 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { createServer, Response } from 'miragejs';
import { authHandler, deauthHandler } from './route-handlers/auth';
import { targetHandler } from './route-handlers/target';
import { pickRandomStatusString } from './factories/session';
import initializeMockIPC from './scenarios/ipc';
import makeBooleanFilter from './helpers/bexpr-filter';
import { faker } from '@faker-js/faker';
import { asciicasts } from './data/asciicasts';
import { TYPE_WORKER_PKI } from 'api/models/worker';
import environmentConfig from 'ember-get-config';
// mirage models (alphabetical)
import accountModel from './models/account';
import aliasModel from './models/alias';
import authMethodModel from './models/auth-method';
import baseModel from './models/base';
import channelRecordingModel from './models/channel-recording';
import connectionRecordingModel from './models/connection-recording';
import credentialLibraryModel from './models/credential-library';
import credentialStoreModel from './models/credential-store';
import credentialModel from './models/credential';
import groupModel from './models/group';
import hostCatalogModel from './models/host-catalog';
import hostSetModel from './models/host-set';
import hostModel from './models/host';
import managedGroupModel from './models/managed-group';
import policyModel from './models/policy';
import roleModel from './models/role';
import scopeModel from './models/scope';
import sessionRecordingModel from './models/session-recording';
import sessionModel from './models/session';
import storageBucketModel from './models/storage-bucket';
import targetModel from './models/target';
import userModel from './models/user';
import workerModel from './models/worker';
// mirage serializers (alphabetical)
import accountSerializer from './serializers/account';
import aliasSerializer from './serializers/alias';
import applicationSerializer from './serializers/application';
import authMethodSerializer from './serializers/auth-method';
import channelRecordingSerializer from './serializers/channel-recording';
import connectionRecordingSerializer from './serializers/connection-recording';
import credentialLibrarySerializer from './serializers/credential-library';
import credentialStoreSerializer from './serializers/credential-store';
import credentialSerializer from './serializers/credential';
import groupSerializer from './serializers/group';
import hostCatalogSerializer from './serializers/host-catalog';
import hostSetSerializer from './serializers/host-set';
import hostSerializer from './serializers/host';
import managedGroupSerializer from './serializers/managed-group';
import policySerializer from './serializers/policy';
import roleSerializer from './serializers/role';
import scopeSerializer from './serializers/scope';
import sessionRecordingSerializer from './serializers/session-recording';
import sessionSerializer from './serializers/session';
import storageBucketSerializer from './serializers/storage-bucket';
import targetSerializer from './serializers/target';
import userSerializer from './serializers/user';
import workerSerializer from './serializers/worker';
// mirage scenarios (alphabetical)
import defaultScenario from './scenarios/default';
import ipcScenario from './scenarios/ipc';
// mirage factories (alphabetical)
import accountFactory from './factories/account';
import aliasFactory from './factories/alias';
import authMethodFactory from './factories/auth-method';
import channelRecordingFactory from './factories/channel-recording';
import connectionRecordingFactory from './factories/connection-recording';
import credentialLibraryFactory from './factories/credential-library';
import credentialStoreFactory from './factories/credential-store';
import credentialFactory from './factories/credential';
import groupFactory from './factories/group';
import hostCatalogFactory from './factories/host-catalog';
import hostSetFactory from './factories/host-set';
import hostFactory from './factories/host';
import managedGroupFactory from './factories/managed-group';
import policyFactory from './factories/policy';
import roleFactory from './factories/role';
import scopeFactory from './factories/scope';
import sessionRecordingFactory from './factories/session-recording';
import storageBucketFactory from './factories/storage-bucket';
import sessionFactory from './factories/session';
import targetFactory from './factories/target';
import userFactory from './factories/user';
import workerFactory from './factories/worker';
const isTesting = environmentConfig.environment === 'test';
// Main function
// More info about server configuration:
// https://github.com/miragejs/miragejs/blob/7ff4f3f6fe56bf0cb1648f5af3f5210fcb07e20b/types/index.d.ts#L375-L404
export default function (mirageConfig) {
let finalConfig = {
...mirageConfig,
scenarios: {
default: defaultScenario,
ipcScenario: ipcScenario,
},
factories: {
account: accountFactory,
alias: aliasFactory,
authMethod: authMethodFactory,
channelRecording: channelRecordingFactory,
connectionRecording: connectionRecordingFactory,
credentialLibrary: credentialLibraryFactory,
credentialStore: credentialStoreFactory,
credential: credentialFactory,
group: groupFactory,
hostCatalog: hostCatalogFactory,
hostSet: hostSetFactory,
host: hostFactory,
managedGroup: managedGroupFactory,
policy: policyFactory,
role: roleFactory,
scope: scopeFactory,
sessionRecording: sessionRecordingFactory,
session: sessionFactory,
storageBucket: storageBucketFactory,
target: targetFactory,
user: userFactory,
worker: workerFactory,
},
models: {
account: accountModel,
alias: aliasModel,
authMethod: authMethodModel,
base: baseModel,
channelRecording: channelRecordingModel,
connectionRecording: connectionRecordingModel,
credentialLibrary: credentialLibraryModel,
credentialStore: credentialStoreModel,
credential: credentialModel,
group: groupModel,
hostCatalog: hostCatalogModel,
hostSet: hostSetModel,
host: hostModel,
managedGroup: managedGroupModel,
policy: policyModel,
role: roleModel,
scope: scopeModel,
sessionRecording: sessionRecordingModel,
session: sessionModel,
storageBucket: storageBucketModel,
target: targetModel,
user: userModel,
worker: workerModel,
},
serializers: {
account: accountSerializer,
alias: aliasSerializer,
application: applicationSerializer,
authMethod: authMethodSerializer,
channelRecording: channelRecordingSerializer,
connectionRecording: connectionRecordingSerializer,
credentialLibrary: credentialLibrarySerializer,
credentialStore: credentialStoreSerializer,
credential: credentialSerializer,
group: groupSerializer,
hostCatalog: hostCatalogSerializer,
hostSet: hostSetSerializer,
host: hostSerializer,
managedGroup: managedGroupSerializer,
policy: policySerializer,
role: roleSerializer,
scope: scopeSerializer,
sessionRecording: sessionRecordingSerializer,
session: sessionSerializer,
storageBucket: storageBucketSerializer,
target: targetSerializer,
user: userSerializer,
worker: workerSerializer,
},
routes,
};
return createServer(finalConfig);
}
// Only routes are defined here
function routes() {
initializeMockIPC(this, environmentConfig);
this.passthrough();
// make this `http://localhost:8080`, for example, if your API is on a different server
// this.urlPrefix = '';
this.get('/metadata.json', () => {
// this configuration is optional
const licensedFeatures = environmentConfig?.features?.licensedFeatures;
const edition = licensedFeatures ? 'enterprise' : 'oss';
const features = licensedFeatures ? Object.keys(licensedFeatures) : [];
const metadata = {
license: {
edition,
features,
},
};
return metadata;
});
// make this `/api`, for example, if your API is namespaced
this.namespace = environmentConfig.api?.namespace;
// delay for each request, automatically set to 0 during testing
this.timing = 1;
// Scope resources
this.get(
'/scopes',
({ scopes }, { queryParams: { scope_id, recursive, filter } }) => {
let resultSet;
// Default parent scope is global
if (!scope_id) scope_id = 'global';
// We do not return global scope to mimic our API.
if (recursive && scope_id === 'global') {
resultSet = scopes.where((scope) => {
return scope.scope ? scope.type !== 'global' : false;
});
} else {
resultSet = scopes.where((scope) => {
return scope.scope
? scope.scope.id === scope_id && scope.type !== 'global'
: false;
});
}
const filteredResultSet = resultSet.filter(makeBooleanFilter(filter));
// add a response_type to the response to simulate pagination
// without having response_type, the unsupported controller modal
// will show when running with mirage
return { items: filteredResultSet.models, response_type: 'complete' };
},
);
this.post('/scopes', function ({ scopes }) {
// Parent scope comes through the payload via `scope_id`, but this needs
// to be expanded to a scope object `scope: {id:..., type:...}` before
// saving, which is the gist of this function.
const attrs = this.normalizedRequestAttrs();
const parentScopeAttrs = this.serialize(scopes.find(attrs.scopeId));
attrs.scope = parentScopeAttrs;
return scopes.create(attrs);
});
this.get('/scopes/:id');
this.patch('/scopes/:id');
this.del('/scopes/:id');
this.post(
'/scopes/:idMethod',
function ({ scopes }, { params: { idMethod } }) {
const attrs = this.normalizedRequestAttrs();
const id = idMethod.split(':')[0];
const method = idMethod.split(':')[1];
const scope = scopes.find(id);
const updatedAttrs = {
version: attrs.version,
storagePolicyId: attrs.storagePolicyId,
};
if (method === 'attach-storage-policy') {
updatedAttrs.storagePolicyId = attrs.storagePolicyId;
}
if (method === 'detach-storage-policy') {
updatedAttrs.storagePolicyId = '';
}
return scope.update(updatedAttrs);
},
);
// Auth & IAM resources
this.get('/auth-methods');
this.post('/auth-methods', function ({ authMethods }) {
const attrs = this.normalizedRequestAttrs();
if (attrs.type === 'oidc') {
attrs.attributes.state = 'active-public';
}
return authMethods.create(attrs);
});
this.get('/auth-methods/:id');
this.patch(
'/auth-methods/:id',
function ({ authMethods }, { params: { id } }) {
const attrs = this.normalizedRequestAttrs();
if (attrs.type === 'oidc' || attrs.type === 'ldap') {
attrs.attributes.state = 'active-public';
}
return authMethods.find(id).update(attrs);
},
);
this.del('/auth-methods/:id', ({ authMethods }, { params: { id } }) => {
const authMethod = authMethods.find(id);
const scope = authMethod.scope;
authMethod.destroy();
// If the primary auth method is deleted, the associated scope's
// primary_auth_method_id field should be set to null.
if (scope.primaryAuthMethodId === id) {
scope.update({ primaryAuthMethodId: null });
}
return new Response(202);
});
// Auth Method Accounts
this.get(
'/accounts',
(
{ accounts },
{ queryParams: { auth_method_id: authMethodId, filter } },
) => {
let resultSet;
if (authMethodId) {
resultSet = accounts.where({ authMethodId });
} else {
resultSet = accounts.all();
}
return resultSet.filter(makeBooleanFilter(filter));
},
);
this.post('/accounts');
this.get('/accounts/:id');
this.patch('/accounts/:id');
this.del('/accounts/:id');
this.post(
'/accounts/:idMethod',
function ({ accounts }, { params: { idMethod } }) {
const attrs = this.normalizedRequestAttrs();
const id = idMethod.split(':')[0];
const method = idMethod.split(':')[1];
const account = accounts.find(id);
const updatedAttrs = {
version: attrs.version,
attributes: account.attributes,
};
// Set new password
if (method === 'set-password') {
updatedAttrs.attributes.password = attrs.password;
}
// Set new password
if (method === 'change-password') {
updatedAttrs.attributes.password = attrs.new_password;
}
return account.update(updatedAttrs);
},
);
// Authenticate + auth methods custom routes
this.post('/auth-methods/:id_method', authHandler);
// Deauthenticate route
this.delete('/auth-tokens/:id', deauthHandler);
// IAM : Users
this.get(
'/users',
({ users }, { queryParams: { scope_id, recursive, filter } }) => {
let resultSet;
if (recursive && scope_id === 'global') {
resultSet = users.all();
} else if (recursive) {
resultSet = users.where((user) => {
const userModel = users.find(user.id);
return (
user.scopeId === scope_id ||
userModel?.scope?.scope?.id === scope_id
);
});
} else {
resultSet = users.where((user) => user.scopeId === scope_id);
}
return resultSet.filter(makeBooleanFilter(filter));
},
);
this.post('/users');
this.get('/users/:id');
this.patch('/users/:id');
this.del('/users/:id');
this.post('/users/:idMethod', function ({ users }, { params: { idMethod } }) {
const attrs = this.normalizedRequestAttrs();
const id = idMethod.split(':')[0];
const method = idMethod.split(':')[1];
const user = users.find(id);
const updatedAttrs = {
version: attrs.version,
accountIds: user.accountIds,
};
// If adding accounts, push them into the array
if (method === 'add-accounts') {
attrs.accountIds.forEach((id) => {
if (!updatedAttrs.accountIds.includes(id)) {
updatedAttrs.accountIds.push(id);
}
});
}
// If deleting accounts, filter them out of the array
if (method === 'remove-accounts') {
updatedAttrs.accountIds = updatedAttrs.accountIds.filter((id) => {
return !attrs.accountIds.includes(id);
});
}
return user.update(updatedAttrs);
});
// IAM: Groups
this.get(
'/groups',
({ groups }, { queryParams: { scope_id, recursive, filter } }) => {
let resultSet;
if (recursive && scope_id === 'global') {
resultSet = groups.all();
} else if (recursive) {
resultSet = groups.where((group) => {
const groupModel = groups.find(group.id);
return (
group.scopeId === scope_id ||
groupModel?.scope?.scope?.id === scope_id
);
});
} else {
resultSet = groups.where((group) => group.scopeId === scope_id);
}
return resultSet.filter(makeBooleanFilter(filter));
},
);
this.post('/groups');
this.get('/groups/:id');
this.patch('/groups/:id');
this.del('/groups/:id');
this.post(
'/groups/:idMethod',
function ({ groups }, { params: { idMethod } }) {
const attrs = this.normalizedRequestAttrs();
const id = idMethod.split(':')[0];
const method = idMethod.split(':')[1];
const group = groups.find(id);
const updatedAttrs = {
version: attrs.version,
memberIds: group.memberIds,
};
// If adding members, push them into the array
if (method === 'add-members') {
attrs.memberIds.forEach((id) => {
if (!updatedAttrs.memberIds.includes(id)) {
updatedAttrs.memberIds.push(id);
}
});
}
// If deleting members, filter them out of the array
if (method === 'remove-members') {
updatedAttrs.memberIds = updatedAttrs.memberIds.filter((id) => {
return !attrs.memberIds.includes(id);
});
}
return group.update(updatedAttrs);
},
);
// IAM: Roles
this.get('/roles');
this.post('/roles');
this.get('/roles/:id');
this.patch('/roles/:id');
this.del('/roles/:id');
this.post(
'/roles/:idMethod',
function (
{ roles, users, groups, managedGroups },
{ params: { idMethod } },
) {
const attrs = this.normalizedRequestAttrs();
const id = idMethod.split(':')[0];
const method = idMethod.split(':')[1];
const role = roles.find(id);
let updatedAttrs = {};
if (method === 'set-grant-scopes') {
updatedAttrs = {
version: attrs.version,
grantScopeIds: attrs.grantScopeIds,
};
}
// Principals is a combined list of users, groups and managed groups, but in Mirage we've
// internally modelled them as separate lists. Therefore we must check
// the type by looking up the user or group first, then determine which
// list to add the principal to.
if (method === 'add-principals') {
updatedAttrs = {
version: attrs.version,
userIds: role.userIds,
groupIds: role.groupIds,
managedGroupIds: role.managedGroupIds,
};
attrs.principalIds.forEach((id) => {
const isUser = users.find(id);
const isGroup = groups.find(id);
const isManagedGroup = managedGroups.find(id);
if (isUser && !updatedAttrs.userIds.includes(id)) {
updatedAttrs.userIds.push(id);
}
if (isGroup && !updatedAttrs.groupIds.includes(id)) {
updatedAttrs.groupIds.push(id);
}
if (isManagedGroup && !updatedAttrs.managedGroupIds.includes(id)) {
updatedAttrs.managedGroupIds.push(id);
}
});
}
// If deleting principals, filter the ids out of users, groups and managed group lists,
// and in this case don't care about the type
if (method === 'remove-principals') {
updatedAttrs = {
version: attrs.version,
userIds: role.userIds,
groupIds: role.groupIds,
managedGroupIds: role.managedGroupIds,
};
updatedAttrs.userIds = updatedAttrs.userIds.filter((id) => {
return !attrs.principalIds.includes(id);
});
updatedAttrs.groupIds = updatedAttrs.groupIds.filter((id) => {
return !attrs.principalIds.includes(id);
});
updatedAttrs.managedGroupIds = updatedAttrs.managedGroupIds.filter(
(id) => {
return !attrs.principalIds.includes(id);
},
);
}
if (method === 'set-grants') {
updatedAttrs = {
version: attrs.version,
grantStrings: attrs.grantStrings,
};
}
return role.update(updatedAttrs);
},
);
// Other resources
// host-catalog
this.get('/host-catalogs');
this.post(
'/host-catalogs',
function ({ hostCatalogs }, { queryParams: { plugin_name } }) {
const attrs = this.normalizedRequestAttrs();
if (plugin_name) {
attrs.type = 'plugin';
attrs.plugin = {
name: plugin_name,
};
}
return hostCatalogs.create(attrs);
},
);
this.get('/host-catalogs/:id');
this.patch('/host-catalogs/:id');
this.del('/host-catalogs/:id');
// host
this.get(
'/hosts',
function ({ hosts }, { queryParams: { host_catalog_id: hostCatalogId } }) {
return hosts.where({ hostCatalogId });
},
);
this.post('/hosts', function ({ hostCatalogs, hosts }) {
const attrs = this.normalizedRequestAttrs();
const hostCatalog = hostCatalogs.find(attrs.hostCatalogId);
attrs.scopeId = hostCatalog.scope.id;
return hosts.create(attrs);
});
this.get('/hosts/:id');
this.patch('/hosts/:id');
this.del('/hosts/:id');
// host-set
this.get(
'/host-sets',
function (
{ hostSets },
{ queryParams: { host_catalog_id: hostCatalogId } },
) {
return hostSets.where({ hostCatalogId });
},
);
this.post('/host-sets', function ({ hostCatalogs, hostSets }) {
const attrs = this.normalizedRequestAttrs();
const hostCatalog = hostCatalogs.find(attrs.hostCatalogId);
//this needs to be attached while creating a plugin type host set to mimic API
if (hostCatalog.plugin) {
attrs.type = 'plugin';
attrs.plugin = {
name: hostCatalog.plugin.name,
};
}
attrs.scopeId = hostCatalog.scope.id;
return hostSets.create(attrs);
});
this.get('/host-sets/:id');
this.patch('/host-sets/:id');
this.del('/host-sets/:id');
this.post(
'/host-sets/:idMethod',
function ({ hostSets }, { params: { idMethod } }) {
const attrs = this.normalizedRequestAttrs();
const id = idMethod.split(':')[0];
const method = idMethod.split(':')[1];
const hostSet = hostSets.find(id);
const updatedAttrs = {
version: attrs.version,
hostIds: hostSet.hostIds,
};
// If adding hosts, push them into the array
if (method === 'add-hosts') {
attrs.hostIds.forEach((id) => {
if (!updatedAttrs.hostIds.includes(id)) {
updatedAttrs.hostIds.push(id);
}
});
}
// If deleting hosts, filter them out of the array
if (method === 'remove-hosts') {
updatedAttrs.hostIds = updatedAttrs.hostIds.filter((id) => {
return !attrs.hostIds.includes(id);
});
}
return hostSet.update(updatedAttrs);
},
);
// target
this.get(
'/targets',
function ({ targets }, { queryParams: { scope_id, recursive, filter } }) {
let resultSet;
if (recursive && scope_id === 'global') {
resultSet = targets.all();
} else if (recursive) {
resultSet = targets.where((target) => {
const targetModel = targets.find(target.id);
return (
target.scopeId === scope_id ||
targetModel?.scope?.scope?.id === scope_id
);
});
} else {
resultSet = targets.where((target) => target.scopeId === scope_id);
}
return resultSet.filter(makeBooleanFilter(filter));
},
);
this.post('/targets', function ({ targets }) {
const attrs = this.normalizedRequestAttrs();
return targets.create(attrs);
});
this.get('/targets/:id');
this.patch('/targets/:id');
this.del('/targets/:id');
this.post('/targets/:idMethod', targetHandler);
// session
this.get(
'/sessions',
function ({ sessions }, { queryParams: { scope_id, recursive, filter } }) {
let resultSet;
// To simulate changes to `session.status` that may occur in the backend,
// we quietly randomize the value of the field on GET.
// To populate sessions for logged in user,
// update alternate sessions to auth user.
// But only if not in testing mode.
// In tests, we need deterministic statuses.
if (!isTesting) {
sessions.all().models.forEach((session) => {
session.update({
status: pickRandomStatusString(),
});
if (session.id.split('-').pop() % 2) {
session.update({
userId: 'authenticateduser',
});
}
});
}
if (recursive && scope_id === 'global') {
resultSet = sessions.all();
} else if (recursive) {
resultSet = sessions.where((session) => {
const sessionModel = sessions.find(session.id);
return (
session.scopeId === scope_id ||
sessionModel?.scope?.scope?.id === scope_id
);
});
} else {
resultSet = sessions.where((session) => session.scopeId === scope_id);
}
return resultSet.filter(makeBooleanFilter(filter));
},
);
this.get('/sessions/:id', function ({ sessions }, { params: { id } }) {
const session = sessions.find(id);
if (!isTesting) {
session.update({
status: pickRandomStatusString(),
});
}
return session;
});
this.post(
'/sessions/:idMethod',
function ({ sessions }, { params: { idMethod } }) {
const attrs = this.normalizedRequestAttrs();
const id = idMethod.split(':')[0];
const method = idMethod.split(':')[1];
const session = sessions.find(id);
const updatedAttrs = {
version: attrs.version,
};
if (method === 'cancel') {
updatedAttrs.status = 'canceling';
}
return session.update(updatedAttrs);
},
);
// auth-tokens
this.get('/auth-tokens/:id', function () {
// Set to 401 or 404 to simulate token invalidation, which will
// cause the session to fail restoration and logout the user.
return new Response(200);
});
// credential-stores
this.get('/credential-stores');
this.get('/credential-stores/:id');
this.post('/credential-stores');
this.patch('/credential-stores/:id');
this.del('/credential-stores/:id');
// credential-libraries
this.get(
'/credential-libraries',
(
{ credentialLibraries },
{ queryParams: { credential_store_id: credentialStoreId } },
) => credentialLibraries.where({ credentialStoreId }),
);
this.get('/credential-libraries/:id');
this.post(
'/credential-libraries',
function ({ credentialStores, credentialLibraries }) {
const attrs = this.normalizedRequestAttrs();
const credentialStore = credentialStores.find(attrs.credentialStoreId);
attrs.scopeId = credentialStore.scope.id;
return credentialLibraries.create(attrs);
},
);
this.del('/credential-libraries/:id');
this.patch('/credential-libraries/:id');
// credentials
this.get(
'/credentials',
(
{ credentials },
{ queryParams: { credential_store_id: credentialStoreId } },
) => credentials.where({ credentialStoreId }),
);
this.get('/credentials/:id');
this.post('/credentials', function ({ credentialStores, credentials }) {
const attrs = this.normalizedRequestAttrs();
const credentialStore = credentialStores.find(attrs.credentialStoreId);
attrs.scopeId = credentialStore.scope.id;
return credentials.create(attrs);
});
this.del('/credentials/:id');
this.patch('/credentials/:id');
// managed-groups
this.get(
'/managed-groups',
(
{ managedGroups },
{ queryParams: { auth_method_id: authMethodId, filter } },
) => {
let resultSet;
if (authMethodId) {
resultSet = managedGroups.where({ authMethodId });
} else {
resultSet = managedGroups.all();
}
return resultSet.filter(makeBooleanFilter(filter));
},
);
this.post('/managed-groups');
this.get('/managed-groups/:id');
this.patch('/managed-groups/:id');
this.del('/managed-groups/:id');
// workers
this.get(
'/workers',
({ workers }, { queryParams: { scope_id: scopeId } }) => {
return workers.where({ scopeId });
},
);
this.get('/workers/:id');
this.del('/workers/:id');
this.patch('/workers/:id');
this.post('/workers:create:worker-led', ({ workers, scopes }) => {
const globalScope = scopes.find('global');
// This POST only takes in a token so we need to generate a random worker to return
const newWorker = this.create('worker', {
type: TYPE_WORKER_PKI,
scope: globalScope,
});
return workers.create(newWorker.attrs);
});
this.post(
'/workers/:idMethod',
function ({ workers }, { params: { idMethod } }) {
const attrs = this.normalizedRequestAttrs();
const id = idMethod.split(':')[0];
const method = idMethod.split(':')[1];
const worker = workers.find(id);
let updatedAttrs = {
version: attrs.version,
api_tags: worker.api_tags,
};
if (method === 'remove-worker-tags') {
const key = Object.keys(attrs.apiTags)[0];
const value = attrs.apiTags[key][0];
updatedAttrs.api_tags[key] = updatedAttrs.api_tags[key].filter(
(tag) => {
return tag !== value;
},
);
}
if (method === 'set-worker-tags') {
updatedAttrs.api_tags = attrs.apiTags;
}
return worker.update(updatedAttrs);
},
);
// storage-buckets
this.get(
'/storage-buckets',
({ storageBuckets }, { queryParams: { scope_id: scopeId, recursive } }) => {
if (recursive && scopeId === 'global') {
return storageBuckets.all();
}
return storageBuckets.where({ scopeId });
},
);
this.get('/storage-buckets/:id');
this.del('/storage-buckets/:id');
this.patch('/storage-buckets/:id');
this.post(
'/storage-buckets',
function ({ storageBuckets }, { queryParams: { plugin_name } }) {
const attrs = this.normalizedRequestAttrs();
if (plugin_name) {
attrs.type = 'plugin';
attrs.plugin = {
name: plugin_name,
};
}
// Remove the secrets
delete attrs.secrets;
return storageBuckets.create(attrs);
},
);
//storage-policy
this.get(
'/policies',
({ policies }, { queryParams: { scope_id: scopeId } }) => {
return policies.where({ scopeId });
},
);
this.get('/policies/:id');
this.del('/policies/:id');
this.patch('/policies/:id');
this.post('/policies', function ({ policies }) {
const attrs = this.normalizedRequestAttrs();
return policies.create(attrs);
});
// Alias
this.get(
'/aliases',
({ aliases }, { queryParams: { scope_id: scopeId } }) => {
return aliases.where({ scopeId });
},
);
this.get('/aliases/:id');
this.del('/aliases/:id');
this.patch('/aliases/:id');
this.post('/aliases', function ({ aliases }) {
const attrs = this.normalizedRequestAttrs();
return aliases.create(attrs);
});
// session recordings
this.get('/session-recordings');
this.get(
'/session-recordings/:idMethod',
async ({ sessionRecordings }, { params: { idMethod } }) => {
const id = idMethod.split(':')[0];
const method = idMethod.split(':')[1];
if (method === 'download') {
return faker.helpers.arrayElement(asciicasts);
} else {
return sessionRecordings.find(id);
}
},
);
this.post(
'/session-recordings/:idMethod',
function ({ sessionRecordings }, { params: { idMethod } }) {
const attrs = this.normalizedRequestAttrs();
const id = idMethod.split(':')[0];
const method = idMethod.split(':')[1];
const record = sessionRecordings.find(id);
const updatedAttrs = {
version: attrs.version,
};
if (method === 'reapply-storage-policy') {
updatedAttrs.retain_until = faker.date.recent();
updatedAttrs.delete_after = faker.date.recent();
}
return record.update(updatedAttrs);
},
);
this.del('/session-recordings/:id');
/* Uncomment the following line and the Response import above
* Then change the response code to simulate error responses.
* this.get('/scopes', () => new Response(505));
*
* Update error payload to simulate specific error responses.
*/
// this.get('/host-catalogs', () => new Response(404));
// this.get('/scopes/:id', () => new Response(505, {}, {
// errors: [{
// status: 505,
// message: 'HTTP version not supported.',
// }]
// }));
}
+156
View File
@@ -0,0 +1,156 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
export const asciicast1 = `{"version": 2, "width": 124, "height": 36, "timestamp": 1670352594, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}}
[2.138652, "o", "\\u001b[1m\\u001b[7m%\\u001b[27m\\u001b[1m\\u001b[0m \\r \\r\\u001b]2;ember-asciinema-player\\u0007\\u001b]1;..iinema-player\\u0007"]
[2.220877, "o", "\\r\\u001b[0m\\u001b[27m\\u001b[24m\\u001b[J\\u001b[33member-asciinema-player\\u001b[00m \\u001b[32mgit\\u001b[00m:\\u001b[36m(main*)\\u001b[00m $ \\u001b[K"]
[2.221019, "o", "\\u001b[?1h\\u001b="]
[2.221076, "o", "\\u001b[?2004h"]
[2.676444, "o", "y"]
[2.788653, "o", "\\bya"]
[2.868652, "o", "r"]
[3.001764, "o", "n"]
[3.167636, "o", " "]
[3.313668, "o", "t"]
[3.460527, "o", "e"]
[3.561983, "o", "s"]
[3.63678, "o", "t"]
[3.853437, "o", "\\u001b[?1l\\u001b>"]
[3.853473, "o", "\\u001b[?2004l\\r\\r\\n"]
[3.854604, "o", "\\u001b]2;yarn test\\u0007\\u001b]1;yarn\\u0007"]
[4.489178, "o", "\\u001b[2K"]
[4.48982, "o", "\\u001b[1G\\u001b[1myarn run v1.22.19\\u001b[22m\\r\\n"]
[4.555618, "o", "\\u001b[2K"]
[4.555649, "o", "\\u001b[1G\\u001b[2m$ npm-run-all lint\\u001b[22m\\r\\n"]
[5.345253, "o", "\\u001b[2K"]
[5.345509, "o", "\\u001b[1G\\u001b[2m$ npm-run-all --print-name --aggregate-output --continue-on-error --parallel \\"lint:!(fix)\\"\\u001b[22m\\r\\n"]
[7.225926, "o", "\\r\\n> ember-asciinema-player@0.0.0 lint:js ember-asciinema-player/package.json\\r\\n> eslint . --cache \\r\\n\\r\\n$ eslint . --cache\\r\\n"]
[8.398641, "o", "\\r\\n> ember-asciinema-player@0.0.0 lint:hbs ember-asciinema-player/package.json\\r\\n> ember-template-lint . \\r\\n\\r\\n$ ember-template-lint .\\r\\n\\r\\n"]
[8.418537, "o", "\\u001b[2K\\u001b[1G\\u001b[2m$ ember test\\u001b[22m\\r\\n"]
[12.256156, "o", "\\u001b[?25l"]
[12.256464, "o", "\\u001b[32m⠋\\u001b[39m \\u001b[32mBuilding\\u001b[39m"]
[12.2573, "o", "\\u001b[2K"]
[12.257386, "o", "\\u001b[1G\\u001b[?25h"]
[12.257689, "o", "Environment: test\\r\\n"]
[12.257857, "o", "\\u001b[?25l\\u001b[32m⠋\\u001b[39m \\u001b[32mBuilding\\u001b[39m"]
[12.539035, "o", "\\u001b[2K"]
[12.539144, "o", "\\u001b[1G"]
[12.539219, "o", "\\u001b[32m⠙\\u001b[39m \\u001b[32mbuilding... \\u001b[39m"]
[12.745202, "o", "\\u001b[2K\\u001b[1G"]
[12.745343, "o", "\\u001b[32m⠹\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[Babel: @ember/test-helpers > applyPatches]"]
[12.825004, "o", "\\u001b[2K"]
[12.825031, "o", "\\u001b[1G"]
[12.825122, "o", "\\u001b[32m⠸\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[Babel: ember-qunit > applyPatches]"]
[12.933588, "o", "\\u001b[2K\\u001b[1G"]
[12.933624, "o", "\\u001b[32m⠼\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[Babel: ember-load-initializers > applyPatches]"]
[13.194738, "o", "\\u001b[2K\\u001b[1G"]
[13.195009, "o", "\\u001b[32m⠴\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[Babel: ember-asciinema-player > applyPatches]"]
[13.307878, "o", "\\u001b[2K\\u001b[1G\\u001b[32m⠦\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[Package /assets/vendor.js]"]
[13.750717, "o", "\\u001b[2K\\u001b[1G"]
[13.750755, "o", "\\u001b[32m⠧\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[Concat: Test Support JS]"]
[13.869411, "o", "\\u001b[2K\\u001b[1G"]
[13.869438, "o", "\\u001b[32m⠇\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[ember-auto-import-webpack]"]
[14.655374, "o", "\\u001b[2K\\u001b[1G"]
[14.655415, "o", "\\u001b[32m⠏\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[ember-auto-import-webpack]"]
[14.73686, "o", "\\u001b[2K\\u001b[1G\\u001b[?25h"]
[14.738533, "o", "\\u001b[?25h"]
[14.738818, "o", "\\u001b[?25l\\u001b[32m⠋\\u001b[39m cleaning up"]
[14.739061, "o", "\\u001b[2K\\u001b[1G\\u001b[?25hcleaning up...\\r\\n"]
[14.739197, "o", "\\u001b[?25l\\u001b[32m⠋\\u001b[39m cleaning up"]
[14.907985, "o", "\\u001b[2K"]
[14.90808, "o", "\\u001b[1G\\u001b[?25h"]
[14.908128, "o", "\\u001b[32mBuilt project successfully.\\u001b[39m\\r\\n"]
[16.224632, "o", "ok 1 Chrome 108.0 - [2 ms] - ember-qunit: Ember.onerror validation: Ember.onerror is functioning properly\\r\\n"]
[16.346568, "o", "\\r\\n1..1\\r\\n# tests 1\\r\\n# pass 1\\r\\n# skip 0\\r\\n# todo 0\\r\\n# fail 0\\r\\n\\r\\n# ok\\r\\n"]
[16.403167, "o", "\\u001b[2K"]
[16.403388, "o", "\\u001b[1G✨ Done in 11.92s.\\r\\n"]
[16.413967, "o", "\\u001b[1m\\u001b[7m%\\u001b[27m\\u001b[1m\\u001b[0m \\r \\r"]
[16.414147, "o", "\\u001b]2;ember-asciinema-player\\u0007\\u001b]1;..iinema-player\\u0007"]
[16.528341, "o", "\\r\\u001b[0m\\u001b[27m\\u001b[24m\\u001b[J\\u001b[33member-asciinema-player\\u001b[00m \\u001b[32mgit\\u001b[00m:\\u001b[36m(main*)\\u001b[00m $ \\u001b[K"]
[16.528501, "o", "\\u001b[?1h\\u001b="]
[16.52863, "o", "\\u001b[?2004h"]
[19.078794, "o", "\\u001b[?2004l\\r\\r\\n"]`;
export const asciicast2 = `{"version": 2, "width": 112, "height": 30, "timestamp": 1672793720, "env": {"SHELL": "/bin/bash", "TERM": "xterm-256color"}}
[0.036928, "o", "\\u001b[?2004h\\u001b[1;32m[\\u001b]0;johan@nixos: ~/src/hashicorp/boundary-enterprise\\u0007johan@nixos:~/src/hashicorp/boundary-enterprise]$\\u001b[0m "]
[2.827983, "o", "b"]
[2.9386, "o", "o"]
[2.97036, "o", "u"]
[3.032542, "o", "n"]
[3.108687, "o", "d"]
[3.246045, "o", "a"]
[3.340117, "o", "r"]
[3.841216, "o", "y"]
[4.314958, "o", " "]
[6.052102, "o", "u"]
[6.146486, "o", "s"]
[6.207341, "o", "e"]
[6.273087, "o", "r"]
[6.378873, "o", "s"]
[6.799687, "o", " "]
[7.08347, "o", "l"]
[7.159196, "o", "i"]
[7.249656, "o", "s"]
[7.352251, "o", "t"]
[7.450038, "o", " "]
[7.688195, "o", "-"]
[7.931818, "o", "r"]
[8.001542, "o", "e"]
[8.387771, "o", "c"]
[8.526339, "o", "u"]
[8.631438, "o", "r"]
[8.81899, "o", "s"]
[8.958184, "o", "i"]
[9.085688, "o", "v"]
[9.134885, "o", "e"]
[9.786868, "o", "\\r\\n"]
[9.788828, "o", "\\u001b[?2004l\\r"]
[10.086691, "o", "\\u001b[0m\\r\\nUser information:\\r\\n ID: u_anon\\r\\n Scope ID: global\\r\\n Version: 1\\r\\n Name: anonymous\\r\\n Description: The anonymous user matches any request, whether authenticated or not\\r\\n Authorized Actions:\\r\\n no-op\\r\\n read\\r\\n update\\r\\n delete\\r\\n add-accounts\\r\\n set-accounts\\r\\n remove-accounts\\r\\n\\r\\n ID: u_auth\\r\\n Scope ID: global\\r\\n Version: 1\\r\\n Name: authenticated\\r\\n Description: The authenticated user matches any user that has a valid token\\r\\n Authorized Actions:\\r\\n no-op\\r\\n read\\r\\n update\\r\\n delete\\r\\n add-accounts\\r\\n set-accounts\\r\\n remove-accounts\\r\\n\\r\\n ID: u_recovery\\r\\n Scope ID: global\\r\\n Version: 1\\r\\n Name: recovery\\r\\n Description: The recovery user is used for any request that was performed with the recovery KMS\\r\\n workflow\\r\\n Autho"]
[10.086736, "o", "rized Actions:\\r\\n no-op\\r\\n read\\r\\n update\\r\\n delete\\r\\n add-accounts\\r\\n set-accounts\\r\\n remove-accounts\\r\\n\\r\\n ID: u_1234567890\\r\\n Scope ID: global\\r\\n Version: 3\\r\\n Name: admin\\r\\n Description: Initial admin user within the \\"global\\" scope\\r\\n Primary Account ID: acctpw_1234567890\\r\\n Login Name: admin\\r\\n Authorized Actions:\\r\\n no-op\\r\\n read\\r\\n update\\r\\n delete\\r\\n add-accounts\\r\\n set-accounts\\r\\n remove-accounts\\r\\n\\r\\n ID: u_0987654321\\r\\n Scope ID: global\\r\\n Version: 3\\r\\n Name: user\\r\\n Description: Initial unprivileged user\\r\\n Primary Account ID: acctpw_0987654321\\r\\n Login Name: user\\r\\n Authorized Actions:\\r\\n no-op\\r\\n read\\r\\n update\\r\\n delete\\r\\n add-accounts\\r\\n set-accounts\\r\\n remove-accounts\\u001b[0m\\r\\n"]
[10.106453, "o", "\\u001b[?2004h\\u001b[1;32m[\\u001b]0;johan@nixos: ~/src/hashicorp/boundary-enterprise\\u0007johan@nixos:~/src/hashicorp/boundary-enterprise]$\\u001b[0m "]
[12.501496, "o", "b"]
[12.703134, "o", "o"]
[12.752198, "o", "u"]
[12.775329, "o", "n"]
[12.905261, "o", "d"]
[13.002373, "o", "a"]
[13.100111, "o", "r"]
[13.354779, "o", "y"]
[13.699827, "o", " "]
[13.998849, "o", "t"]
[14.139192, "o", "a"]
[14.19663, "o", "r"]
[14.392075, "o", "g"]
[14.489938, "o", "e"]
[14.579533, "o", "t"]
[14.674628, "o", "s"]
[14.830951, "o", " "]
[15.015311, "o", "l"]
[15.077147, "o", "i"]
[15.262221, "o", "s"]
[15.355571, "o", "t"]
[15.456597, "o", " "]
[15.91612, "o", "-"]
[16.264491, "o", "r"]
[16.351595, "o", "e"]
[16.798456, "o", "c"]
[16.891689, "o", "u"]
[16.997975, "o", "r"]
[17.176378, "o", "s"]
[17.290104, "o", "i"]
[17.434691, "o", "v"]
[17.482502, "o", "e"]
[18.393541, "o", "\\r\\n"]
[18.39558, "o", "\\u001b[?2004l\\r"]
[18.673452, "o", "\\u001b[0m\\r\\nTarget information:\\r\\n ID: ttcp_1234567890\\r\\n Scope ID: p_1234567890\\r\\n Version: 2\\r\\n Type: tcp\\r\\n Name: Generated target\\r\\n Description: Provides an initial target in Boundary\\r\\n Authorized Actions:\\r\\n no-op\\r\\n read\\r\\n update\\r\\n delete\\r\\n add-host-sources\\r\\n set-host-sources\\r\\n remove-host-sources\\r\\n add-credential-sources\\r\\n set-credential-sources\\r\\n remove-credential-sources\\r\\n authorize-session\\u001b[0m\\r\\n"]
[18.692528, "o", "\\u001b[?2004h\\u001b[1;32m[\\u001b]0;johan@nixos: ~/src/hashicorp/boundary-enterprise\\u0007johan@nixos:~/src/hashicorp/boundary-enterprise]$\\u001b[0m "]
[20.083966, "o", "e"]
[20.37301, "o", "x"]
[20.51445, "o", "i"]
[20.612885, "o", "t"]
[21.303896, "o", "\\r\\n"]
[21.307902, "o", "\\u001b[?2004l\\rexit\\r\\n"]`;
export const asciicasts = [asciicast1, asciicast2];
@@ -0,0 +1,61 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/account';
import { faker } from '@faker-js/faker';
import permissions from '../helpers/permissions';
import generateId from '../helpers/id';
import {
TYPE_AUTH_METHOD_PASSWORD,
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_LDAP,
TYPES_AUTH_METHOD,
} from 'api/models/auth-method';
export default factory.extend({
authorized_actions() {
let authorizedActions = ['no-op', 'read', 'update', 'delete'];
if (this.type === TYPE_AUTH_METHOD_PASSWORD) {
authorizedActions.push('set-password');
}
return permissions.authorizedActionsFor('account') || authorizedActions;
},
id: () => generateId('acct_'),
type: (i) => TYPES_AUTH_METHOD[i % TYPES_AUTH_METHOD.length],
attributes() {
switch (this.type) {
case TYPE_AUTH_METHOD_PASSWORD:
return { login_name: faker.internet.userName() };
case TYPE_AUTH_METHOD_LDAP: {
const randomNum = faker.number.int({ min: 3, max: 5 });
let member_of_groups = [];
const login_name = faker.internet.userName();
let dn = `uid=${login_name}`;
for (let i = 0; i < randomNum; i++) {
member_of_groups.push(faker.word.noun());
dn += `,dc=${faker.internet.domainSuffix()}`;
}
return {
login_name,
dn,
member_of_groups,
email: faker.internet.email(),
full_name: faker.person.fullName(),
};
}
case TYPE_AUTH_METHOD_OIDC:
return {
issuer:
Math.random() > 0.5 ? faker.internet.ip() : faker.internet.ipv6(),
subject: 'sub',
email: faker.internet.email(),
full_name: faker.person.fullName(),
};
}
},
});
+22
View File
@@ -0,0 +1,22 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/alias';
import permissions from '../helpers/permissions';
import generateId from '../helpers/id';
const destinationIDs = ['', generateId('t_')];
export default factory.extend({
id: () => generateId('alt_'),
destination_id: (i) => destinationIDs[i % destinationIDs.length],
authorized_actions: () =>
permissions.authorizedActionsFor('alias') || [
'no-op',
'read',
'update',
'delete',
],
});
@@ -0,0 +1,72 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/auth-method';
import { trait } from 'miragejs';
import permissions from '../helpers/permissions';
import generateId from '../helpers/id';
import {
TYPE_AUTH_METHOD_PASSWORD,
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_LDAP,
TYPES_AUTH_METHOD,
} from 'api/models/auth-method';
export default factory.extend({
authorized_actions: () =>
permissions.authorizedActionsFor('auth-method') || [
'no-op',
'read',
'update',
'delete',
'authenticate',
],
authorized_collection_actions() {
switch (this.type) {
case TYPE_AUTH_METHOD_PASSWORD:
return { accounts: ['create', 'list'] };
case TYPE_AUTH_METHOD_LDAP:
case TYPE_AUTH_METHOD_OIDC:
return {
accounts: ['create', 'list'],
'managed-groups': ['create', 'list'],
};
}
},
id: () => generateId('am_'),
// Cycle through available types
type: (i) => TYPES_AUTH_METHOD[i % TYPES_AUTH_METHOD.length],
/**
* Adds accounts (with associated users) to auth method and managed groups.
*/
withAccountsAndUsersAndManagedGroups: trait({
afterCreate(authMethod, server) {
const { scope, type } = authMethod;
server.createList('user', 5, { scope }).map((user) => {
const { id } = server.create('account', { scope, type, authMethod });
user.update({ accountIds: [id] });
});
if (type === TYPE_AUTH_METHOD_OIDC || type === TYPE_AUTH_METHOD_LDAP) {
server
.createList('managed-group', 2, { scope, authMethod })
.map((managedGroup) => {
const accounts = server.createList('account', 2, {
scope,
type,
authMethod,
});
const accountIds = accounts.map((account) => account.id);
managedGroup.update({ memberIds: accountIds });
});
}
},
}),
});
@@ -0,0 +1,22 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/channel-recording';
import generateId from '../helpers/id';
import { faker } from '@faker-js/faker';
import { MIME_TYPE_ASCIICAST } from 'api/models/channel-recording';
export default factory.extend({
id: () => generateId('chr_'),
start_time(i) {
return i % 3 === 2
? null
: faker.date.recent({ days: 3, refDate: this.end_time });
},
end_time: (i) => (i % 3 === 2 ? null : faker.date.recent()),
duration: (i) =>
i % 3 === 2 ? null : `${faker.number.int({ max: 100000 })}s`,
mime_types: (i) => (i % 3 === 2 ? null : [MIME_TYPE_ASCIICAST]),
});
@@ -0,0 +1,21 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/connection-recording';
import generateId from '../helpers/id';
import { faker } from '@faker-js/faker';
export default factory.extend({
id: () => generateId('cr_'),
connection_id: () => generateId('sc_'),
start_time(i) {
return i % 3 === 2
? null
: faker.date.recent({ days: 3, refDate: this.end_time });
},
end_time: (i) => (i % 3 === 2 ? null : faker.date.recent()),
duration: (i) =>
i % 3 === 2 ? null : `${faker.number.int({ max: 100000 })}s`,
});
@@ -0,0 +1,55 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/credential-library';
import { faker } from '@faker-js/faker';
import permissions from '../helpers/permissions';
import generateId from '../helpers/id';
import {
TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP,
TYPES_CREDENTIAL_LIBRARY,
options,
} from 'api/models/credential-library';
export default factory.extend({
id: () => generateId('cl_'),
type: (i) => TYPES_CREDENTIAL_LIBRARY[i % TYPES_CREDENTIAL_LIBRARY.length],
credential_type(i) {
if (this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC) {
return options.credential_types[i % options.credential_types.length];
}
},
authorized_actions: () =>
permissions.authorizedActionsFor('credential-library') || [
'no-op',
'read',
'update',
'delete',
],
attributes() {
switch (this.type) {
case TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC:
return {
http_method: 'GET',
http_request_body: faker.word.words(1),
path: faker.system.directoryPath(),
};
case TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE:
return {
username: faker.internet.userName(),
key_bits: faker.number.int(999),
path: faker.system.directoryPath(),
};
case TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP:
return {
path: faker.system.directoryPath(),
};
}
},
});
@@ -0,0 +1,75 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/credential-store';
import { faker } from '@faker-js/faker';
import { trait } from 'miragejs';
import permissions from '../helpers/permissions';
import generateId from '../helpers/id';
const types = ['vault', 'static'];
export default factory.extend({
authorized_actions: () =>
permissions.authorizedActionsFor('credential-store') || [
'no-op',
'read',
'update',
'delete',
],
authorized_collection_actions() {
switch (this.type) {
case 'vault':
return {
'credential-libraries': ['create', 'list'],
};
case 'static':
default:
return {
credentials: ['create', 'list'],
};
}
},
id: () => generateId('cs_'),
type: (i) => types[i % types.length],
attributes() {
switch (this.type) {
case 'vault':
return {
address:
Math.random() > 0.5 ? faker.internet.ip() : faker.internet.ipv6(),
namespace: faker.word.words(1),
ca_cert: 'ca-cert-123',
tls_server_name: faker.internet.domainName(),
tls_skip_verify: faker.datatype.boolean(),
token_hmac: 'token-hmac-abcde0987654321',
client_certificate: 'client-cert-123',
client_certificate_key_hmac: 'client-cert-key-hmac-abcde0987654321',
worker_filter: faker.word.words(),
};
}
},
withAssociations: trait({
afterCreate(credentialStore, server) {
const { scope, type } = credentialStore;
switch (type) {
case 'vault':
server.createList('credential-library', 3, {
scope,
credentialStore,
});
break;
case 'static':
default:
server.createList('credential', 5, {
scope,
credentialStore,
});
}
},
}),
});
@@ -0,0 +1,62 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/credential';
import { faker } from '@faker-js/faker';
import permissions from '../helpers/permissions';
import generatedId from '../helpers/id';
import {
TYPES_CREDENTIAL,
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
TYPE_CREDENTIAL_USERNAME_PASSWORD,
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
TYPE_CREDENTIAL_JSON,
TYPE_CREDENTIAL_PASSWORD,
} from 'api/models/credential';
const types = [...TYPES_CREDENTIAL];
export default factory.extend({
type: (i) => types[i % types.length],
id() {
switch (this.type) {
case TYPE_CREDENTIAL_SSH_PRIVATE_KEY:
return generatedId('credspk_');
case TYPE_CREDENTIAL_USERNAME_PASSWORD:
return generatedId('credup_');
case TYPE_CREDENTIAL_JSON:
return generatedId('credjson_');
case TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN:
return generatedId('credupd_');
case TYPE_CREDENTIAL_PASSWORD:
return generatedId('credp_');
}
},
authorized_actions: () =>
permissions.authorizedActionsFor('credential') || [
'no-op',
'read',
'update',
'delete',
],
attributes() {
switch (this.type) {
case TYPE_CREDENTIAL_USERNAME_PASSWORD:
return { username: faker.internet.userName() };
case TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN:
return {
username: faker.internet.userName(),
domain: faker.internet.domainName(),
};
case TYPE_CREDENTIAL_SSH_PRIVATE_KEY:
return { username: faker.internet.userName() };
default:
return {};
}
},
});
+35
View File
@@ -0,0 +1,35 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/group';
import { trait } from 'miragejs';
import permissions from '../helpers/permissions';
import generateId from '../helpers/id';
export default factory.extend({
authorized_actions: () =>
permissions.authorizedActionsFor('group') || [
'no-op',
'read',
'update',
'delete',
'add-members',
'remove-members',
],
id: () => generateId('g_'),
/**
* Generates some users for the same scope and assigns them to the group
* as members.
*/
withMembers: trait({
afterCreate(group, server) {
const scope = group.scope;
const members = server.createList('user', 2, { scope });
group.update({ members });
},
}),
});
@@ -0,0 +1,135 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/host-catalog';
import { trait } from 'miragejs';
import permissions from '../helpers/permissions';
import { faker } from '@faker-js/faker';
import generateId from '../helpers/id';
import {
TYPE_HOST_CATALOG_PLUGIN_AZURE,
TYPE_HOST_CATALOG_PLUGIN_GCP,
TYPE_HOST_CATALOG_PLUGIN_AWS,
TYPE_HOST_CATALOG_STATIC,
TYPE_HOST_CATALOG_DYNAMIC,
TYPES_HOST_CATALOG,
} from 'api/models/host-catalog';
// Represents known plugin types, except "foobar" which models the possibility
// of receiving an _unknown_ type, which the UI must handle gracefully.
const pluginTypes = [
TYPE_HOST_CATALOG_PLUGIN_GCP,
TYPE_HOST_CATALOG_PLUGIN_AWS,
TYPE_HOST_CATALOG_PLUGIN_AZURE,
'foobar',
];
export default factory.extend({
id: () => generateId('hc_'),
// Cycle through available types
type: (i) => TYPES_HOST_CATALOG[i % TYPES_HOST_CATALOG.length],
authorized_actions: () =>
permissions.authorizedActionsFor('host-catalog') || [
'no-op',
'read',
'update',
'delete',
],
authorized_collection_actions: function () {
const isStatic = this.type === TYPE_HOST_CATALOG_STATIC;
return {
// only static catalogs allow host create at this time
hosts: isStatic ? ['create', 'list'] : ['list'],
'host-sets': ['create', 'list'],
};
},
worker_filter: function () {
if (
this.type === TYPE_HOST_CATALOG_DYNAMIC &&
(this.plugin?.name === TYPE_HOST_CATALOG_PLUGIN_AWS ||
this.plugin?.name === TYPE_HOST_CATALOG_PLUGIN_GCP)
) {
return `"${faker.word.noun()}" in "${faker.system.directoryPath()}"`;
}
},
plugin: function (i) {
if (this.type === TYPE_HOST_CATALOG_DYNAMIC) {
return {
id: `plugin-id-${i}`,
name: pluginTypes[i % pluginTypes.length],
description: faker.word.words(),
};
}
},
attributes() {
switch (this.plugin?.name) {
case TYPE_HOST_CATALOG_PLUGIN_AWS:
return {
disable_credential_rotation: faker.datatype.boolean(),
region: `us-${faker.location.cardinalDirection()}-${faker.number.int(
9,
)}`,
};
case TYPE_HOST_CATALOG_PLUGIN_AZURE:
return {
disable_credential_rotation: faker.datatype.boolean(),
tenant_id: faker.string.uuid(),
client_id: faker.string.uuid(),
subscription_id: faker.string.uuid(),
};
case TYPE_HOST_CATALOG_PLUGIN_GCP:
return {
disable_credential_rotation: faker.datatype.boolean(),
project_id: faker.string.uuid(),
client_email: faker.internet.email(),
target_service_account_id: faker.string.uuid(),
zone: `us-${faker.location.cardinalDirection()}${faker.number.int(
9,
)}-${faker.string.fromCharacters('abc')}`,
};
}
},
secrets() {
switch (this.plugin?.name) {
case TYPE_HOST_CATALOG_PLUGIN_AWS:
return {
access_key_id: faker.string.nanoid(),
secret_access_key: faker.string.nanoid(),
};
case TYPE_HOST_CATALOG_PLUGIN_AZURE:
return {
secret_id: faker.string.nanoid(),
secret_value: faker.string.nanoid(),
};
case TYPE_HOST_CATALOG_PLUGIN_GCP:
return {
private_key: faker.string.nanoid(),
private_key_id: faker.string.nanoid(),
};
}
},
withChildren: trait({
afterCreate(hostCatalog, server) {
const { scope, type } = hostCatalog;
const hosts = server.createList('host', 10, { scope, hostCatalog, type });
const hostSets = server.createList('host-set', 3, {
scope,
hostCatalog,
hosts,
type,
});
const hostSetIds = hostSets.map((hostSet) => hostSet.id);
hosts.forEach((host) => {
host.update({ hostSetIds });
});
},
}),
});
@@ -0,0 +1,74 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/host-set';
import permissions from '../helpers/permissions';
import { faker } from '@faker-js/faker';
import generateId from '../helpers/id';
import {
TYPE_HOST_CATALOG_PLUGIN_AWS,
TYPE_HOST_CATALOG_PLUGIN_AZURE,
TYPE_HOST_CATALOG_PLUGIN_GCP,
} from 'api/models/host-catalog';
function generateFilters(prefix) {
const filtersAmount = faker.number.int({ min: 1, max: 5 });
let filters = [];
for (let i = 0; i < filtersAmount; ++i) {
filters.push(`${prefix}${faker.word.words(1)}=${faker.word.words(1)}`);
}
return {
filters,
};
}
export default factory.extend({
id: () => generateId('hs_'),
type() {
return this.hostCatalog?.type || factory.attrs.type;
},
authorized_actions: function () {
const isStatic = this.type === 'static';
const defaults = ['no-op', 'read', 'update', 'delete'];
// Only static allows host management at this time.
if (isStatic) defaults.push('add-hosts', 'remove-hosts');
return permissions.authorizedActionsFor('host-set') || defaults;
},
plugin() {
if (this.type === 'plugin') {
return this.hostCatalog.plugin;
}
},
preferred_endpoints() {
if (this.type === 'plugin') {
const endpointsAmount = faker.number.int({ min: 1, max: 5 });
let result = [];
for (let i = 0; i < endpointsAmount; ++i) {
result.push(
Math.random() > 0.5 ? faker.internet.ip() : faker.internet.ipv6(),
);
}
return result;
}
},
sync_interval_seconds() {
if (this.type === 'plugin') {
return faker.number.int();
}
},
attributes() {
switch (this.plugin?.name) {
case TYPE_HOST_CATALOG_PLUGIN_AWS:
return generateFilters('tag:');
case TYPE_HOST_CATALOG_PLUGIN_AZURE:
return {
filter: `${faker.database.column()}=${faker.database.collation()}`,
};
case TYPE_HOST_CATALOG_PLUGIN_GCP:
return generateFilters('labels.');
}
},
});
+67
View File
@@ -0,0 +1,67 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/host';
import permissions from '../helpers/permissions';
import { faker } from '@faker-js/faker';
import generateId from '../helpers/id';
import {
TYPE_HOST_CATALOG_STATIC,
TYPE_HOST_CATALOG_DYNAMIC,
} from 'api/models/host-catalog';
export default factory.extend({
id: () => generateId('h_'),
name() {
if (this.type === TYPE_HOST_CATALOG_STATIC) return faker.word.words();
},
type() {
return this.hostCatalog?.type || factory.attrs.type;
},
authorized_actions: function () {
const isStatic = this.type === TYPE_HOST_CATALOG_STATIC;
const defaults = ['no-op', 'read'];
// Only static allows update/delete at this time.
if (isStatic) defaults.push('update', 'delete');
return permissions.authorizedActionsFor('host') || defaults;
},
plugin() {
if (this.type === TYPE_HOST_CATALOG_DYNAMIC) {
return this.hostCatalog.plugin;
}
},
ip_addresses() {
const addressesAmount = Math.floor(Math.random() * 6) + 1;
let result = [];
for (let i = 0; i < addressesAmount; ++i) {
result.push(
Math.random() > 0.5 ? faker.internet.ip() : faker.internet.ipv6(),
);
}
return result;
},
// Return external fields only for plugins
external_id() {
if (this.type === TYPE_HOST_CATALOG_DYNAMIC) {
return faker.string.uuid();
}
},
external_name(i) {
if (this.type === TYPE_HOST_CATALOG_DYNAMIC) {
return `${this.plugin.name} provided name ${i}`;
}
},
// Only aws plugins have dns_names
dns_names() {
if (this.plugin?.name === 'aws') {
const dnsNamesAmount = Math.floor(Math.random() * 6) + 1;
let result = [];
for (let i = 0; i < dnsNamesAmount; i++) {
result.push(faker.internet.domainName());
}
return result;
}
},
});
@@ -0,0 +1,48 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/managed-group';
import { faker } from '@faker-js/faker';
import permissions from '../helpers/permissions';
import generateId from '../helpers/id';
import {
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_LDAP,
} from 'api/models/auth-method';
export default factory.extend({
authorized_actions: () =>
permissions.authorizedActionsFor('managed-group') || [
'no-op',
'read',
'update',
'delete',
],
id: () => generateId('mg_'),
type() {
return this.authMethod?.type;
},
attributes() {
switch (this.type) {
case TYPE_AUTH_METHOD_OIDC:
return {
filter: `"${faker.word.noun()}" in "${faker.system.directoryPath()}"`,
};
case TYPE_AUTH_METHOD_LDAP: {
const num = faker.number.int({ min: 2, max: 4 });
const names = [];
for (let i = 0; i < num; i++) {
names.push(faker.word.noun());
}
return {
group_names: names,
};
}
}
},
});
+35
View File
@@ -0,0 +1,35 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/policy';
import permissions from '../helpers/permissions';
import { faker } from '@faker-js/faker';
import generateId from '../helpers/id';
export default factory.extend({
id: () => generateId('pst_'),
authorized_actions: () =>
permissions.authorizedActionsFor('policy') || [
'no-op',
'read',
'update',
'delete',
],
attributes() {
return {
retain_for: {
days: faker.number.int({ min: 10, max: 100 }),
overridable: faker.datatype.boolean(),
},
delete_after: {
days: faker.number.int({ min: 101, max: 500 }),
overridable: faker.datatype.boolean(),
},
};
},
});
+72
View File
@@ -0,0 +1,72 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/role';
import { trait } from 'miragejs';
import permissions from '../helpers/permissions';
import generateId from '../helpers/id';
import {
TYPE_SCOPE_GLOBAL,
TYPE_SCOPE_ORG,
TYPE_SCOPE_PROJECT,
} from 'api/models/scope';
export default factory.extend({
authorized_actions: () =>
permissions.authorizedActionsFor('role') || [
'no-op',
'read',
'update',
'delete',
'set-grants',
'add-principals',
'remove-principals',
'set-grant-scopes',
],
// eslint-disable-next-line ember/avoid-leaking-state-in-ember-objects
grant_strings: () => [
'ids=*;action=*',
'ids=*;type=host-catalog;actions=create,read',
],
id: () => generateId('r_'),
grant_scope_ids: () => ['this'],
/**
* Adds grant scopes to the role.
*/
withScopes: trait({
afterCreate(role, server) {
const { scope } = role;
const newScope =
scope.id === 'global'
? server.create('scope', {
scope: { id: 'global', type: TYPE_SCOPE_GLOBAL },
type: 'org',
})
: server.create('scope', {
scope: { id: scope.id, type: TYPE_SCOPE_ORG },
type: TYPE_SCOPE_PROJECT,
});
role.update({ grant_scope_ids: ['this', newScope.id] });
},
}),
/**
* Adds principals to the role.
*/
withPrincipals: trait({
afterCreate(role, server) {
const { scope } = role;
const { id: scopeId } = scope;
const users = server.createList('user', 2, { scope });
const groups = server.createList('group', 2, { scope });
const managedGroups = server.schema.managedGroups.where({
scopeId,
}).models;
role.update({ users, groups, managedGroups });
},
}),
});
+129
View File
@@ -0,0 +1,129 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/scope';
import { trait } from 'miragejs';
import permissions from '../helpers/permissions';
import generateId from '../helpers/id';
import { faker } from '@faker-js/faker';
import { TYPE_AUTH_METHOD_PASSWORD } from 'api/models/auth-method';
export default factory.extend({
type: 'global',
authorized_actions() {
const authorizedActions = ['no-op', 'read', 'update'];
// Storage policies can only be attached to global and org scopes.
if (this.type === 'global' || this.type === 'org') {
authorizedActions.push('attach-storage-policy', 'detach-storage-policy');
}
if (this.type === 'org' || this.type === 'project') {
authorizedActions.push('delete');
}
return permissions.authorizedActionsFor('scope') || authorizedActions;
},
authorized_collection_actions() {
const collectionActions = {
scopes: ['create', 'list'],
groups: ['create', 'list'],
roles: ['create', 'list'],
};
// Resources that are available only on the global scope
if (this.type === 'global') {
collectionActions.workers = ['create:worker-led', 'list'];
collectionActions.aliases = ['create', 'list'];
}
// Resources that are only available on the global or the org scope
if (this.type === 'global' || this.type === 'org') {
collectionActions['storage-buckets'] = ['create', 'list'];
collectionActions['session-recordings'] = ['list'];
collectionActions.users = ['create', 'list'];
collectionActions['auth-methods'] = ['create', 'list'];
collectionActions['policies'] = ['create', 'list'];
}
if (this.type === 'project') {
collectionActions['credential-stores'] = ['create', 'list'];
collectionActions['host-catalogs'] = ['create', 'list'];
collectionActions.sessions = ['list'];
collectionActions.targets = ['create', 'list'];
}
return collectionActions;
},
id() {
let id = 'global';
if (this.type === 'org') {
id = generateId('o_');
}
if (this.type === 'project') {
id = generateId('p_');
}
return id;
},
name() {
let name = faker.word.words();
if (this.type === 'global') {
name = 'global';
}
return name;
},
scope() {
if (this.type === 'global') {
return {
id: 'global',
type: 'global',
};
}
},
withChildren: trait({
afterCreate(record, server) {
if (record.type === 'global') {
server.createList(
'scope',
1,
{
type: 'org',
scope: { id: record.id, type: record.type },
},
'withChildren',
);
}
if (record.type === 'org') {
server.createList('scope', 3, {
type: 'project',
scope: { id: record.id, type: record.type },
});
}
},
}),
withGlobalAuth: trait({
afterCreate(record, server) {
if (record.type === 'global') {
const authMethod = server.create('auth-method', {
type: TYPE_AUTH_METHOD_PASSWORD,
scope: record,
});
server.create('account', {
type: TYPE_AUTH_METHOD_PASSWORD,
full_name: 'admin',
scope: record,
authMethod,
});
}
},
}),
});
@@ -0,0 +1,118 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/session-recording';
import permissions from '../helpers/permissions';
import generateId from '../helpers/id';
import { faker } from '@faker-js/faker';
import { trait } from 'miragejs';
import {
TYPES_SESSION_RECORDING as types,
STATE_SESSION_RECORDING_STARTED,
STATE_SESSION_RECORDING_AVAILABLE,
STATE_SESSION_RECORDING_UNKNOWN,
} from 'api/models/session-recording';
const states = [
STATE_SESSION_RECORDING_AVAILABLE,
STATE_SESSION_RECORDING_STARTED,
STATE_SESSION_RECORDING_UNKNOWN,
];
const pickRandomElement = (arr) => faker.helpers.arrayElement(arr);
export default factory.extend({
id: () => generateId('sr_'),
// Cycle through available types
type: (i) => types[i % types.length],
state: (i) => states[i % states.length],
start_time() {
return this.state === STATE_SESSION_RECORDING_AVAILABLE
? faker.date.recent({ days: 3, refDate: this.end_time })
: null;
},
end_time() {
return this.state === STATE_SESSION_RECORDING_AVAILABLE
? faker.date.recent({ days: 1, refDate: this.updated_time })
: null;
},
duration() {
return this.state === STATE_SESSION_RECORDING_AVAILABLE
? `${faker.number.int({ max: 100000 })}s`
: null;
},
retain_until() {
return this.state === STATE_SESSION_RECORDING_AVAILABLE
? faker.date.recent()
: null;
},
delete_after() {
return this.state === STATE_SESSION_RECORDING_AVAILABLE
? faker.date.recent()
: null;
},
authorized_actions: () =>
permissions.authorizedActionsFor('session-recording') || [
'no-op',
'read',
'download',
'reapply-storage-policy',
'delete',
],
withNonExistingUserAndTarget: trait({
afterCreate(sessionRecording, server) {
const user = pickRandomElement(server.schema.users.all().models);
user.id = 'deleted_user';
const session = pickRandomElement(server.schema.sessions.all().models);
session.id = 'deleted_session';
const target = pickRandomElement(
server.schema.targets.where((target) => target.storage_bucket_id)
.models,
);
target.id = 'deleted_target';
sessionRecording.update({
sessionId: session.id,
storage_bucket_id: target.storage_bucket_id,
create_time_values: {
user: user.attrs,
target: target.attrs,
},
});
},
}),
withExistingUserAndTarget: trait({
afterCreate(sessionRecording, server) {
const user = pickRandomElement(server.schema.users.all().models);
const session = pickRandomElement(server.schema.sessions.all().models);
const target = pickRandomElement(
server.schema.targets.where((target) => target.storage_bucket_id)
.models,
);
sessionRecording.update({
sessionId: session.id,
storage_bucket_id: target.storage_bucket_id,
create_time_values: {
user: user.attrs,
target: target.attrs,
},
});
},
}),
withConnectionAndChannels: trait({
afterCreate(session_recording, server) {
server
.createList('connection-recording', 3, { session_recording })
.forEach((connection_recording) => {
server.createList('channel-recording', 3, { connection_recording });
});
},
}),
});
@@ -0,0 +1,55 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/session';
import { trait } from 'miragejs';
import { faker } from '@faker-js/faker';
import generateId from '../helpers/id';
import permissions from '../helpers/permissions';
const statusStrings = ['pending', 'active', 'canceling', 'terminated'];
export const pickRandomStatusString = () =>
faker.helpers.arrayElement(statusStrings);
export default factory.extend({
id: () => generateId('ss_'),
authorized_actions: () =>
permissions.authorizedActionsFor('session') || ['read', 'cancel'],
status: pickRandomStatusString(),
/**
* Sessions generated with this trait will automatically form
* associations to user, target, host set, and host (if available) within
* the current scope.
*/
withAssociations: trait({
afterCreate(record, server) {
const scopeId = record.scopeId;
// Assign a user to this session if not already assigned.
if (record.scope && !record.user) {
const scopeId =
record.scope.type === 'project'
? record.scope?.scope.id
: record.scope.id;
const user = server.schema.users.where({ scopeId }).models[0];
if (user) record.update({ user });
}
// Assign target and host set to this session, if not already assigned.
const target = server.schema.targets.where({ scopeId }).models[0];
if (target) {
record.update({ target });
const hostSet = target.hostSets.models[0];
if (hostSet) {
record.update({ hostSet });
const host = hostSet.hosts.models[0];
if (host) record.update({ host });
}
}
},
}),
});
@@ -0,0 +1,60 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/storage-bucket';
import permissions from '../helpers/permissions';
import { faker } from '@faker-js/faker';
import generateId from '../helpers/id';
import {
TYPES_STORAGE_BUCKET as types,
TYPES_STORAGE_BUCKET_PLUGIN,
TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
TYPE_STORAGE_BUCKET_PLUGIN_MINIO,
} from 'api/models/storage-bucket';
// Represents known plugin types, except "foobar" which models the possibility
// of receiving an _unknown_ type, which the UI must handle gracefully.
const pluginTypes = [...TYPES_STORAGE_BUCKET_PLUGIN, 'foobar'];
export default factory.extend({
id: () => generateId('sb_'),
// Cycle through available types
type: (i) => types[i % types.length],
authorized_actions: () =>
permissions.authorizedActionsFor('storage-bucket') || [
'no-op',
'read',
'update',
'delete',
],
plugin: function (i) {
if (this.type === 'plugin') {
return {
id: `plugin-id-${i}`,
name: pluginTypes[i % pluginTypes.length],
description: faker.word.words(),
};
}
},
attributes() {
switch (this.plugin?.name) {
case TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3:
return {
region: `us-${faker.location.cardinalDirection()}-${faker.number.int(
9,
)}`,
disable_credential_rotation: faker.datatype.boolean(),
};
case TYPE_STORAGE_BUCKET_PLUGIN_MINIO:
return {
endpoint_url: faker.internet.url(),
};
}
},
});
+266
View File
@@ -0,0 +1,266 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/target';
import { trait } from 'miragejs';
import { faker } from '@faker-js/faker';
import permissions from '../helpers/permissions';
import generateId from '../helpers/id';
import {
TYPES_TARGET,
TYPE_TARGET_SSH,
TYPE_TARGET_RDP,
} from 'api/models/target';
import {
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
TYPE_CREDENTIAL_USERNAME_PASSWORD,
TYPE_CREDENTIAL_JSON,
TYPE_CREDENTIAL_PASSWORD,
} from 'api/models/credential';
import { TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP } from 'api/models/credential-library';
const randomBoolean = (chance = 0.5) => Math.random() < chance;
const randomFilter = () =>
`"${faker.word.noun()}" in "${faker.system.directoryPath()}"`;
const hostSetChance = 0.3;
const types = [...TYPES_TARGET];
/**
* Helper to select items from a collection based on a condition.
*/
const selectItems = (collection, condition) =>
collection
.where(condition)
.models.filter(() => randomBoolean())
.map((item) => item.id);
/**
* Helper to update target with selected credential sources and host sets.
*/
const updateTarget = (
target,
hostSets,
isInjectable,
injectedSources,
brokeredSources,
) => {
const fieldsToUpdate = {
hostSets,
brokeredCredentialSourceIds: brokeredSources,
};
if (isInjectable) {
fieldsToUpdate.injectedApplicationCredentialSourceIds = injectedSources;
}
target.update(fieldsToUpdate);
};
export default factory.extend({
authorized_actions: () =>
permissions.authorizedActionsFor('target') || [
'no-op',
'read',
'update',
'delete',
'authorize-session',
'add-host-sources',
'remove-host-sources',
'add-credential-sources',
'remove-credential-sources',
],
id: () => generateId('t_'),
/**
* -1 means "unlimited" and we want to generate these on occasion.
*/
session_connection_limit: () =>
faker.helpers.arrayElement([-1, faker.number.int()]),
egress_worker_filter: (i) => (i % 2 !== 0 ? randomFilter() : null),
ingress_worker_filter: (i) => (i % 2 !== 0 ? randomFilter() : null),
type: (i) => types[i % types.length],
/**
* Randomly selects existing host sets and credential libraries to assign to target.
*/
withAssociations: trait({
afterCreate(target, server) {
const { scope } = target;
const randomlySelectedHostSets = server.schema.hostSets
// BLERG: fun fact, for no reason at all, the element passed
// into a where function is not a full model instance, as you might
// expect at this level of abstraction, but appears to be a serialized
// representation of the model instance. It's very confusing since
// the result set of `where` _is a collection of model instances_.
.where((hostSet) => hostSet.scopeId === scope.id)
.models.filter(() => randomBoolean(hostSetChance));
const randomlySelectedBrokeredCredentialSources = [
...selectItems(
server.schema.credentialLibraries,
(credentialLibrary) => credentialLibrary.scopeId === target.scope.id,
),
...selectItems(
server.schema.credentials,
(credential) => credential.scopeId === target.scope.id,
),
];
// Select compatible credential libraries and credentials for injection
// Since not every credential or credential library can be injected, we filter them to mimic the expected behavior.
const filteredCredentialLibraries = selectItems(
server.schema.credentialLibraries,
(cred) =>
cred.scopeId === scope.id &&
cred.credential_type !== TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN &&
cred.credential_type !== TYPE_CREDENTIAL_PASSWORD &&
cred.type !== TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP,
);
const filteredCredentials = selectItems(
server.schema.credentials,
(cred) =>
cred.scopeId === scope.id &&
![
TYPE_CREDENTIAL_JSON,
TYPE_CREDENTIAL_PASSWORD,
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
].includes(cred.type),
);
// RDP specific credential libraries and credentials
const filteredCredentialLibrariesForRDP = selectItems(
server.schema.credentialLibraries,
(cred) =>
(cred.scopeId === scope.id &&
[
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
TYPE_CREDENTIAL_USERNAME_PASSWORD,
].includes(cred.credential_type)) ||
cred.type == TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP,
);
const filteredCredentialsForRDP = selectItems(
server.schema.credentials,
(cred) =>
cred.scopeId === scope.id &&
[
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
TYPE_CREDENTIAL_USERNAME_PASSWORD,
].includes(cred.type),
);
// Update target based on type
const isInjectable =
target.type === TYPE_TARGET_SSH || target.type === TYPE_TARGET_RDP;
const injectedCredentialSources =
target.type === TYPE_TARGET_RDP
? [...filteredCredentialLibrariesForRDP, ...filteredCredentialsForRDP]
: [...filteredCredentialLibraries, ...filteredCredentials];
updateTarget(
target,
randomlySelectedHostSets,
isInjectable,
injectedCredentialSources,
randomlySelectedBrokeredCredentialSources,
);
// Add storage bucket for SSH targets
const storageBuckets = server.schema.storageBuckets.all().models;
const randomlySelectedStorageBucket =
storageBuckets.length === 0
? undefined
: faker.helpers.arrayElement(storageBuckets);
if (randomlySelectedStorageBucket && target.type === TYPE_TARGET_SSH) {
target.update({
storage_bucket_id: randomlySelectedStorageBucket.id,
enable_session_recording: true,
});
}
// Add Aliases
const aliases = server.schema.aliases.all().models;
const randomlySelectedAliases =
aliases.length === 0 ? undefined : faker.helpers.arrayElement(aliases);
if (randomlySelectedAliases) {
target.update({
aliases: randomlySelectedAliases.aliases,
});
}
},
}),
withManyHosts: trait({
afterCreate(target, server) {
const { scope } = target;
const hostCatalog = server.create('host-catalog', { scope });
const hosts = server.createList('host', 15, {
scope,
hostCatalog,
type: hostCatalog.type,
});
const hostSets = [
server.create('host-set', {
scope,
hostCatalog,
hosts,
type: hostCatalog.type,
}),
];
const hostSetIds = hostSets.map((hostSet) => hostSet.id);
hosts.forEach((host) => {
host.update({ hostSetIds });
});
target.update({ hostSets });
},
}),
withTwoHosts: trait({
afterCreate(target, server) {
const { scope } = target;
const hostCatalog = server.create('host-catalog', { scope });
const hosts = server.createList('host', 2, {
scope,
hostCatalog,
type: hostCatalog.type,
});
const hostSets = [
server.create('host-set', {
scope,
hostCatalog,
hosts,
type: hostCatalog.type,
}),
];
const hostSetIds = hostSets.map((hostSet) => hostSet.id);
hosts.forEach((host) => {
host.update({ hostSetIds });
});
target.update({ hostSets });
},
}),
withOneHost: trait({
afterCreate(target, server) {
const { scope } = target;
const hostCatalog = server.create('host-catalog', { scope });
const hosts = [
server.create('host', { scope, hostCatalog, type: hostCatalog.type }),
];
const hostSets = [
server.create('host-set', {
scope,
hostCatalog,
hosts,
type: hostCatalog.type,
}),
];
const hostSetIds = hostSets.map((hostSet) => hostSet.id);
hosts.forEach((host) => {
host.update({ hostSetIds });
});
target.update({ hostSets });
},
}),
});
+21
View File
@@ -0,0 +1,21 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/user';
import permissions from '../helpers/permissions';
import generateId from '../helpers/id';
export default factory.extend({
authorized_actions: () =>
permissions.authorizedActionsFor('user') || [
'no-op',
'read',
'update',
'delete',
'add-accounts',
'remove-accounts',
],
id: () => generateId('u_'),
});
+72
View File
@@ -0,0 +1,72 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import factory from '../generated/factories/worker';
import permissions from '../helpers/permissions';
import generateId from '../helpers/id';
import { faker } from '@faker-js/faker';
export default factory.extend({
id: () => generateId('w_'),
authorized_actions: () =>
permissions.authorizedActionsFor('worker') || [
'no-op',
'read',
'update',
'delete',
'set-worker-tags',
'remove-worker-tags',
],
config_tags: (i) => {
if (i % 3 === 0) {
return {
os: ['ubuntu'],
env: ['dev', 'local'],
};
}
if (i % 2 === 0) {
return { os: ['ubuntu'] };
}
return undefined;
},
api_tags: (i) => {
if (i % 3 === 0) {
return {
type: [faker.word.words(1), faker.word.words(1)],
os: ['z-os'],
env: ['prod', 'qa'],
sample: [faker.word.words(1), faker.word.words(1)],
path: [faker.word.words(1)],
};
}
if (i % 2 === 0) {
return { os: ['z-os'] };
}
return undefined;
},
canonical_tags() {
const configTags = this.config_tags || {};
const apiTags = this.api_tags || {};
const canonicalTags = {};
Object.keys(configTags).forEach((key) => {
canonicalTags[key] = new Set(configTags[key]);
});
Object.keys(apiTags).forEach((key) => {
if (canonicalTags[key]) {
apiTags[key].forEach((tag) => canonicalTags[key].add(tag));
} else {
canonicalTags[key] = new Set(apiTags[key]);
}
});
Object.keys(canonicalTags).forEach((key) => {
canonicalTags[key] = Array.from(canonicalTags[key]);
});
return canonicalTags;
},
});
@@ -0,0 +1,18 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedAccountFactory
*/
export default Factory.extend({
name: () => faker.word.words(),
description: () => faker.word.words(),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
version: () => faker.number.int(),
});
@@ -0,0 +1,21 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
import { TYPE_ALIAS_TARGET } from 'api/models/alias';
/**
* GeneratedAliasModelFactory
*/
export default Factory.extend({
name: () => faker.word.words(),
type: TYPE_ALIAS_TARGET,
value: () => faker.word.words(),
description: () => faker.word.words(),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
version: () => faker.number.int(),
});
@@ -0,0 +1,83 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
import {
TYPE_AUTH_METHOD_PASSWORD,
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_LDAP,
} from 'api/models/auth-method';
/**
* GeneratedAuthMethodModelFactory
*/
export default Factory.extend({
type: TYPE_AUTH_METHOD_PASSWORD,
name: () => faker.word.words(),
description: () => faker.word.words(),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
disabled: () => faker.datatype.boolean(),
attributes: function () {
let attrs;
switch (this.type) {
case TYPE_AUTH_METHOD_OIDC:
attrs = {
api_url_prefix: 'http://127.0.0.1:9200',
callback_url:
'http://127.0.0.1:9200/v1/auth-methods/oidc:authenticate:callback',
client_id: 'YTwdJBB55LpKnuXb9n65',
client_secret_hmac: 'GY7blb4ynM5TEPNi_qnphJ9aDWhW8FpmN3D-eP_L7Zk',
issuer: 'http://127.0.0.1:57070',
max_age: 1800,
disable_discovered_config_validation: false,
state: 'active-public',
account_claim_maps: ['oid=sub', 'full_name=name'],
claims_scopes: ['profile', 'email'],
signing_algorithms: ['RS256', 'RS384'],
allowed_audiences: ['www.alice.com', 'www.alice.com/admin'],
idp_ca_certs: ['certificate-1234', 'certificate-5678'],
prompts: ['consent'],
};
break;
case TYPE_AUTH_METHOD_LDAP:
attrs = {
state: 'active-public',
urls: [`ldap://${faker.internet.domainName()}`],
certificates: [faker.string.alphanumeric(50)],
client_certificate: faker.string.alphanumeric(50),
client_certificate_key: `---Begin Certificate --- ${faker.string.alphanumeric(
170,
)} ---End Certificate ---`,
client_certificate_key_hmac: faker.string.alphanumeric(50),
enable_groups: true,
start_tls: false,
insecure_tls: false,
bind_dn: 'cn=read-only-admin,dc=example,dc=com',
bind_password: 'password',
bind_password_hmac: faker.string.alphanumeric(50),
upn_domain: 'example.com',
discover_dn: false,
anon_group_search: false,
user_dn: 'dc=example,dc=com',
user_attr: 'uid',
user_filter: '({{.UserAttr}}={{.Username}})',
group_dn: 'dc=example,dc=com',
group_attr: 'cn',
group_filter: '(|(memberUid={{.Username}})',
account_attribute_maps: [
'preferredName=fullName',
'preferredEmail=email',
],
use_token_groups: false,
};
break;
}
return attrs;
},
});
@@ -0,0 +1,16 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedChannelRecordingModelFactory
*/
export default Factory.extend({
bytes_up: () => faker.string.numeric(faker.number.int({ min: 1, max: 12 })),
bytes_down: () => faker.string.numeric(faker.number.int({ min: 1, max: 12 })),
errors: () => faker.number.int({ max: 10 }),
});
@@ -0,0 +1,16 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedConnectionRecordingModelFactory
*/
export default Factory.extend({
bytes_up: () => faker.string.numeric(faker.number.int({ min: 1, max: 12 })),
bytes_down: () => faker.string.numeric(faker.number.int({ min: 1, max: 12 })),
errors: () => faker.number.int({ max: 10 }),
});
@@ -0,0 +1,17 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedCredentialLibraryModelFactory
*/
export default Factory.extend({
name: () => faker.word.words(),
description: () => faker.word.words(),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
});
@@ -0,0 +1,17 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedCredentialStoreModelFactory
*/
export default Factory.extend({
name: () => faker.word.words(),
description: () => faker.word.words(),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
});
@@ -0,0 +1,18 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedCredentialModelFactory
*/
export default Factory.extend({
name: () => faker.word.words(),
description: () => faker.word.words(),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
version: () => faker.number.int(),
});
@@ -0,0 +1,19 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedGroupModelFactory
* Group contains all fields related to a Group resource
*/
export default Factory.extend({
name: () => faker.word.words(),
description: () => faker.word.words(),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
disabled: () => faker.datatype.boolean(),
});
@@ -0,0 +1,19 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedHostCatalogModelFactory
*/
export default Factory.extend({
type: 'static',
name: () => faker.word.words(),
description: () => faker.word.words(),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
disabled: () => faker.datatype.boolean(),
});
@@ -0,0 +1,20 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedHostSetModelFactory
*/
export default Factory.extend({
type: 'static',
name: () => faker.word.words(),
description: () => faker.word.words(),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
version: () => faker.number.int(),
size: () => faker.number.int(),
});
@@ -0,0 +1,24 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedHostModelFactory
*/
export default Factory.extend({
type: 'static',
description: () => faker.word.words(),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
version: () => faker.number.int(),
attributes: () => {
return {
address:
Math.random() > 0.5 ? faker.internet.ip() : faker.internet.ipv6(),
};
},
});
@@ -0,0 +1,19 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedManagedGroupFactory
* A managed group is a resource that represents a collection of accounts.
*/
export default Factory.extend({
name: () => faker.word.words(),
description: () => faker.word.words(),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
version: () => faker.number.int(),
});
@@ -0,0 +1,18 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedOrgModelFactory
*
*/
export default Factory.extend({
name: () => faker.word.words(),
description: () => faker.word.words(),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
});
@@ -0,0 +1,19 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedStoragePolicyModelFactory
*/
export default Factory.extend({
name: () => faker.word.words(),
type: 'storage',
description: () => faker.word.words(),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
version: () => faker.number.int(),
});
@@ -0,0 +1,19 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedProjectModelFactory
* Project contains all fields related to a Project resource
*/
export default Factory.extend({
name: () => faker.word.words(),
description: () => faker.word.words(),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
disabled: () => faker.datatype.boolean(),
});
@@ -0,0 +1,20 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedRoleModelFactory
* Role contains all field related to a Role resource
*/
export default Factory.extend({
name: () => faker.word.words(),
description: () => faker.word.words(),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
disabled: () => faker.datatype.boolean(),
version: () => faker.number.int(),
});
@@ -0,0 +1,17 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedScopeModelFactory
*/
export default Factory.extend({
description: () => faker.word.words(),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
disabled: () => faker.datatype.boolean(),
});
@@ -0,0 +1,18 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedSessionRecordingModelFactory
*/
export default Factory.extend({
bytes_up: () => faker.string.numeric(faker.number.int({ min: 1, max: 12 })),
bytes_down: () => faker.string.numeric(faker.number.int({ min: 1, max: 12 })),
errors: () => faker.number.int(10),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
});
@@ -0,0 +1,18 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedSessionModelFactory
*/
export default Factory.extend({
type: 'tcp',
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
expiration_time: () => faker.date.soon(),
version: () => faker.number.int(),
});
@@ -0,0 +1,22 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedStorageBucketModelFactory
*/
export default Factory.extend({
name: () => faker.word.words(),
description: () => faker.word.words(),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
version: () => faker.number.int(),
bucket_name: () => faker.internet.domainWord(),
bucket_prefix: () => faker.system.directoryPath(),
worker_filter: () =>
`"${faker.word.noun()}" in "${faker.system.directoryPath()}"`,
});
@@ -0,0 +1,22 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedTargetModelFactory
*/
export default Factory.extend({
name: () => faker.word.words(),
session_max_seconds: () => faker.number.int(),
description: () => faker.word.words(),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
disabled: () => faker.datatype.boolean(),
default_port: () => faker.internet.port(),
default_client_port: () => faker.internet.port(),
version: () => faker.number.int(),
});
@@ -0,0 +1,20 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
/**
* GeneratedUserModelFactory
* User contains all fields related to a User resource
*/
export default Factory.extend({
name: () => faker.word.words(),
description: () => faker.word.words(),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
disabled: () => faker.datatype.boolean(),
email: () => faker.internet.email().toLowerCase(),
});
@@ -0,0 +1,29 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
import { TYPE_WORKER_PKI } from 'api/models/worker';
/**
* GeneratedWorkerModelFactory
* Worker contains all fields related to a Worker resource
*/
export default Factory.extend({
name: () => faker.word.words(),
description: () => faker.word.words(),
type: TYPE_WORKER_PKI,
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
version: () => faker.number.int(),
address: () =>
`${faker.string.uuid()}.proxy.boundary.hashicorp.cloud:${faker.internet.port()}`,
last_status_time: () => faker.date.recent(),
active_connection_count: () => faker.number.int(10),
release_version: () =>
`Boundary v${faker.number.int(11)}.${faker.number.int(
50,
)}.${faker.number.int(12)}`,
});
@@ -0,0 +1,40 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { makeEvaluator } from 'js-bexpr';
import { underscore } from '@ember/string';
/**
* Because Mirage, a record cannot be serialized outside of a route handler
* context. Here is a quick and dirty attempt to serialize attribute names
* to underscore format. This is not entirely realistic, but may suffice.
*/
const quickSerialize = (item) => {
const json = item.toJSON();
Object.keys(json).forEach((key) => {
const newKey = underscore(key);
json[newKey] = json[key];
});
return json;
};
/**
* Creates a filter function that operates on a single object, which itself
* returns the result of evaluating the expression against the object.
*/
export default (expression) => {
if (expression) {
const evaluator = makeEvaluator(expression);
// Since API query selectors begin with `/item`, the instance is wrapped.
return (record) => {
const item = quickSerialize(record);
return evaluator({ item });
};
} else {
// If no expression was passed, always return true,
// thus filtering out none.
return () => true;
}
};
+46
View File
@@ -0,0 +1,46 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
/**
* The Mulberry32 PRNG, JavaScript implementation. This is a seedable PRNG,
* which we need in order to deterministically generate IDs which are stable
* across reloads. Unfortunately, the built-in `Math.random` function
* is not seedable.
*
* Source:
* https://github.com/bryc/code/blob/master/jshash/PRNGs.md#mulberry32
*
* @param {number} a
* @return {number}
*/
function mulberry32(a = 0) {
return function () {
a += 0x6d2b79f5 | 0;
var t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const random = mulberry32();
/**
* Generates a random ID string at least 10 characters long with the
* specified prefix.
*
* @param {string} prefix
* @return {string}
*/
export default function (prefix = '') {
let result = '';
for (let i = 0; i < 10; i++) {
result = result.concat(
characters[Math.floor(random() * characters.length)],
);
}
return prefix.concat(result);
}
@@ -0,0 +1,61 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
/**
* Allows development-time toggling of resource permissions via the window
* querystring. Pass a JSON object into the query to toggle permissions.
* Note that the value must be a valid JSON object in order to be parsed.
*
* @example
* ?authorized_actions={"account": ["read"]}
*
* Within a Mirage factory, pass the actions into the mock:
*
* @example
* import { Factory } from 'miragejs';
* import permissions from '../helpers/permissions';
*
* export default Factory.extend({
* authorized_actions: () =>
* permissions.authorizedActionsFor('account') ||
* ['no-op', 'read', 'update', 'delete']
* });
*/
class ToggledCapabilities {
// =attributes
// Get the query string and drop the leading '?'
#rawQuery = window.location.search.substring(1);
// Split the querystring into key/value pairs
#queryPairs = this.#rawQuery.split('&').map((item) => item.split('='));
// Find the value of a pair with key `authorized_actions`
// Safe-ish when the item is not present
#rawAuthorizedActions = (this.#queryPairs.find(
(item) => item[0] === 'authorized_actions',
) || [])[1];
// Safe-ish when the value is not present
#authorizedActions =
JSON.parse(
decodeURIComponent(this.#rawAuthorizedActions || '') || 'null',
) || {};
// =methods
/**
* Returns authorized actions for the specified field, if any are defined
* in the `authorized_actions` query. Returns undefined otherwise.
* @return {?string[]}
*/
authorizedActionsFor(field) {
if (this.#authorizedActions[field]) {
return structuredClone(this.#authorizedActions[field]);
}
}
}
export default new ToggledCapabilities();
+12
View File
@@ -0,0 +1,12 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo } from 'miragejs';
import Model from './base';
export default Model.extend({
scope: belongsTo(),
authMethod: belongsTo(),
});
+11
View File
@@ -0,0 +1,11 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo } from 'miragejs';
import Model from './base';
export default Model.extend({
scope: belongsTo(),
});
@@ -0,0 +1,11 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo } from 'miragejs';
import Model from './base';
export default Model.extend({
scope: belongsTo(),
});
+25
View File
@@ -0,0 +1,25 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Model } from 'miragejs';
export default Model.extend({
assignDefaults(defaultAttrs, attrs) {
Object.entries(defaultAttrs).map(([key, value]) => {
if (!attrs[key]) {
attrs[key] = value;
}
});
},
defaultAttrs: () => ({
version: 1,
authorized_actions: ['no-op', 'read', 'update', 'delete'],
created_time: new Date(),
}),
save() {
this.assignDefaults(this.defaultAttrs(), this.attrs);
return Model.prototype.save.apply(this, arguments);
},
});
@@ -0,0 +1,10 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo, Model } from 'miragejs';
export default Model.extend({
connection_recording: belongsTo(),
});
@@ -0,0 +1,11 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo, hasMany, Model } from 'miragejs';
export default Model.extend({
session_recording: belongsTo(),
channel_recordings: hasMany(),
});
@@ -0,0 +1,12 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo } from 'miragejs';
import Model from './base';
export default Model.extend({
scope: belongsTo(),
credentialStore: belongsTo(),
});
@@ -0,0 +1,11 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo } from 'miragejs';
import Model from './base';
export default Model.extend({
scope: belongsTo(),
});
@@ -0,0 +1,12 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo } from 'miragejs';
import Model from './base';
export default Model.extend({
scope: belongsTo(),
credentialStore: belongsTo(),
});
+12
View File
@@ -0,0 +1,12 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo, hasMany } from 'miragejs';
import Model from './base';
export default Model.extend({
scope: belongsTo(),
members: hasMany('user'),
});
@@ -0,0 +1,12 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo, hasMany } from 'miragejs';
import Model from './base';
export default Model.extend({
scope: belongsTo(),
hostSets: hasMany(),
});
+14
View File
@@ -0,0 +1,14 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo, hasMany } from 'miragejs';
import Model from './base';
export default Model.extend({
scope: belongsTo(),
target: belongsTo(),
hostCatalog: belongsTo(),
hosts: hasMany(),
});
+12
View File
@@ -0,0 +1,12 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo } from 'miragejs';
import Model from './base';
export default Model.extend({
scope: belongsTo(),
hostCatalog: belongsTo(),
});
@@ -0,0 +1,12 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo } from 'miragejs';
import Model from './base';
export default Model.extend({
scope: belongsTo(),
authMethod: belongsTo(),
});
+10
View File
@@ -0,0 +1,10 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo } from 'miragejs';
import Model from './base';
export default Model.extend({
scope: belongsTo(),
});
+14
View File
@@ -0,0 +1,14 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo, hasMany } from 'miragejs';
import Model from './base';
export default Model.extend({
scope: belongsTo(),
users: hasMany(),
groups: hasMany(),
managedGroups: hasMany(),
});
+8
View File
@@ -0,0 +1,8 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import Model from './base';
export default Model.extend({});
@@ -0,0 +1,11 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo, hasMany, Model } from 'miragejs';
export default Model.extend({
scope: belongsTo(),
connection_recordings: hasMany(),
});
+15
View File
@@ -0,0 +1,15 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo } from 'miragejs';
import Model from './base';
export default Model.extend({
scope: belongsTo(),
user: belongsTo(),
target: belongsTo(),
host: belongsTo(),
hostSet: belongsTo(),
});
@@ -0,0 +1,10 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo } from 'miragejs';
import Model from './base';
export default Model.extend({
scope: belongsTo(),
});
+12
View File
@@ -0,0 +1,12 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import Model from './base';
import { belongsTo, hasMany } from 'miragejs';
export default Model.extend({
scope: belongsTo(),
hostSets: hasMany(),
});
+12
View File
@@ -0,0 +1,12 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo, hasMany } from 'miragejs';
import Model from './base';
export default Model.extend({
scope: belongsTo(),
accounts: hasMany('account'),
});
+11
View File
@@ -0,0 +1,11 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { belongsTo } from 'miragejs';
import Model from './base';
export default Model.extend({
scope: belongsTo(),
});
@@ -0,0 +1,130 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { Response } from 'miragejs';
// Quick and dirty counter system to simulate the polling behavior
// of the API. If the OIDC flow is pending, the route returns 100.
// If complete it returns 200 with the token JSON.
let oidcAttemptCounter = 0;
const oidcRequiredAttempts = 3;
const commandHandlers = {
password: {
login: (payload, auth_method_id, account_id, scopeAttrs) => {
if (payload.attributes.login_name === 'error') {
return new Response(400);
} else {
return new Response(
200,
{},
{
attributes: {
scope: scopeAttrs,
id: 'token123',
token: 'thetokenstring',
account_id: account_id || 'authenticatedaccount',
user_id: 'authenticateduser',
auth_method_id: auth_method_id || 'authmethod123',
created_time: '',
updated_time: '',
last_used_time: '',
expiration_time: '',
},
},
);
}
},
},
/**
* OIDC authentication is a two-step process:
*
* 1. `authenticate:start` kicks of authentication by requesting some details
* from the Boundary server about the request, including the third-party
* URL to which to redirect the user.
* 2. `authenticate` accepts the token_id and state parameters in
* order to retrieve a Boundary token. This endpoint may be polled until
* the authentication flow is completed.
*/
oidc: {
start: () =>
new Response(
200,
{},
{
attributes: {
auth_url: 'https://www.duckduckgo.com',
token_id: 'token_1234',
},
},
),
token: (_, auth_method_id, account_id, scopeAttrs) => {
oidcAttemptCounter++;
if (oidcAttemptCounter < oidcRequiredAttempts) {
return new Response(202);
} else {
return new Response(
200,
{},
{
attributes: {
scope: scopeAttrs,
id: 'token123',
token: 'thetokenstring',
account_id: account_id || '1',
user_id: 'authenticateduser',
auth_method_id: auth_method_id || 'authmethod123',
created_time: '',
updated_time: '',
last_used_time: '',
expiration_time: '',
},
},
);
}
},
},
};
// Handles all custom methods on /auth-methods/:id route
export function authHandler({ scopes, authMethods, accounts }, request) {
const [, id, method] = request.params.id_method.match(
/(?<id>.[^:]*):(?<method>(.*))/,
);
const authMethod = authMethods.find(id);
if (method === 'authenticate') {
const payload = JSON.parse(request.requestBody);
const { command } = payload;
const scope = scopes.find(authMethod.scopeId);
const account = accounts.where({ authMethodId: authMethod.id })?.models[0];
const scopeAttrs = this.serialize(scopes.find(scope.id));
return commandHandlers[authMethod.type][command](
payload,
authMethod.id,
account?.id,
scopeAttrs,
);
}
// TODO: this handler doesn't really belong here, but we already route
// POST requests on existing auth methods to this route handler file under the
// assumption it would be for authentication. While authentication should
// still occur here, we should handle other custom methods in the
// mirage config and route here only for auth.
if (method === 'change-state') {
const attrs = this.normalizedRequestAttrs();
return authMethod.update({
attributes: {
state: attrs.attributes.state,
},
});
}
}
export function deauthHandler() {
return new Response(200);
}
@@ -0,0 +1,105 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
export function targetHandler({ targets }, { params: { idMethod } }) {
const attrs = this.normalizedRequestAttrs();
const id = idMethod.split(':')[0];
const method = idMethod.split(':')[1];
const target = targets.find(id);
const {
brokeredCredentialSourceIds: originalBrokeredCredentials = [],
injectedApplicationCredentialSourceIds: originalInjectedCredentials = [],
} = target;
const selectedBrokeredCredentials = attrs.brokeredCredentialSourceIds;
const selectedInjectedCredentials =
attrs.injectedApplicationCredentialSourceIds;
const selectedHostSourceIds = attrs.hostSourceIds;
const originHostSetIds = target.hostSetIds;
const updatedAttrs = {
version: attrs.version,
};
const addToSourcesList = (originalSourceIds, selectedSourceIds) => {
const sourceIds = new Set([...originalSourceIds, ...selectedSourceIds]);
return [...sourceIds];
};
const removeFromSourcesList = (originalSourceIds, selectedSourceIds) => {
const sourceIds = new Set(originalSourceIds);
for (const elem of selectedSourceIds) {
sourceIds.delete(elem);
}
return [...sourceIds];
};
// If adding host sources, push them into the array
if (method === 'add-host-sources') {
if (selectedHostSourceIds) {
const listOfHostSources = addToSourcesList(
originHostSetIds,
selectedHostSourceIds,
);
if (listOfHostSources?.length) {
updatedAttrs.hostSetIds = listOfHostSources;
}
}
}
// If deleting host sources, filter them out of the array
if (method === 'remove-host-sources') {
if (selectedHostSourceIds) {
const removedHostSources = removeFromSourcesList(
originHostSetIds,
selectedHostSourceIds,
);
updatedAttrs.hostSetIds = removedHostSources;
}
}
// If adding brokered or injected application cred sources, push them into the array
if (method === 'add-credential-sources') {
if (selectedBrokeredCredentials) {
const listOfBrokeredCredentialSources = addToSourcesList(
originalBrokeredCredentials,
selectedBrokeredCredentials,
);
if (listOfBrokeredCredentialSources?.length) {
updatedAttrs.brokeredCredentialSourceIds =
listOfBrokeredCredentialSources;
}
}
if (selectedInjectedCredentials) {
const listOfInjectedCredentialSources = addToSourcesList(
originalInjectedCredentials,
selectedInjectedCredentials,
);
if (listOfInjectedCredentialSources?.length) {
updatedAttrs.injectedApplicationCredentialSourceIds =
listOfInjectedCredentialSources;
}
}
}
// If deleting brokered or injected application cred sources, filter them out of the array
if (method === 'remove-credential-sources') {
if (selectedBrokeredCredentials) {
const removedCredentialSources = removeFromSourcesList(
originalBrokeredCredentials,
selectedBrokeredCredentials,
);
updatedAttrs.brokeredCredentialSourceIds = removedCredentialSources;
}
if (selectedInjectedCredentials) {
const removedCredentialSources = removeFromSourcesList(
originalInjectedCredentials,
selectedInjectedCredentials,
);
updatedAttrs.injectedApplicationCredentialSourceIds =
removedCredentialSources;
}
}
return target.update(updatedAttrs);
}
+174
View File
@@ -0,0 +1,174 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { TYPE_AUTH_METHOD_OIDC } from 'api/models/auth-method';
export default function (server) {
// Scope resources
// creates a global scope
const globalScope = server.create('scope', {
id: 'global',
type: 'global',
name: 'Global',
});
const orgScope = server.createList(
'scope',
3,
{
type: 'org',
scope: { id: globalScope.id, type: globalScope.type },
},
'withChildren',
)[0];
// Auth
const globalAuthMethods = server.createList(
'auth-method',
5,
{ scope: globalScope },
'withAccountsAndUsersAndManagedGroups',
);
const orgAuthMethods = server.createList(
'auth-method',
3,
{ scope: orgScope },
'withAccountsAndUsersAndManagedGroups',
);
// Authenticated user/account
const user = server.create('user', {
id: 'authenticateduser',
scope: globalScope,
});
const { id: accountID } = server.create('account', {
id: 'authenticatedaccount',
scope: globalScope,
authMethod: globalAuthMethods[0],
type: 'password',
});
user.update({ accountIds: [accountID] });
// Assign primary auth methods per scope
// TODO make this generic
globalScope.update({ primaryAuthMethodId: globalAuthMethods[0].id });
orgScope.update({ primaryAuthMethodId: orgAuthMethods[1].id });
// Groups and Users
server.createList('group', 1, { scope: globalScope }, 'withMembers');
server.createList('group', 5, { scope: orgScope }, 'withMembers');
// Role
const globalScopeRoles = server.createList(
'role',
1,
{ scope: globalScope },
'withPrincipals',
);
//create managed groups for role/principals in globalScope
globalScopeRoles.forEach((role) => {
const { scope } = role;
const oidcAuthMethod = globalAuthMethods.filter(
(auth) => auth.type === TYPE_AUTH_METHOD_OIDC,
)[0];
const { type } = oidcAuthMethod;
const managedGroups = server.createList('managed-group', 2, {
scope,
authMethodId: oidcAuthMethod.id,
type,
});
role.update({ managedGroups });
});
const orgScopeRoles = server.createList(
'role',
5,
{ scope: orgScope },
'withPrincipals',
);
//create managed groups for role/principals in orgScope
orgScopeRoles.forEach((role) => {
const { scope } = role;
const oidcAuthMethod = orgAuthMethods.filter(
(auth) => auth.type === TYPE_AUTH_METHOD_OIDC,
)[0];
const { type } = oidcAuthMethod;
const managedGroups = server.createList('managed-group', 2, {
scope,
authMethodId: oidcAuthMethod.id,
type,
});
role.update({ managedGroups });
});
// Storage Buckets
server.createList('storage-bucket', 3, { scope: globalScope });
server.createList('storage-bucket', 2, { scope: orgScope });
// Storage Policies
server.createList('policy', 3, { scope: globalScope });
server.createList('policy', 3, { scope: orgScope });
// Other resources
server.schema.scopes.where({ type: 'project' }).models.forEach((scope) => {
server.createList(
'host-catalog',
2,
{ scope, type: 'static' },
'withChildren',
);
server.createList(
'host-catalog',
8,
{ scope, type: 'plugin' },
'withChildren',
);
server.createList('credential-store', 5, { scope }, 'withAssociations');
server.createList('target', 4, { scope }, 'withAssociations');
server.create('target', { scope, address: '0.0.0.0' });
// Sessions have target data. Create it after targets.
server.createList('session', 4, { scope }, 'withAssociations');
// Also create sessions for "authenticateduser" for Desktop,
// which filters user_id by the currently authenticated user.
server.createList('session', 1, { scope, user }, 'withAssociations');
// Create IAM resources for project
server.createList('group', 3, { scope });
server.createList('role', 3, { scope });
});
// Aliases
const aliasDestinationTarget = server.schema.targets.all().models[0];
const { id: destination_id } = aliasDestinationTarget;
const aliases = server.createList('alias', 4, {
scope: globalScope,
destination_id,
});
aliasDestinationTarget.update({
aliases: aliases.map((alias) => ({ id: alias.id, value: alias.value })),
});
// Workers
server.createList('worker', 3, { scope: globalScope });
// Session Recordings
server.createList(
'session-recording',
3,
{ scope: globalScope },
'withConnectionAndChannels',
'withExistingUserAndTarget',
);
server.create(
'session-recording',
{ scope: globalScope },
'withConnectionAndChannels',
'withNonExistingUserAndTarget',
);
server.createList(
'session-recording',
2,
{ scope: orgScope },
'withConnectionAndChannels',
'withExistingUserAndTarget',
);
}
+296
View File
@@ -0,0 +1,296 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { faker } from '@faker-js/faker';
export default function initializeMockIPC(server, config) {
const isTesting = config.environment === 'test';
/**
* We strive to make this application runnable in a regular web browser, since
* it is a convenient environment for development and testing. But only an
* Electron environment has true IPC. Outside of Electron or with mirage,
* we mock the handling of the message-based IPC requests originating from the
* renderer (the Ember app).
*/
class MockIPC {
async invoke(method, payload) {
return await this[method](payload);
}
clusterUrl = null;
checkOS() {
return {
isLinux: false,
isMac: true,
isWindows: false,
};
}
getClusterUrl() {
return this.clusterUrl;
}
setClusterUrl(clusterUrl) {
this.clusterUrl = clusterUrl;
return this.clusterUrl;
}
resetClusterUrl() {
this.clusterUrl = null;
}
openExternal(href) {
window.open(href);
}
cliExists() {
return true;
}
/**
* Creates a new session instance associated with the specified target.
* @param connect_details
* @param
*/
connect({ target_id }) {
const target = server.schema.targets.find(target_id);
const { scope, type } = target;
const newSession = server.schema.sessions.create({
scope,
target,
userId: 'authenticateduser',
type,
created_time: new Date().toISOString(),
expiration_time: faker.date.soon(),
});
const decoded = {
key1: 'value 1',
key2: true,
key3: 'test',
};
const raw = btoa(JSON.stringify(decoded));
return {
address: window.location.hostname,
port: faker.internet.port(),
protocol: type,
session_id: newSession.id,
credentials: [
{
credential_source: {
id: 'clvlt_4cvscMTl0N',
name: 'Credential Library 1',
description: 'Source Description',
credential_store_id: 'csvlt_Q1HFGt7Jpm',
type: 'vault',
},
secret: {
raw: btoa('just-a-random-string'),
},
},
{
credential_source: {
id: 'clvlt_4cvscMTl0N',
name: 'Credential Library 0',
description: 'Source Description',
credential_store_id: 'csvlt_Q1HFGt7Jpm',
type: 'vault-generic',
},
secret: {
raw: 'eyJhcnJheSI6WyJvbmUiLCJ0d28iLCJ0aHJlZSIsIm9uZSIsInR3byIsInRocmVlIiwib25lIiwidHdvIiwidGhyZWUiLCJvbmUiLCJ0d28iLCJ0aHJlZSJdLCJuZXN0ZWQiOnsiYm9vbCI6dHJ1ZSwibG9uZyI6IjEyMjM1MzQ1NmFzZWRmYTQzd3J0ZjIzNGYyM2FzZGdmYXNkZnJnYXdzZWZhd3NlZnNkZjQiLCJzZWNlcmV0Ijoic28gbmVzdGVkIn0sInRlc3QiOiJwaHJhc2UifQ',
decoded: {
data: {
backslash: 'password\\withslash\\',
backslash1: 'password\\fwithslash\\f',
password: '123',
username: 'test',
email: {
address: 'test.com',
},
},
metadata: {
created_time: '2024-04-12T18:38:36.226715555Z',
custom_metadata: null,
deletion_time: '',
destroyed: false,
version: 8,
},
},
},
},
{
credential_source: {
id: 'credjson_7cKBbBEkC3',
credential_store_id: 'csst_yzlsot2pum',
type: 'static',
credential_type: 'json',
},
secret: {
raw: 'eyJhcnJheSI6WyJvbmUiLCJ0d28iLCJ0aHJlZSIsIm9uZSIsInR3byIsInRocmVlIiwib25lIiwidHdvIiwidGhyZWUiLCJvbmUiLCJ0d28iLCJ0aHJlZSJdLCJuZXN0ZWQiOnsiYm9vbCI6dHJ1ZSwibG9uZyI6IjEyMjM1MzQ1NmFzZWRmYTQzd3J0ZjIzNGYyM2FzZGdmYXNkZnJnYXdzZWZhd3NlZnNkZjQiLCJzZWNlcmV0Ijoic28gbmVzdGVkIn0sInRlc3QiOiJwaHJhc2UifQ==',
decoded: {
secret_key: 'QWERTYUIOP',
secret_access_key: 'QWERT.YUIOP234567890',
nested_secret: {
session_token: 'ZXCVBNMLKJHGFDSAQWERTYUIOP0987654321',
complex_nest: {
blackslash: 'password\\withslash\\',
hash: 'qazxswedcvfrtgbnjhyujm.1234567890',
},
},
},
},
credential: {
secret_key: 'QWERTYUIOP',
secret_access_key: 'QWERT.YUIOP234567890',
nested_secret: {
session_token: 'ZXCVBNMLKJHGFDSAQWERTYUIOP0987654321',
complex_nest: {
hash: 'qazxswedcvfrtgbnjhyujm.1234567890',
},
},
},
},
{
credential_source: {
id: 'clvlt_4cvscMTl0N',
name: 'Credential Library 2',
description: 'Source Description',
credential_store_id: 'csvlt_Q1HFGt7Jpm',
type: 'vault',
},
secret: {
raw,
decoded,
},
},
{
credential_source: {
id: 'clvlt_9KWscxpcY7',
name: 'Credential Library 3',
description: 'Source Description',
credential_store_id: 'csvlt_Q1HFGt7Jpm',
type: 'vault',
},
secret: {
raw: btoa('just-a-random-string'),
},
},
{
credential_source: {
id: 'clvlt_9KWscxpcY7',
name: 'Credential Library 4',
description: 'Source Description',
credential_store_id: 'csvlt_Q1HFGt7Jpm',
type: 'vault',
},
secret: {
raw,
decoded,
},
},
],
};
}
/**
* Check for window chrome on MacOS
*/
hasMacOSChrome() {
return true;
}
/**
* Check for OS chrome state
*/
showWindowActions() {
if (navigator.userAgent.includes('Mac')) {
return false;
}
return true;
}
/**
* Do nothing when attempting to minimize, toggle fullscreen,
* and close a browser window
* * Do nothing when canceling a session invokes the stop method
*/
minimizeWindow() {}
closeWindow() {}
toggleFullscreenWindow() {}
stop() {}
checkCommand() {
return faker.system.filePath();
}
addTokenToDaemons() {}
searchCacheDaemon() {}
isCacheDaemonRunning() {
return false;
}
isClientAgentRunning() {
return false;
}
cacheDaemonStatus() {
return { version: 'Boundary v1.0.0' };
}
getCliVersion() {
return { versionNumber: '1.0.0' };
}
getDesktopVersion() {
return { desktopVersion: '1.0.0' };
}
getLogLevel() {
return 'info';
}
getLogPath() {
return '~/.config/Boundary/logs/desktop-client.log';
}
setLogLevel() {}
clientAgentStatus() {
return { version: '0.0.1-dev', status: 'running' };
}
pauseClientAgent() {}
resumeClientAgent() {}
hasRunningSessions() {}
stopAll() {}
getRdpClients() {
return ['windows-app', 'none', 'mstsc'];
}
getPreferredRdpClient() {
return 'windows-app';
}
getRecommendedRdpClient() {
return {
name: 'windows-app',
link: 'https://apps.apple.com/us/app/windows-app/id1295203466',
};
}
setPreferredRdpClient() {}
launchRdpClient() {}
}
/**
* Establishes a mock IPC handler, which mimics the behavior of the Electron
* main process, and routes messages to it in a way similar to our
* preload.js script.
*
* Initializes mock IPC only in a non-testing context and when mirage is turned on.
* We mock certain functions even in electron (e.g. hasMacOSChrome) when running
* locally which will force a certain appearance regardless of platform
*/
if (config.mirage?.enabled && !isTesting) {
const mockIPC = new MockIPC();
window.addEventListener('message', async function (event) {
if (event.origin !== window.location.origin) return;
const { method, payload } = event?.data ?? {};
if (method) {
const response = await mockIPC.invoke(method, payload);
event.ports[0].postMessage(response);
}
});
}
}
@@ -0,0 +1,20 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
modelName: 'account',
_hashForModel(model) {
const json = ApplicationSerializer.prototype._hashForModel.apply(
this,
arguments,
);
json.auth_method_id = model.authMethodId;
delete json.attributes?.password;
return json;
},
});
@@ -0,0 +1,10 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
modelName: 'alias',
});
@@ -0,0 +1,87 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { RestSerializer } from 'miragejs';
import { underscore } from '@ember/string';
import { isArray } from '@ember/array';
/**
* Manages serialization/normalization of data into and out of the mock API.
*/
export default RestSerializer.extend({
// =attributes
/**
* False to disable automatically rooting payloads under a key.
* This is handled by the `serialize()` method, see below.
* @type {boolean}
*/
root: false,
/**
* @type {boolean}
*/
embed: true,
// =methods
/**
* Generates an underscored key for the attribute.
* @param {string} attr
* @return {string}
*/
keyForAttribute(attr) {
return underscore(attr);
},
keyForRelationshipId(relationshipName) {
return `${this._container.inflector.singularize(
underscore(relationshipName),
)}_id`;
},
/**
* Serialize items into a root key `items`.
* @return {object}
*/
serialize() {
let json = RestSerializer.prototype.serialize.apply(this, arguments);
// If array, root it under a standard `items` key
if (isArray(json)) json = { items: json };
return json;
},
/**
* Serialize scope into the payload, if set.
* @param {object} model
* @return {object}
*/
_hashForModel(model) {
const json = RestSerializer.prototype._hashForModel.apply(this, arguments);
if (model.scope) {
json.scope = {
id: model.scope.id,
type: model.scope.type,
};
}
return json;
},
/**
* Adds a root to the payload, since `root: false` doesn't appear to affect
* the normalize method.
* @param {object} json
* @return {object}
*/
normalize(json) {
let newJSON = json;
if (this.modelName) {
newJSON = {};
newJSON[this.modelName] = json;
}
const value = RestSerializer.prototype.normalize.apply(this, [newJSON]);
return value;
},
});
@@ -0,0 +1,26 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
modelName: 'auth-method',
_hashForModel(model) {
const json = ApplicationSerializer.prototype._hashForModel.apply(
this,
arguments,
);
// If the scope marks this auth method as primary, designate it
// as such with a read-only boolean field (this is how the API behaves).
const isPrimary =
model.scope && model.scope.primaryAuthMethodId === model.id;
json.is_primary = isPrimary;
delete json.attributes?.client_certificate_key;
delete json.attributes?.bind_password;
delete json.attributes?.client_secret;
return json;
},
});
@@ -0,0 +1,10 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
modelName: 'channel-recording',
});
@@ -0,0 +1,25 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
import ChannelSerializer from './channel-recording';
export default ApplicationSerializer.extend({
modelName: 'connection-recording',
_hashForModel(model) {
const json = ApplicationSerializer.prototype._hashForModel.apply(
this,
arguments,
);
const { channelRecordings } = this.schema;
if (model.channelRecordingIds) {
json.channel_recordings = model.channelRecordingIds.map((id) => {
const channel = channelRecordings.find(id);
return ChannelSerializer.prototype._hashForModel.apply(this, [channel]);
});
}
return json;
},
});
@@ -0,0 +1,30 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
modelName: 'credential-library',
_hashForModel(model) {
const json = ApplicationSerializer.prototype._hashForModel.apply(
this,
arguments,
);
json.credential_store_id = model.credentialStoreId;
// BE API only updates these fields if there's an update mask, otherwise they remain unmodified
// but we don't need this flow in our mocks, so we are deleting fields with null value
if (json.credential_mapping_overrides) {
Object.keys(json.credential_mapping_overrides).forEach((key) => {
if (json.credential_mapping_overrides[key] === null) {
delete json.credential_mapping_overrides[key];
}
});
}
return json;
},
});
@@ -0,0 +1,22 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
modelName: 'credential-store',
_hashForModel(/* model */) {
const json = ApplicationSerializer.prototype._hashForModel.apply(
this,
arguments,
);
// in a real API, the following fields are set-only and returned in
// associated HMAC fields
delete json.attributes?.token;
delete json.attributes?.client_certificate_key;
return json;
},
});
@@ -0,0 +1,26 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
modelName: 'credential',
_hashForModel(model) {
const json = ApplicationSerializer.prototype._hashForModel.apply(
this,
arguments,
);
json.credential_store_id = model.credentialStoreId;
// in a real API, the password, private_key, and private_key_passphrase
// fields are not returned so we delete them from the response
delete json.attributes?.password;
delete json.attributes?.private_key;
delete json.attributes?.private_key_passphrase;
delete json.attributes?.json_object;
return json;
},
});
@@ -0,0 +1,26 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
import { underscore } from '@ember/string';
export default ApplicationSerializer.extend({
modelName: 'group',
keyForRelationshipIds(relationshipName) {
return `${this._container.inflector.singularize(
underscore(relationshipName),
)}_ids`;
},
_hashForModel(model) {
const json = ApplicationSerializer.prototype._hashForModel.apply(
this,
arguments,
);
if (model.memberIds.length) json.member_ids = model.memberIds;
return json;
},
});
@@ -0,0 +1,10 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
modelName: 'host-catalog',
});
@@ -0,0 +1,29 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
import { underscore } from '@ember/string';
export default ApplicationSerializer.extend({
modelName: 'host-set',
keyForRelationshipIds(relationshipName) {
return `${this._container.inflector.singularize(
underscore(relationshipName),
)}_ids`;
},
_hashForModel(model) {
const json = ApplicationSerializer.prototype._hashForModel.apply(
this,
arguments,
);
json.host_catalog_id = model.hostCatalogId;
if (model.hostIds?.length) {
json.host_ids = model.hostIds;
}
return json;
},
});
+19
View File
@@ -0,0 +1,19 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
modelName: 'host',
_hashForModel(model) {
const json = ApplicationSerializer.prototype._hashForModel.apply(
this,
arguments,
);
json.host_catalog_id = model.hostCatalogId;
return json;
},
});
@@ -0,0 +1,19 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
modelName: 'managed-group',
_hashForModel(model) {
const json = ApplicationSerializer.prototype._hashForModel.apply(
this,
arguments,
);
json.auth_method_id = model.authMethodId;
return json;
},
});
@@ -0,0 +1,10 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
modelName: 'policy',
});
+43
View File
@@ -0,0 +1,43 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
modelName: 'role',
_hashForModel(model) {
const json = ApplicationSerializer.prototype._hashForModel.apply(
this,
arguments,
);
const serializedUsers = model.users.models.map((user) => ({
scope_id: model.scope.id,
id: user.id,
type: 'user',
}));
const serializedGroups = model.groups.models.map((group) => ({
scope_id: model.scope.id,
id: group.id,
type: 'group',
}));
const serializedManagedGroups = model.managedGroups.models.map(
(managedGroup) => ({
scope_id: model.scope.id,
id: managedGroup.id,
type: 'managed group',
}),
);
json.principals = [
...serializedUsers,
...serializedGroups,
...serializedManagedGroups,
];
if (!json.principals.length) delete json.principals;
return json;
},
});
@@ -0,0 +1,10 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
modelName: 'scope',
});
@@ -0,0 +1,46 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
import ConnectionRecordingSerializer from './connection-recording';
import ScopeSerializer from './scope';
export default ApplicationSerializer.extend({
modelName: 'session-recording',
_hashForModel(model) {
const json = ApplicationSerializer.prototype._hashForModel.apply(
this,
arguments,
);
const { connectionRecordings, scopes } = this.schema;
// Manually map our models to the actual embedded json representation
if (model.connectionRecordingIds) {
json.connection_recordings = model.connectionRecordingIds.map((id) => {
const connection = connectionRecordings.find(id);
return ConnectionRecordingSerializer.prototype._hashForModel.apply(
this,
[connection],
);
});
}
const userScopeId = model.create_time_values?.user?.scopeId;
if (userScopeId) {
const scope = scopes.find(userScopeId);
json.create_time_values.user.scope =
ScopeSerializer.prototype._hashForModel.apply(this, [scope]);
}
const targetScopeId = model.create_time_values?.target?.scopeId;
if (targetScopeId) {
const scope = scopes.find(targetScopeId);
json.create_time_values.target.scope =
ScopeSerializer.prototype._hashForModel.apply(this, [scope]);
}
delete json.create_time_values?.target?.storage_bucket_id;
json.session_id = model.sessionId;
return json;
},
});
@@ -0,0 +1,29 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
import { underscore } from '@ember/string';
export default ApplicationSerializer.extend({
modelName: 'session',
keyForRelationshipId(relationshipName) {
return `${this._container.inflector.singularize(
underscore(relationshipName),
)}_id`;
},
_hashForModel(model) {
const json = ApplicationSerializer.prototype._hashForModel.apply(
this,
arguments,
);
json.user_id = model.userId;
json.target_id = model.targetId;
json.host_set_id = model.hostSetId;
json.host_id = model.hostId;
return json;
},
});
@@ -0,0 +1,9 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
modelName: 'storage-bucket',
});
@@ -0,0 +1,50 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
import { underscore } from '@ember/string';
export default ApplicationSerializer.extend({
modelName: 'target',
keyForRelationshipIds(relationshipName) {
return `${this._container.inflector.singularize(
underscore(relationshipName),
)}_ids`;
},
_hashForModel(model) {
const json = ApplicationSerializer.prototype._hashForModel.apply(
this,
arguments,
);
const { hostSets, aliases } = this.schema;
if (model.hostSetIds?.length) {
json.host_sources = model.hostSetIds.map((host_set_id) => {
const hostSet = hostSets.find(host_set_id);
const host_catalog_id = hostSet?.hostCatalog?.id;
return { id: host_set_id, host_catalog_id };
});
}
// After we clear an associated alias, we try to mimic the BE logic
// by manually filter out the alias id from the target model
const associatedAliases = aliases
.all()
.models.reduce((arr, { destinationId, id }) => {
// We iterate through the aliases list and
// find the cleared alias by looking for camelCase destinationId since there is no other way to find the impacted alias
if (destinationId !== null) arr.push(id);
return arr;
}, []);
json.aliases = model.aliases?.filter((alias) =>
associatedAliases.includes(alias.id),
);
return json;
},
});
+26
View File
@@ -0,0 +1,26 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
import { underscore } from '@ember/string';
export default ApplicationSerializer.extend({
modelName: 'user',
keyForRelationshipIds(relationshipName) {
return `${this._container.inflector.singularize(
underscore(relationshipName),
)}_ids`;
},
_hashForModel(model) {
const json = ApplicationSerializer.prototype._hashForModel.apply(
this,
arguments,
);
if (model.accountIds.length) json.account_ids = model.accountIds;
return json;
},
});
Loaded 100 of 191 files, more files were not shown because too many files have changed in this diff. Show more