remove Vault Enterprise Control Groups from UI (#2388)
Some checks are pending
CI / Setup (push) Waiting to run
CI / Verify doc-ui only PRs (push) Waiting to run
CI / Run Go tests (push) Blocked by required conditions
CI / Run Go tests tagged with testonly (push) Blocked by required conditions
CI / Run Go tests with data race detection (push) Blocked by required conditions
CI / Test UI (push) Blocked by required conditions
CI / tests-completed (push) Blocked by required conditions
Run linters / Vulnerable dependencies (push) Waiting to run
Run linters / Code checks (push) Waiting to run
Run linters / Semgrep (push) Waiting to run
Run linters / Go mod checks (push) Waiting to run
Run linters / EL8 Go build checks (push) Waiting to run
Run linters / Protobuf checks (push) Waiting to run
CodeQL Advanced / Analyze (go) (push) Waiting to run
Deploy docs / deploy (push) Waiting to run
Go Dependency Submission / go-dependency-submission (push) Waiting to run
Mirror Repo / mirror (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run

Signed-off-by: Jan Martens <jan@martens.eu.org>
This commit is contained in:
Jan Martens
2026-02-06 21:24:07 +01:00
committed by GitHub
parent 9bf86e34ec
commit 62ef8a71b3
38 changed files with 12 additions and 1418 deletions

View File

@@ -18,7 +18,6 @@ const { POLLING_URLS, NAMESPACE_ROOT_URLS } = APP;
export default RESTAdapter.extend({
auth: service(),
namespaceService: service('namespace'),
controlGroup: service(),
flashMessages: service(),
@@ -64,31 +63,12 @@ export default RESTAdapter.extend({
},
ajax(intendedUrl, method, passedOptions = {}) {
let url = intendedUrl;
let type = method;
let options = passedOptions;
const controlGroup = this.controlGroup;
const controlGroupToken = controlGroup.tokenForUrl(url);
// if we have a Control Group token that matches the intendedUrl,
// then we want to unwrap it and return the unwrapped response as
// if it were the initial request
// To do this, we rewrite the function args
if (controlGroupToken) {
url = '/v1/sys/wrapping/unwrap';
type = 'POST';
options = {
clientToken: controlGroupToken.token,
data: {
token: controlGroupToken.token,
},
};
}
const url = intendedUrl;
const type = method;
const options = passedOptions;
const opts = this._preRequest(url, options);
return this._super(url, type, opts).then((...args) => {
if (controlGroupToken) {
controlGroup.deleteControlGroupToken(controlGroupToken.accessor);
}
const [resp] = args;
if (resp && resp.warnings) {
const flash = this.flashMessages;
@@ -96,7 +76,7 @@ export default RESTAdapter.extend({
flash.info(message);
});
}
return controlGroup.checkForControlGroup(args, resp, options.wrapTTL);
return RSVP.resolve(...args);
});
},

View File

@@ -1,24 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import ApplicationAdapter from './application';
export default ApplicationAdapter.extend({
pathForType() {
return 'config/control-group';
},
urlForDeleteRecord(id, modelName) {
return this.buildURL(modelName);
},
urlForFindRecord(id, modelName) {
return this.buildURL(modelName);
},
urlForUpdateRecord(id, modelName) {
return this.buildURL(modelName);
},
});

View File

@@ -1,29 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import ApplicationAdapter from './application';
export default ApplicationAdapter.extend({
pathForType() {
return 'control-group';
},
findRecord(store, type, id) {
const baseUrl = this.buildURL(type.modelName);
return this.ajax(`${baseUrl}/request`, 'POST', {
data: {
accessor: id,
},
}).then((response) => {
response.id = id;
return response;
});
},
urlForUpdateRecord(id, modelName) {
const base = this.buildURL(modelName);
return `${base}/authorize`;
},
});

View File

@@ -5,7 +5,6 @@
import { allSettled } from 'rsvp';
import ApplicationAdapter from '../application';
import ControlGroupError from 'vault/lib/control-group-error';
export default ApplicationAdapter.extend({
namespace: 'v1',
@@ -35,9 +34,6 @@ export default ApplicationAdapter.extend({
([staticResp, dynamicResp]) => {
if (staticResp.state === 'rejected' && dynamicResp.state === 'rejected') {
let reason = staticResp.reason;
if (dynamicResp.reason instanceof ControlGroupError) {
throw dynamicResp.reason;
}
if (reason?.httpStatus < dynamicResp.reason?.httpStatus) {
reason = dynamicResp.reason;
}

View File

@@ -5,7 +5,6 @@
import { assign } from '@ember/polyfills';
import { assert } from '@ember/debug';
import ControlGroupError from 'vault/lib/control-group-error';
import ApplicationAdapter from '../application';
import { allSettled } from 'rsvp';
import { addToArray } from 'vault/helpers/add-to-array';
@@ -78,9 +77,6 @@ export default ApplicationAdapter.extend({
([staticResp, dynamicResp]) => {
if (staticResp.state === 'rejected' && dynamicResp.state === 'rejected') {
let reason = staticResp.reason;
if (dynamicResp.reason instanceof ControlGroupError) {
throw dynamicResp.reason;
}
if (reason?.httpStatus < dynamicResp.reason?.httpStatus) {
reason = dynamicResp.reason;
}

View File

@@ -5,7 +5,6 @@
import ApplicationAdapter from '../application';
import { encodePath } from 'vault/utils/path-encoding-helpers';
import ControlGroupError from '../../lib/control-group-error';
import { inject as service } from '@ember/service';
function pickKeys(obj, picklist) {
@@ -118,19 +117,12 @@ export default class KeymgmtKeyAdapter extends ApplicationAdapter {
getDistribution(backend, kms, key) {
const url = `${this.buildURL()}/${backend}/kms/${kms}/key/${key}`;
return this.ajax(url, 'GET')
.then((res) => {
return {
...res.data,
purposeArray: res.data.purpose.split(','),
};
})
.catch((e) => {
if (e instanceof ControlGroupError) {
throw e;
}
return null;
});
return this.ajax(url, 'GET').then((res) => {
return {
...res.data,
purposeArray: res.data.purpose.split(','),
};
});
}
async queryRecord(store, type, query) {

View File

@@ -45,8 +45,6 @@ export default ApplicationAdapter.extend({
if (!query.path || !mountModel) {
throw error;
}
// control groups will throw a 403 permission denied error. If this happens return the mountModel
// error is handled on routing
}
return mountModel;
},

View File

@@ -36,7 +36,7 @@ export default ApplicationAdapter.extend({
findRecord() {
return this._super(...arguments).catch((errorOrModel) => {
// if the response is a real 404 or if the secret is gated by a control group this will be an error,
// if the response is a real 404 this will be an error,
// otherwise the response will be the body of a deleted / destroyed version
if (errorOrModel instanceof AdapterError) {
throw errorOrModel;

View File

@@ -10,7 +10,6 @@ import { getOwner } from '@ember/application';
import { schedule } from '@ember/runloop';
import { camelize } from '@ember/string';
import { task } from 'ember-concurrency';
import ControlGroupError from 'vault/lib/control-group-error';
import {
parseCommand,
logFromResponse,
@@ -24,7 +23,6 @@ import {
export default Component.extend({
console: service(),
router: service(),
controlGroup: service(),
store: service(),
'data-test-component': 'console/ui-panel',
attributeBindings: ['data-test-component'],
@@ -87,9 +85,6 @@ export default Component.extend({
const resp = yield service[camelize(method)].call(service, path, data, flags);
this.logAndOutput(command, logFromResponse(resp, path, method, flags));
} catch (error) {
if (error instanceof ControlGroupError) {
return this.logAndOutput(command, this.controlGroup.logFromError(error));
}
this.logAndOutput(command, logFromError(error, path, method));
}
}),

View File

@@ -1,40 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { task } from 'ember-concurrency';
export default Component.extend({
router: service(),
controlGroup: service(),
store: service(),
// public attrs
model: null,
controlGroupResponse: null,
//internal state
error: null,
unwrapData: null,
unwrap: task(function* (token) {
const adapter = this.store.adapterFor('tools');
this.set('error', null);
try {
const response = yield adapter.toolAction('unwrap', null, { clientToken: token });
this.set('unwrapData', response.auth || response.data);
this.controlGroup.deleteControlGroupToken(this.model.id);
} catch (e) {
this.set('error', `Token unwrap failed: ${e.errors[0]}`);
}
}).drop(),
markAndNavigate: task(function* () {
this.controlGroup.markTokenForUnwrap(this.model.id);
const { url } = this.controlGroupResponse.uiParams;
yield this.router.transitionTo(url);
}).drop(),
});

View File

@@ -1,96 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { inject as service } from '@ember/service';
import { alias, or } from '@ember/object/computed';
import Component from '@ember/component';
import { computed } from '@ember/object';
import { task } from 'ember-concurrency';
export default Component.extend({
tagName: '',
auth: service(),
controlGroup: service(),
// public API
model: null,
didReceiveAttrs() {
this._super(...arguments);
const accessor = this.model.id;
const data = this.controlGroup.wrapInfoForAccessor(accessor);
this.set('controlGroupResponse', data);
},
currentUserEntityId: alias('auth.authData.entity_id'),
currentUserIsRequesting: computed('currentUserEntityId', 'model.requestEntity.id', function () {
if (!this.model.requestEntity) return false;
return this.currentUserEntityId === this.model.requestEntity.id;
}),
currentUserHasAuthorized: computed('currentUserEntityId', 'model.authorizations.@each.id', function () {
const authorizations = this.model.authorizations || [];
return Boolean(authorizations.findBy('id', this.currentUserEntityId));
}),
isSuccess: or('currentUserHasAuthorized', 'model.approved'),
requestorName: computed('currentUserIsRequesting', 'model.requestEntity', function () {
const entity = this.model.requestEntity;
if (this.currentUserIsRequesting) {
return 'You';
}
if (entity && entity.name) {
return entity.name;
}
return 'Someone';
}),
bannerPrefix: computed('model.approved', 'currentUserHasAuthorized', function () {
if (this.currentUserHasAuthorized) {
return 'Thanks!';
}
if (this.model.approved) {
return 'Success!';
}
return 'Locked';
}),
bannerText: computed('model.approved', 'currentUserIsRequesting', 'currentUserHasAuthorized', function () {
const isApproved = this.model.approved;
const { currentUserHasAuthorized, currentUserIsRequesting } = this;
if (currentUserHasAuthorized) {
return 'You have given authorization';
}
if (currentUserIsRequesting && isApproved) {
return 'You have been given authorization';
}
if (isApproved) {
return 'This Control Group has been authorized';
}
if (currentUserIsRequesting) {
return 'The path you requested is locked by a Control Group';
}
return 'Someone is requesting access to a path locked by a Control Group';
}),
refresh: task(function* () {
try {
yield this.model.reload();
} catch (e) {
this.set('errors', e);
}
}).drop(),
authorize: task(function* () {
try {
yield this.model.save();
yield this.refresh.perform();
} catch (e) {
this.set('errors', e);
}
}).drop(),
});

View File

@@ -31,7 +31,6 @@ const MODEL_TYPES = {
};
export default Component.extend({
controlGroup: service(),
store: service(),
router: service(),
// set on the component
@@ -102,12 +101,6 @@ export default Component.extend({
model.set('hasGenerated', true);
})
.catch((error) => {
// Handle control group AdapterError
if (error.message === 'Control Group encountered') {
this.controlGroup.saveTokenFromError(error);
const err = this.controlGroup.logFromError(error);
error.errors = [err.content];
}
throw error;
})
.finally(() => {

View File

@@ -31,7 +31,6 @@
*/
import Component from '@glimmer/component';
import ControlGroupError from 'vault/lib/control-group-error';
import Ember from 'ember';
import keys from 'vault/lib/keycodes';
import { action, set } from '@ember/object';
@@ -52,7 +51,6 @@ export default class SecretCreateOrUpdate extends Component {
@tracked validationErrorCount = 0;
@tracked validationMessages = null;
@service controlGroup;
@service flashMessages;
@service router;
@service store;
@@ -163,11 +161,6 @@ export default class SecretCreateOrUpdate extends Component {
}
})
.catch((error) => {
if (error instanceof ControlGroupError) {
const errorMessage = this.controlGroup.logFromError(error);
this.error = errorMessage.content;
this.controlGroup.saveTokenFromError(error);
}
throw error;
});
}
@@ -239,7 +232,6 @@ export default class SecretCreateOrUpdate extends Component {
const secretPath = type === 'create' ? this.args.modelForData.path : this.args.model.id;
this.persistKey(() => {
// Show flash message in case there's a control group on read
this.flashMessages.success(
`Secret ${secretPath} ${type === 'create' ? 'created' : 'updated'} successfully.`
);

View File

@@ -1,16 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Controller from '@ember/controller';
export default Controller.extend({
actions: {
onSave({ saveType }) {
if (saveType === 'destroyRecord') {
this.send('reload');
}
},
},
});

View File

@@ -1,21 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import AdapterError from '@ember-data/adapter/error';
export default class ControlGroupError extends AdapterError {
constructor(wrapInfo) {
const { accessor, creation_path, creation_time, token, ttl } = wrapInfo;
super();
this.message = 'Control Group encountered';
// add items from the wrapInfo object to the error
this.token = token;
this.accessor = accessor;
this.creation_path = creation_path;
this.creation_time = creation_time;
this.ttl = ttl;
}
}

View File

@@ -1,25 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Model, { attr } from '@ember-data/model';
import { alias } from '@ember/object/computed';
import { computed } from '@ember/object';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
export default Model.extend({
fields: computed(function () {
return expandAttributeMeta(this, ['maxTtl']);
}),
configurePath: lazyCapabilities(apiPath`sys/config/control-group`),
canDelete: alias('configurePath.canDelete'),
maxTtl: attr({
defaultValue: 0,
editType: 'ttl',
label: 'Maximum TTL',
}),
});

View File

@@ -1,20 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Model, { hasMany, belongsTo, attr } from '@ember-data/model';
import { alias } from '@ember/object/computed';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
export default Model.extend({
approved: attr('boolean'),
requestPath: attr('string'),
requestEntity: belongsTo('identity/entity', { async: false }),
authorizations: hasMany('identity/entity', { async: false }),
authorizePath: lazyCapabilities(apiPath`sys/control-group/authorize`),
canAuthorize: alias('authorizePath.canUpdate'),
configurePath: lazyCapabilities(apiPath`sys/config/control-group`),
canConfigure: alias('configurePath.canUpdate'),
});

View File

@@ -100,9 +100,6 @@ Router.map(function () {
this.route('show', { path: '/:item_alias_id/:section' });
});
});
// this.route('control-groups');
// this.route('control-groups-configure', { path: '/control-groups/configure' });
// this.route('control-group-accessor', { path: '/control-groups/:accessor' });
this.route('namespaces', function () {
this.route('index', { path: '/' });
this.route('create');

View File

@@ -5,10 +5,8 @@
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import ControlGroupError from 'vault/lib/control-group-error';
export default Route.extend({
controlGroup: service(),
routing: service('router'),
namespaceService: service('namespace'),
@@ -17,14 +15,6 @@ export default Route.extend({
window.scrollTo(0, 0);
},
error(error, transition) {
const controlGroup = this.controlGroup;
if (error instanceof ControlGroupError) {
return controlGroup.handleError(error);
}
if (error.path === '/v1/sys/wrapping/unwrap') {
controlGroup.unmarkTokenForUnwrap();
}
const router = this.routing;
//FIXME transition.intent likely needs to be replaced
let errorURL = transition.intent.url;

View File

@@ -10,7 +10,7 @@ import ModelBoundaryRoute from 'vault/mixins/model-boundary-route';
export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
modelTypes: computed(function () {
return ['capabilities', 'control-group', 'identity/group', 'identity/group-alias', 'identity/alias'];
return ['capabilities', 'identity/group', 'identity/group-alias', 'identity/alias'];
}),
model() {
return {};

View File

@@ -1,30 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import UnloadModel from 'vault/mixins/unload-model-route';
export default Route.extend(UnloadModel, {
store: service(),
version: service(),
model(params) {
return this.version.hasControlGroups ? this.store.findRecord('control-group', params.accessor) : null;
},
actions: {
willTransition() {
return true;
},
// deactivate happens later than willTransition,
// so since we're using the model to render links
// we don't want the UI blinking
deactivate() {
this.unloadModel();
return true;
},
},
});

View File

@@ -1,34 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import UnloadModel from 'vault/mixins/unload-model-route';
export default Route.extend(UnloadModel, {
store: service(),
version: service(),
model() {
const type = 'control-group-config';
return this.version.hasControlGroups
? this.store.findRecord(type, 'config').catch((e) => {
// if you haven't saved a config, the API 404s, so create one here to edit and return it
if (e.httpStatus === 404) {
return this.store.createRecord(type, {
id: 'config',
});
}
throw e;
})
: null;
},
actions: {
reload() {
this.refresh();
},
},
});

View File

@@ -1,17 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import UnloadModel from 'vault/mixins/unload-model-route';
export default Route.extend(UnloadModel, {
store: service(),
version: service(),
model() {
return this.version.hasControlGroups ? this.store.createRecord('control-group') : null;
},
});

View File

@@ -11,7 +11,6 @@ import ModelBoundaryRoute from 'vault/mixins/model-boundary-route';
export default Route.extend(ModelBoundaryRoute, {
auth: service(),
controlGroup: service(),
flashMessages: service(),
console: service(),
permissions: service(),
@@ -26,7 +25,6 @@ export default Route.extend(ModelBoundaryRoute, {
const authType = this.auth.getAuthType();
const ns = this.namespaceService.path;
this.auth.deleteCurrentToken();
this.controlGroup.deleteTokens();
this.namespaceService.reset();
this.console.set('isOpen', false);
this.console.clearLog(true);

View File

@@ -6,7 +6,6 @@
import { resolve } from 'rsvp';
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import ControlGroupError from 'vault/lib/control-group-error';
const SUPPORTED_DYNAMIC_BACKENDS = ['database', 'ssh', 'aws', 'pki'];
@@ -30,11 +29,6 @@ export default Route.extend({
getDatabaseCredential(backend, secret, roleType = '') {
return this.store.queryRecord('database/credential', { backend, secret, roleType }).catch((error) => {
if (error instanceof ControlGroupError) {
throw error;
}
// Unless it's a control group error, we want to pass back error info
// so we can render it on the GenerateCredentialsDatabase component
const status = error?.httpStatus;
let title;
let message = `We ran into a problem and could not continue: ${

View File

@@ -1,33 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend(EmbeddedRecordsMixin, {
attrs: {
requestEntity: { embedded: 'always' },
authorizations: { embedded: 'always' },
},
normalizeResponse(store, primaryModelClass, payload) {
const entity = payload?.data?.request_entity;
if (Array.isArray(payload.data.authorizations)) {
for (const authorization of payload.data.authorizations) {
authorization.id = authorization.entity_id;
authorization.name = authorization.entity_name;
}
}
if (entity && Object.keys(entity).length === 0) {
payload.data.request_entity = null;
}
return this._super(...arguments);
},
serialize(snapshot) {
return { accessor: snapshot.id };
},
});

View File

@@ -1,131 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Service, { inject as service } from '@ember/service';
import RSVP from 'rsvp';
import ControlGroupError from 'vault/lib/control-group-error';
import getStorage from 'vault/lib/token-storage';
import parseURL from 'core/utils/parse-url';
const CONTROL_GROUP_PREFIX = 'vault:cg-';
const TOKEN_SEPARATOR = '☃';
// list of endpoints that return wrapped responses
// without `wrap-ttl`
const WRAPPED_RESPONSE_PATHS = ['sys/wrapping/rewrap', 'sys/wrapping/wrap'];
const storageKey = (accessor, path) => {
return `${CONTROL_GROUP_PREFIX}${accessor}${TOKEN_SEPARATOR}${path}`;
};
export { storageKey, CONTROL_GROUP_PREFIX, TOKEN_SEPARATOR };
export default Service.extend({
version: service(),
router: service(),
storage() {
return getStorage();
},
keyFromAccessor(accessor) {
const keys = this.storage().keys() || [];
const returnKey = keys
.filter((k) => k.startsWith(CONTROL_GROUP_PREFIX))
.find((key) => key.replace(CONTROL_GROUP_PREFIX, '').startsWith(accessor));
return returnKey ? returnKey : null;
},
storeControlGroupToken(info) {
const key = storageKey(info.accessor, info.creation_path);
this.storage().setItem(key, info);
},
deleteControlGroupToken(accessor) {
this.unmarkTokenForUnwrap();
const key = this.keyFromAccessor(accessor);
this.storage().removeItem(key);
},
deleteTokens() {
const keys = this.storage().keys() || [];
keys.filter((k) => k.startsWith(CONTROL_GROUP_PREFIX)).forEach((key) => this.storage().removeItem(key));
},
wrapInfoForAccessor(accessor) {
const key = this.keyFromAccessor(accessor);
return key ? this.storage().getItem(key) : null;
},
tokenToUnwrap: null,
markTokenForUnwrap(accessor) {
this.set('tokenToUnwrap', this.wrapInfoForAccessor(accessor));
},
unmarkTokenForUnwrap() {
this.set('tokenToUnwrap', null);
},
tokenForUrl(url) {
let pathForUrl = parseURL(url).pathname;
pathForUrl = pathForUrl.replace('/v1/', '');
const tokenInfo = this.tokenToUnwrap;
if (tokenInfo && tokenInfo.creation_path === pathForUrl) {
const { token, accessor, creation_time } = tokenInfo;
return { token, accessor, creationTime: creation_time };
}
return null;
},
checkForControlGroup(callbackArgs, response, wasWrapTTLRequested) {
const creationPath = response && response?.wrap_info?.creation_path;
if (
wasWrapTTLRequested ||
!response ||
(creationPath && WRAPPED_RESPONSE_PATHS.includes(creationPath)) ||
!response.wrap_info
) {
return RSVP.resolve(...callbackArgs);
}
const error = new ControlGroupError(response.wrap_info);
return RSVP.reject(error);
},
handleError(error) {
const { accessor, token, creation_path, creation_time, ttl } = error;
const data = { accessor, token, creation_path, creation_time, ttl };
data.uiParams = { url: this.router.currentURL };
this.storeControlGroupToken(data);
return this.router.transitionTo('vault.cluster.access.control-group-accessor', accessor);
},
// Handle error from non-read request (eg. POST or UPDATE) so it can be retried
saveTokenFromError(error) {
const { accessor, token, creation_path, creation_time, ttl } = error;
const data = { accessor, token, creation_path, creation_time, ttl };
this.storeControlGroupToken(data);
// In the read flow the accessor is marked once the user clicks "Visit" from the control group page
// On a POST/UPDATE flow we don't redirect, so we need to mark automatically so that on the next try
// the request will attempt unwrap.
this.markTokenForUnwrap(accessor);
},
logFromError(error) {
const { accessor, token, creation_path, creation_time, ttl } = error;
const data = { accessor, token, creation_path, creation_time, ttl };
this.storeControlGroupToken(data);
const href = this.router.urlFor('vault.cluster.access.control-group-accessor', accessor);
const lines = [
`A Control Group was encountered at ${error.creation_path}.`,
`The Control Group Token is ${error.token}.`,
`The Accessor is ${error.accessor}.`,
`Visit <a href='${href}'>${href}</a> for more details.`,
];
return {
type: 'error-with-html',
content: lines.join('\n'),
};
},
});

View File

@@ -15,7 +15,6 @@ const API_PATHS = {
groups: 'identity/group/id',
leases: 'sys/leases/lookup',
namespaces: 'sys/namespaces',
'control-groups': 'sys/control-group/',
},
policies: {
acl: 'sys/policies/acl',
@@ -44,7 +43,6 @@ const API_PATHS_TO_ROUTE_PARAMS = {
'identity/group/id': { route: 'vault.cluster.access.identity', models: ['groups'] },
'sys/leases/lookup': { route: 'vault.cluster.access.leases', models: [] },
'sys/namespaces': { route: 'vault.cluster.access.namespaces', models: [] },
'sys/control-group/': { route: 'vault.cluster.access.control-groups', models: [] },
'identity/mfa/method': { route: 'vault.cluster.access.mfa', models: [] },
'identity/oidc/client': { route: 'vault.cluster.access.oidc', models: [] },
};

View File

@@ -11,10 +11,6 @@ export default class VersionService extends Service {
@service store;
@tracked version = null;
get hasControlGroups() {
return false;
}
@task
*getVersion() {
if (this.version) return;

View File

@@ -1,53 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
.control-group,
.control-group-success {
@extend .box;
box-shadow: $box-shadow-middle, 0 0 1px $grey-dark;
}
.control-group-success.is-editor {
background: $grey-lightest;
}
.control-group a {
color: currentColor;
}
.control-group-header {
box-shadow: 0 0 1px currentColor;
padding: $size-9 $size-6;
background: $grey-lightest;
color: $grey-dark;
position: relative;
strong {
color: currentColor;
}
}
.control-group-header.is-success {
color: $green-dark;
background: $green-lightest;
}
.control-group .authorizations {
margin-top: $size-9;
}
.control-group .hover-copy-button-static {
color: $orange;
}
.control-group-token-text {
color: $grey;
position: relative;
padding: $size-8 0;
.hover-copy-button-static {
position: relative;
top: auto;
left: auto;
display: inline-block;
}
}

View File

@@ -62,7 +62,6 @@
@import './components/codemirror';
@import './components/confirm';
@import './components/console-ui-panel';
@import './components/control-group';
@import './components/diff-version-selector';
@import './components/doc-link';
@import './components/empty-state-component';

View File

@@ -1,63 +0,0 @@
{{#if (and this.controlGroupResponse.token this.controlGroupResponse.uiParams.url)}}
<div class="control-group-success" data-test-navigate-message>
You have been granted access to
<code>{{this.model.requestPath}}</code>. Be careful, you can only access this data once. If you need access again in the
future you will need to get authorized again.
<div class="box is-shadowless is-fullwidth is-marginless has-slim-padding">
<button data-test-navigate-button type="button" class="button is-primary" {{action (perform this.markAndNavigate)}}>
Visit
</button>
</div>
</div>
{{else}}
{{#if this.unwrapData}}
<div class="control-group-success {{if this.unwrapData 'is-editor'}}">
<div class="has-copy-button">
<JsonEditor
data-test-json-viewer
@showToolbar={{false}}
@value={{stringify this.unwrapData}}
@readOnly={{true}}
@viewportMargin="Infinity"
@gutters={{false}}
@theme="hashi-read-only auto-height"
/>
<HoverCopyButton @copyValue={{stringify this.unwrapData}} />
</div>
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
<LinkTo @route="vault.cluster.access.control-groups" class="button">
<Chevron @direction="left" />
Back
</LinkTo>
</div>
{{else}}
<div class="control-group-success" data-test-unwrap-form>
<form {{action (perform this.unwrap this.token) on="submit"}}>
<MessageError @errorMessage={{this.error}} />
<p>
If you have the token, you can now can access
<code>{{this.model.requestPath}}</code>
</p>
<label for="token" class="is-label">
Token to access data
</label>
<div class="control">
<Input
data-test-token-input
class="input"
autocomplete="off"
spellcheck="false"
name="token"
@value={{this.token}}
/>
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
<button data-test-unwrap-button type="submit" class="button is-primary" disabled={{not this.token}}>
Access
</button>
</div>
</form>
</div>
{{/if}}
{{/if}}

View File

@@ -1,120 +0,0 @@
<div class="box is-fullwidth is-bottomless is-sideless">
<MessageError @model={{this.model}} />
<div class="control-group-header {{if this.isSuccess 'is-success'}}">
<p>
<Icon @name={{if this.isSuccess "check-circle-fill" "lock-fill"}} />
<strong data-test-banner-prefix>{{this.bannerPrefix}}</strong>
<span data-test-banner-text>{{this.bannerText}}</span>
</p>
</div>
{{#if
(and
this.model.approved
(not this.currentUserHasAuthorized)
(or (not this.model.requestEntity) this.currentUserIsRequesting)
)
}}
<ControlGroupSuccess
data-test-control-group-success
@model={{this.model}}
@controlGroupResponse={{this.controlGroupResponse}}
/>
{{else}}
<div class="control-group">
<div data-test-requestor-text>
{{#if this.model.requestEntity.canRead}}
<LinkTo
@route="vault.cluster.access.identity.show"
@models={{array "entities" this.model.requestEntity.id "details"}}
>
{{this.requestorName}}
</LinkTo>
{{else}}
{{this.requestorName}}
{{/if}}
{{#if this.currentUserIsRequesting}}are{{else}}is{{/if}}
{{#if this.model.approved}}
authorized to access
<code>{{this.model.requestPath}}</code>
{{else}}
requesting access to
<code>{{this.model.requestPath}}</code>
{{/if}}
</div>
{{#if (or (not this.model.requestEntity) this.currentUserIsRequesting)}}
<div class="message is-list is-highlight has-copy-button" tabindex="-1" data-test-accessor-callout>
<HoverCopyButton @alwaysShow={{true}} @copyValue={{this.model.id}} />
<div class="message-body">
<h4 class="title is-7 is-marginless">
Accessor
</h4>
<code class="is-word-break" data-test-accessor-value>{{this.model.id}}</code>
</div>
</div>
{{/if}}
<div class="authorizations" data-test-authorizations>
{{#if (gt this.model.authorizations.length 0)}}
<span class="has-text-success">
<Icon @name="check-circle" />
</span>
Already approved by
{{#each this.model.authorizations as |authorization index|}}
{{~#if authorization.canRead~}}
<LinkTo @route="vault.cluster.access.identity.show" @models={{array "entities" authorization.id "details"}}>
{{authorization.name}}
</LinkTo>
{{~else~}}
{{authorization.name}}
{{~/if~}}{{#if (lt (inc index) this.model.authorizations.length)}},{{/if}}
{{/each}}
{{else}}
<span class="has-text-grey">
<Icon @name="check-circle" />
</span>
Awaiting authorization.
{{/if}}
</div>
</div>
{{/if}}
{{#if this.controlGroupResponse.token}}
<p class="control-group-token-text" data-test-token>
Weve saved your request token, but you may want to copy it just in case:
<span class="tag has-font-monospaced" data-test-token-value>{{this.controlGroupResponse.token}}</span>
<HoverCopyButton @alwaysShow={{true}} @copyValue={{this.controlGroupResponse.token}} />
</p>
{{/if}}
{{! template-lint-configure simple-unless "warn" }}
{{#unless (and this.model.approved (or (not this.model.requestEntity) this.currentUserIsRequesting))}}
<div class="field is-grouped box is-fullwidth is-bottomless">
{{#if this.model.canAuthorize}}
{{#if (or this.model.approved this.currentUserHasAuthorized)}}
<LinkTo @route="vault.cluster.access.control-groups" class="button" data-test-back-link={{true}}>
<Chevron @direction="left" />
Back
</LinkTo>
{{else}}
<button
type="button"
class="button is-primary {{if this.authorize.isRunning 'is-loading'}}"
{{action (perform this.authorize)}}
data-test-authorize-button
>
Authorize
</button>
{{/if}}
{{else}}
<button
type="button"
class="button is-primary {{if this.refresh.isRunning 'is-loading'}}"
{{action (perform this.refresh)}}
data-test-refresh-button
>
Refresh
</button>
{{/if}}
</div>
{{/unless}}
</div>

View File

@@ -1,87 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { later, run, _cancelTimers as cancelTimers } from '@ember/runloop';
import { resolve } from 'rsvp';
import Service from '@ember/service';
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, settled } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import { create } from 'ember-cli-page-object';
import controlGroupSuccess from '../../pages/components/control-group-success';
const component = create(controlGroupSuccess);
const controlGroupService = Service.extend({
deleteControlGroupToken: sinon.stub(),
markTokenForUnwrap: sinon.stub(),
});
const storeService = Service.extend({
adapterFor() {
return {
toolAction() {
return resolve({ data: { foo: 'bar' } });
},
};
},
});
module('Integration | Component | control group success', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
run(() => {
this.owner.unregister('service:store');
this.owner.register('service:control-group', controlGroupService);
this.controlGroup = this.owner.lookup('service:control-group');
this.owner.register('service:store', storeService);
this.router = this.owner.lookup('service:router');
this.router.reopen({
transitionTo: sinon.stub().returns(resolve()),
});
});
});
const MODEL = {
approved: false,
requestPath: 'foo/bar',
id: 'accessor',
requestEntity: { id: 'requestor', name: 'entity8509' },
reload: sinon.stub(),
};
test('render with saved token', async function (assert) {
assert.expect(3);
const response = {
uiParams: { url: '/foo' },
token: 'token',
};
this.set('model', MODEL);
this.set('response', response);
await render(hbs`{{control-group-success model=this.model controlGroupResponse=this.response }}`);
assert.ok(component.showsNavigateMessage, 'shows unwrap message');
await component.navigate();
later(() => cancelTimers(), 50);
return settled().then(() => {
assert.ok(this.controlGroup.markTokenForUnwrap.calledOnce, 'marks token for unwrap');
assert.ok(this.router.transitionTo.calledOnce, 'calls router transition');
});
});
test('render without token', async function (assert) {
assert.expect(2);
this.set('model', MODEL);
await render(hbs`{{control-group-success model=this.model}}`);
assert.ok(component.showsUnwrapForm, 'shows unwrap form');
await component.token('token');
component.unwrap();
later(() => cancelTimers(), 50);
return settled().then(() => {
assert.ok(component.showsJsonViewer, 'shows unwrapped data');
});
});
});

View File

@@ -1,187 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Service from '@ember/service';
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import { create } from 'ember-cli-page-object';
import controlGroup from '../../pages/components/control-group';
const component = create(controlGroup);
const controlGroupService = Service.extend({
init() {
this._super(...arguments);
this.set('wrapInfo', null);
},
wrapInfoForAccessor() {
return this.wrapInfo;
},
});
const authService = Service.extend();
module('Integration | Component | control group', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.owner.register('service:auth', authService);
this.owner.register('service:control-group', controlGroupService);
this.controlGroup = this.owner.lookup('service:controlGroup');
this.auth = this.owner.lookup('service:auth');
});
const setup = (modelData = {}, authData = {}) => {
const modelDefaults = {
approved: false,
requestPath: 'foo/bar',
id: 'accessor',
requestEntity: { id: 'requestor', name: 'entity8509' },
reload: sinon.stub(),
};
const authDataDefaults = { entity_id: 'requestor' };
return {
model: {
...modelDefaults,
...modelData,
},
authData: {
...authDataDefaults,
...authData,
},
};
};
test('requestor rendering', async function (assert) {
const { model, authData } = setup();
this.set('model', model);
this.set('auth.authData', authData);
await render(hbs`{{control-group model=this.model}}`);
assert.ok(component.showsAccessorCallout, 'shows accessor callout');
assert.strictEqual(component.bannerPrefix, 'Locked');
assert.strictEqual(component.bannerText, 'The path you requested is locked by a Control Group');
assert.strictEqual(component.requestorText, `You are requesting access to ${model.requestPath}`);
assert.false(component.showsTokenText, 'does not show token message when there is no token');
assert.ok(component.showsRefresh, 'shows refresh button');
assert.ok(component.authorizationText, 'Awaiting authorization.');
});
test('requestor rendering: with token', async function (assert) {
const { model, authData } = setup();
this.set('model', model);
this.set('auth.authData', authData);
this.set('controlGroup.wrapInfo', { token: 'token' });
await render(hbs`{{control-group model=this.model}}`);
assert.true(component.showsTokenText, 'shows token message');
assert.strictEqual(component.token, 'token', 'shows token value');
});
test('requestor rendering: some approvals', async function (assert) {
const { model, authData } = setup({ authorizations: [{ name: 'manager 1' }, { name: 'manager 2' }] });
this.set('model', model);
this.set('auth.authData', authData);
await render(hbs`{{control-group model=this.model}}`);
assert.ok(component.authorizationText, 'Already approved by manager 1, manager 2');
});
test('requestor rendering: approved with no token', async function (assert) {
const { model, authData } = setup({ approved: true });
this.set('model', model);
this.set('auth.authData', authData);
await render(hbs`{{control-group model=this.model}}`);
assert.strictEqual(component.bannerPrefix, 'Success!');
assert.strictEqual(component.bannerText, 'You have been given authorization');
assert.false(component.showsTokenText, 'does not show token message when there is no token');
assert.notOk(component.showsRefresh, 'does not shows refresh button');
assert.ok(component.showsSuccessComponent, 'renders control group success');
});
test('requestor rendering: approved with token', async function (assert) {
const { model, authData } = setup({ approved: true });
this.set('model', model);
this.set('auth.authData', authData);
this.set('controlGroup.wrapInfo', { token: 'token' });
await render(hbs`{{control-group model=this.model}}`);
assert.true(component.showsTokenText, 'shows token');
assert.notOk(component.showsRefresh, 'does not shows refresh button');
assert.ok(component.showsSuccessComponent, 'renders control group success');
});
test('authorizer rendering', async function (assert) {
const { model, authData } = setup({ canAuthorize: true }, { entity_id: 'manager' });
this.set('model', model);
this.set('auth.authData', authData);
await render(hbs`{{control-group model=this.model}}`);
assert.strictEqual(component.bannerPrefix, 'Locked');
assert.strictEqual(
component.bannerText,
'Someone is requesting access to a path locked by a Control Group'
);
assert.strictEqual(
component.requestorText,
`${model.requestEntity.name} is requesting access to ${model.requestPath}`
);
assert.false(component.showsTokenText, 'does not show token message when there is no token');
assert.ok(component.showsAuthorize, 'shows authorize button');
});
test('authorizer rendering:authorized', async function (assert) {
const { model, authData } = setup(
{ canAuthorize: true, authorizations: [{ id: 'manager', name: 'manager' }] },
{ entity_id: 'manager' }
);
this.set('model', model);
this.set('auth.authData', authData);
await render(hbs`{{control-group model=this.model}}`);
assert.strictEqual(component.bannerPrefix, 'Thanks!');
assert.strictEqual(component.bannerText, 'You have given authorization');
assert.ok(component.showsBackLink, 'back link is visible');
});
test('authorizer rendering: authorized and success', async function (assert) {
const { model, authData } = setup(
{ approved: true, canAuthorize: true, authorizations: [{ id: 'manager', name: 'manager' }] },
{ entity_id: 'manager' }
);
this.set('model', model);
this.set('auth.authData', authData);
await render(hbs`{{control-group model=this.model}}`);
assert.strictEqual(component.bannerPrefix, 'Thanks!');
assert.strictEqual(component.bannerText, 'You have given authorization');
assert.ok(component.showsBackLink, 'back link is visible');
assert.strictEqual(
component.requestorText,
`${model.requestEntity.name} is authorized to access ${model.requestPath}`
);
assert.notOk(component.showsSuccessComponent, 'does not render control group success');
});
test('third-party: success', async function (assert) {
const { model, authData } = setup(
{ approved: true, canAuthorize: true, authorizations: [{ id: 'foo', name: 'foo' }] },
{ entity_id: 'manager' }
);
this.set('model', model);
this.set('auth.authData', authData);
await render(hbs`{{control-group model=this.model}}`);
assert.strictEqual(component.bannerPrefix, 'Success!');
assert.strictEqual(component.bannerText, 'This Control Group has been authorized');
assert.ok(component.showsBackLink, 'back link is visible');
assert.notOk(component.showsSuccessComponent, 'does not render control group success');
});
});

View File

@@ -1,15 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { isPresent, fillable, clickable } from 'ember-cli-page-object';
export default {
showsJsonViewer: isPresent('[data-test-json-viewer]'),
showsNavigateMessage: isPresent('[data-test-navigate-message]'),
showsUnwrapForm: isPresent('[data-test-unwrap-form]'),
navigate: clickable('[data-test-navigate-button]'),
unwrap: clickable('[data-test-unwrap-button]'),
token: fillable('[data-test-token-input]'),
};

View File

@@ -1,24 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { isPresent, clickable, text } from 'ember-cli-page-object';
export default {
showsAccessorCallout: isPresent('[data-test-accessor-callout]'),
authorizationText: text('[data-test-authorizations]'),
bannerPrefix: text('[data-test-banner-prefix]'),
bannerText: text('[data-test-banner-text]'),
requestorText: text('[data-test-requestor-text]'),
showsTokenText: isPresent('[data-test-token]'),
refresh: clickable('[data-test-refresh-button]'),
authorize: clickable('[data-test-authorize-button]'),
showsSuccessComponent: isPresent('[data-test-control-group-success]'),
accessor: text('[data-test-accessor-value]'),
token: text('[data-test-token-value]'),
showsRefresh: isPresent('[data-test-refresh-button]'),
showsAuthorize: isPresent('[data-test-authorize-button]'),
showsBackLink: isPresent('[data-test-back-link]'),
};

View File

@@ -1,255 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { set } from '@ember/object';
import Service from '@ember/service';
import { module, skip } from 'qunit';
import { setupTest } from 'ember-qunit';
import sinon from 'sinon';
import { storageKey, CONTROL_GROUP_PREFIX, TOKEN_SEPARATOR } from 'vault/services/control-group';
const versionStub = Service.extend();
function storage() {
return {
items: {},
getItem(key) {
var item = this.items[key];
return item && JSON.parse(item);
},
setItem(key, val) {
return (this.items[key] = JSON.stringify(val));
},
removeItem(key) {
delete this.items[key];
},
keys() {
return Object.keys(this.items);
},
};
}
module('Unit | Service | control group', function (hooks) {
setupTest(hooks);
hooks.beforeEach(function () {
this.owner.register('service:version', versionStub);
this.version = this.owner.lookup('service:version');
this.router = this.owner.lookup('service:router');
this.router.reopen({
transitionTo: sinon.stub(),
urlFor: sinon.stub().returns('/ui/vault/foo'),
currentURL: '/vault/secrets/kv/show/foo',
});
});
hooks.afterEach(function () {});
const isOSS = (context) => set(context, 'version.isOSS', true);
const isEnt = (context) => set(context, 'version.isOSS', false);
const resolvesArgs = (assert, result, expectedArgs) => {
return result.then((...args) => {
return assert.deepEqual(args, expectedArgs, 'resolves with the passed args');
});
};
[
[
'it resolves isOSS:true, wrapTTL: true, response: has wrap_info',
isOSS,
[[{ one: 'two', three: 'four' }], { wrap_info: { token: 'foo', accessor: 'bar' } }, true],
(assert, result) => resolvesArgs(assert, result, [{ one: 'two', three: 'four' }]),
],
[
'it resolves isOSS:true, wrapTTL: false, response: has no wrap_info',
isOSS,
[[{ one: 'two', three: 'four' }], { wrap_info: null }, false],
(assert, result) => resolvesArgs(assert, result, [{ one: 'two', three: 'four' }]),
],
[
'it resolves isOSS: false and wrapTTL:true response: has wrap_info',
isEnt,
[[{ one: 'two', three: 'four' }], { wrap_info: { token: 'foo', accessor: 'bar' } }, true],
(assert, result) => resolvesArgs(assert, result, [{ one: 'two', three: 'four' }]),
],
[
'it resolves isOSS: false and wrapTTL:false response: has no wrap_info',
isEnt,
[[{ one: 'two', three: 'four' }], { wrap_info: null }, false],
(assert, result) => resolvesArgs(assert, result, [{ one: 'two', three: 'four' }]),
],
[
'it rejects isOSS: false, wrapTTL:false, response: has wrap_info',
isEnt,
[
[{ one: 'two', three: 'four' }],
{ foo: 'bar', wrap_info: { token: 'secret', accessor: 'lookup' } },
false,
],
(assert, result) => {
// ensure failure if we ever don't reject
assert.expect(2);
return result.then(
() => {},
(err) => {
assert.strictEqual(err.token, 'secret');
assert.strictEqual(err.accessor, 'lookup');
}
);
},
],
].forEach(function ([name, setup, args, expectation]) {
skip(`checkForControlGroup: ${name}`, function (assert) {
const assertCount = name === 'it rejects isOSS: false, wrapTTL:false, response: has wrap_info' ? 2 : 1;
assert.expect(assertCount);
if (setup) {
setup(this);
}
const service = this.owner.lookup('service:control-group');
const result = service.checkForControlGroup(...args);
return expectation(assert, result);
});
});
skip(`handleError: transitions to accessor and stores control group token`, function (assert) {
const error = {
accessor: '12345',
token: 'token',
creation_path: 'kv/',
creation_time: '2022-03-17T20:00:25.594Z',
ttl: 400,
};
const expected = { ...error, uiParams: { url: '/vault/secrets/kv/show/foo' } };
const service = this.owner.factoryFor('service:control-group').create({
storeControlGroupToken: sinon.spy(),
});
service.handleError(error);
assert.ok(service.storeControlGroupToken.calledWith(expected), 'calls storeControlGroupToken');
assert.ok(
this.router.transitionTo.calledWith('vault.cluster.access.control-group-accessor', '12345'),
'calls router transitionTo'
);
});
skip(`logFromError: returns correct content string`, function (assert) {
const error = {
accessor: '12345',
token: 'token',
creation_path: 'kv/',
creation_time: '2022-03-17T20:00:25.594Z',
ttl: 400,
};
const service = this.owner.factoryFor('service:control-group').create({
storeControlGroupToken: sinon.spy(),
});
const contentString = service.logFromError(error);
assert.ok(
this.router.urlFor.calledWith('vault.cluster.access.control-group-accessor', '12345'),
'calls urlFor with accessor'
);
assert.ok(service.storeControlGroupToken.calledWith(error), 'calls storeControlGroupToken');
assert.ok(contentString.content.includes('12345'), 'contains accessor');
assert.ok(contentString.content.includes('kv/'), 'contains creation path');
assert.ok(contentString.content.includes('token'), 'contains token');
});
skip('storageKey', function (assert) {
const accessor = '12345';
const path = 'kv/foo/bar';
const expectedKey = `${CONTROL_GROUP_PREFIX}${accessor}${TOKEN_SEPARATOR}${path}`;
assert.strictEqual(storageKey(accessor, path), expectedKey, 'uses expected key');
});
skip('keyFromAccessor', function (assert) {
const store = storage();
const accessor = '12345';
const path = 'kv/foo/bar';
const data = { foo: 'bar' };
const expectedKey = `${CONTROL_GROUP_PREFIX}${accessor}${TOKEN_SEPARATOR}${path}`;
const subject = this.owner.factoryFor('service:control-group').create({
storage() {
return store;
},
});
store.setItem(expectedKey, data);
store.setItem(`${CONTROL_GROUP_PREFIX}2345${TOKEN_SEPARATOR}${path}`, 'ok');
assert.strictEqual(subject.keyFromAccessor(accessor), expectedKey, 'finds key given the accessor');
assert.strictEqual(subject.keyFromAccessor('foo'), null, 'returns null if no key was found');
});
skip('storeControlGroupToken', function (assert) {
const store = storage();
const subject = this.owner.factoryFor('service:control-group').create({
storage() {
return store;
},
});
const info = {
accessor: '12345',
creation_path: 'foo/',
creation_time: '2022-03-17T20:00:25.594Z',
ttl: 300,
};
const key = `${CONTROL_GROUP_PREFIX}${info.accessor}${TOKEN_SEPARATOR}${info.creation_path}`;
subject.storeControlGroupToken(info);
assert.deepEqual(store.items[key], JSON.stringify(info), 'stores the whole info object');
});
skip('deleteControlGroupToken', function (assert) {
const store = storage();
const subject = this.owner.factoryFor('service:control-group').create({
storage() {
return store;
},
});
const accessor = 'foo';
const path = 'kv/one';
const expectedKey = `${CONTROL_GROUP_PREFIX}${accessor}${TOKEN_SEPARATOR}${path}`;
store.setItem(expectedKey, { one: '2' });
subject.deleteControlGroupToken(accessor);
assert.strictEqual(Object.keys(store.items).length, 0, 'there are no keys stored in storage');
});
skip('deleteTokens', function (assert) {
const store = storage();
const subject = this.owner.factoryFor('service:control-group').create({
storage() {
return store;
},
});
const keyOne = `${CONTROL_GROUP_PREFIX}foo`;
const keyTwo = `${CONTROL_GROUP_PREFIX}bar`;
store.setItem(keyOne, { one: '2' });
store.setItem(keyTwo, { two: '2' });
store.setItem('value', 'one');
assert.strictEqual(Object.keys(store.items).length, 3, 'stores 3 values');
subject.deleteTokens();
assert.strictEqual(Object.keys(store.items).length, 1, 'removes tokens with control group prefix');
assert.strictEqual(store.getItem('value'), 'one', 'keeps the non-prefixed value');
});
skip('wrapInfoForAccessor', function (assert) {
const store = storage();
const subject = this.owner.factoryFor('service:control-group').create({
storage() {
return store;
},
});
const keyOne = `${CONTROL_GROUP_PREFIX}foo`;
store.setItem(keyOne, { one: '2' });
assert.deepEqual(subject.wrapInfoForAccessor('foo'), { one: '2' });
});
});