/** * Copyright (c) HashiCorp, Inc. * SPDX-License-Identifier: MPL-2.0 */ 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) { const data = {}; Object.keys(obj).forEach((key) => { if (picklist.indexOf(key) >= 0) { data[key] = obj[key]; } }); return data; } export default class KeymgmtKeyAdapter extends ApplicationAdapter { @service store; namespace = 'v1'; pathForType() { // backend name prepended in buildURL method return 'key'; } buildURL(modelName, id, snapshot, requestType, query) { let url = super.buildURL(...arguments); if (snapshot) { url = url.replace('key', `${snapshot.attr('backend')}/key`); } else if (query) { url = url.replace('key', `${query.backend}/key`); } return url; } url(backend, id, type) { const url = `${this.buildURL()}/${backend}/key`; if (id) { if (type === 'ROTATE') { return url + '/' + encodePath(id) + '/rotate'; } else if (type === 'PROVIDERS') { return url + '/' + encodePath(id) + '/kms'; } return url + '/' + encodePath(id); } return url; } _updateKey(backend, name, serialized) { // Only these two attributes are allowed to be updated const data = pickKeys(serialized, ['deletion_allowed', 'min_enabled_version']); return this.ajax(this.url(backend, name), 'PUT', { data }); } _createKey(backend, name, serialized) { // Only type is allowed on create const data = pickKeys(serialized, ['type']); return this.ajax(this.url(backend, name), 'POST', { data }); } async createRecord(store, type, snapshot) { const data = store.serializerFor(type.modelName).serialize(snapshot); const name = snapshot.attr('name'); const backend = snapshot.attr('backend'); // Keys must be created and then updated await this._createKey(backend, name, data); if (snapshot.attr('deletionAllowed')) { try { await this._updateKey(backend, name, data); } catch { throw new Error(`Key ${name} was created, but not all settings were saved`); } } return { data: { ...data, id: name, backend, }, }; } updateRecord(store, type, snapshot) { const data = store.serializerFor(type.modelName).serialize(snapshot); const name = snapshot.attr('name'); const backend = snapshot.attr('backend'); return this._updateKey(backend, name, data); } distribute(backend, kms, key, data) { return this.ajax(`${this.buildURL()}/${backend}/kms/${encodePath(kms)}/key/${encodePath(key)}`, 'PUT', { data: { ...data }, }); } async getProvider(backend, name) { try { const resp = await this.ajax(this.url(backend, name, 'PROVIDERS'), 'GET', { data: { list: true, }, }); return resp.data.keys ? resp.data.keys[0] : null; } catch (e) { if (e.httpStatus === 404) { // No results, not distributed yet return null; } else if (e.httpStatus === 403) { return { permissionsError: true }; } throw e; } } 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; }); } async queryRecord(store, type, query) { const { id, backend, recordOnly = false } = query; const keyData = await this.ajax(this.url(backend, id), 'GET'); keyData.data.id = id; keyData.data.backend = backend; let provider, distribution; if (!recordOnly) { provider = await this.getProvider(backend, id); if (provider && !provider.permissionsError) { distribution = await this.getDistribution(backend, provider, id); } } return { ...keyData, provider, distribution }; } async query(store, type, query) { const { backend, provider } = query; const providerAdapter = store.adapterFor('keymgmt/provider'); const url = provider ? providerAdapter.buildKeysURL(query) : this.url(backend); return this.ajax(url, 'GET', { data: { list: true, }, }).then((res) => { res.backend = backend; return res; }); } async rotateKey(backend, id) { const keyModel = this.store.peekRecord('keymgmt/key', id); const result = await this.ajax(this.url(backend, id, 'ROTATE'), 'PUT'); await keyModel.reload(); return result; } removeFromProvider(model) { const url = `${this.buildURL()}/${model.backend}/kms/${model.provider}/key/${model.name}`; return this.ajax(url, 'DELETE').then(() => { model.provider = null; }); } }