diff --git a/CHANGELOG.md b/CHANGELOG.md index de92860cc1de..4dcb052e45e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## Next +## 1.0.3 (February 12th, 2019) CHANGES: @@ -9,6 +9,11 @@ CHANGES: entity either by name or by id [GH-6105] * The Vault UI's navigation and onboarding wizard now only displays items that are permitted in a users' policy [GH-5980, GH-6094] + * An issue was fixed that caused recovery keys to not work on secondary + clusters when using a different unseal mechanism/key than the primary. This + would be hit if the cluster was rekeyed or initialized after 1.0. We recommend + rekeying the recovery keys on the primary cluster if you meet the above + requirements. FEATURES: @@ -47,6 +52,8 @@ BUG FIXES: a performance standby very quickly, before an associated entity has been replicated. If the entity is not found in this scenario, the request will forward to the active node. + * replication: Fix issue where recovery keys would not work on secondary + clusters if using a different unseal mechanism than the primary. * replication: Fix a "failed to register lease" error when using performance standbys * storage/postgresql: The `Get` method will now return an Entry object with diff --git a/terraform/aws/variables.tf b/terraform/aws/variables.tf index 64ffc78b6215..ece9cc78df3d 100644 --- a/terraform/aws/variables.tf +++ b/terraform/aws/variables.tf @@ -3,7 +3,7 @@ //------------------------------------------------------------------- variable "download-url" { - default = "https://releases.hashicorp.com/vault/1.0.2/vault_1.0.2_linux_amd64.zip" + default = "https://releases.hashicorp.com/vault/1.0.3/vault_1.0.3_linux_amd64.zip" description = "URL to download Vault" } diff --git a/ui/app/adapters/auth-config/azure.js b/ui/app/adapters/auth-config/azure.js index 21f5624ac4d0..e43cb2ea860f 100644 --- a/ui/app/adapters/auth-config/azure.js +++ b/ui/app/adapters/auth-config/azure.js @@ -1,2 +1,3 @@ import AuthConfig from './_base'; + export default AuthConfig.extend(); diff --git a/ui/app/adapters/auth-config/gcp.js b/ui/app/adapters/auth-config/gcp.js index 21f5624ac4d0..e43cb2ea860f 100644 --- a/ui/app/adapters/auth-config/gcp.js +++ b/ui/app/adapters/auth-config/gcp.js @@ -1,2 +1,3 @@ import AuthConfig from './_base'; + export default AuthConfig.extend(); diff --git a/ui/app/adapters/auth-config/github.js b/ui/app/adapters/auth-config/github.js index 21f5624ac4d0..e43cb2ea860f 100644 --- a/ui/app/adapters/auth-config/github.js +++ b/ui/app/adapters/auth-config/github.js @@ -1,2 +1,3 @@ import AuthConfig from './_base'; + export default AuthConfig.extend(); diff --git a/ui/app/adapters/auth-config/kubernetes.js b/ui/app/adapters/auth-config/kubernetes.js index 21f5624ac4d0..e43cb2ea860f 100644 --- a/ui/app/adapters/auth-config/kubernetes.js +++ b/ui/app/adapters/auth-config/kubernetes.js @@ -1,2 +1,3 @@ import AuthConfig from './_base'; + export default AuthConfig.extend(); diff --git a/ui/app/adapters/auth-config/ldap.js b/ui/app/adapters/auth-config/ldap.js index 21f5624ac4d0..e43cb2ea860f 100644 --- a/ui/app/adapters/auth-config/ldap.js +++ b/ui/app/adapters/auth-config/ldap.js @@ -1,2 +1,3 @@ import AuthConfig from './_base'; + export default AuthConfig.extend(); diff --git a/ui/app/adapters/auth-config/okta.js b/ui/app/adapters/auth-config/okta.js index 21f5624ac4d0..e43cb2ea860f 100644 --- a/ui/app/adapters/auth-config/okta.js +++ b/ui/app/adapters/auth-config/okta.js @@ -1,2 +1,3 @@ import AuthConfig from './_base'; + export default AuthConfig.extend(); diff --git a/ui/app/adapters/auth-config/radius.js b/ui/app/adapters/auth-config/radius.js index 21f5624ac4d0..e43cb2ea860f 100644 --- a/ui/app/adapters/auth-config/radius.js +++ b/ui/app/adapters/auth-config/radius.js @@ -1,2 +1,3 @@ import AuthConfig from './_base'; + export default AuthConfig.extend(); diff --git a/ui/app/adapters/pki-certificate-sign.js b/ui/app/adapters/pki-certificate-sign.js index eb5ca26157a4..1acdbfb8192b 100644 --- a/ui/app/adapters/pki-certificate-sign.js +++ b/ui/app/adapters/pki-certificate-sign.js @@ -7,4 +7,8 @@ export default Adapter.extend({ } return `/v1/${role.backend}/sign/${role.name}`; }, + + pathForType() { + return 'sign'; + }, }); diff --git a/ui/app/adapters/pki-certificate.js b/ui/app/adapters/pki-certificate.js index 942d2610b106..8954a0ff60d3 100644 --- a/ui/app/adapters/pki-certificate.js +++ b/ui/app/adapters/pki-certificate.js @@ -13,7 +13,6 @@ export default Adapter.extend({ } return url; }, - optionsForQuery(id) { let data = {}; if (!id) { diff --git a/ui/app/adapters/secret-engine.js b/ui/app/adapters/secret-engine.js index 56481655368c..42b998f26905 100644 --- a/ui/app/adapters/secret-engine.js +++ b/ui/app/adapters/secret-engine.js @@ -7,16 +7,20 @@ export default ApplicationAdapter.extend({ return path ? url + '/' + path : url; }, + internalURL(path) { + let url = `/${this.urlPrefix()}/internal/ui/mounts`; + if (path) { + url = `${url}/${path}`; + } + return url; + }, + pathForType() { return 'mounts'; }, query(store, type, query) { - let url = `/${this.urlPrefix()}/internal/ui/mounts`; - if (query.path) { - url = `${url}/${query.path}`; - } - return this.ajax(url, 'GET'); + return this.ajax(this.internalURL(query.path), 'GET'); }, createRecord(store, type, snapshot) { diff --git a/ui/app/adapters/secret.js b/ui/app/adapters/secret.js index d282d08e89d0..59c39a0387ea 100644 --- a/ui/app/adapters/secret.js +++ b/ui/app/adapters/secret.js @@ -34,6 +34,10 @@ export default ApplicationAdapter.extend({ return url; }, + pathForType() { + return 'mounts'; + }, + optionsForQuery(id, action, wrapTTL) { let data = {}; if (action === 'query') { diff --git a/ui/app/components/auth-config-form/config.js b/ui/app/components/auth-config-form/config.js index 31fd4ac0b258..d601ebbf1115 100644 --- a/ui/app/components/auth-config-form/config.js +++ b/ui/app/components/auth-config-form/config.js @@ -8,10 +8,11 @@ const AuthConfigBase = Component.extend({ model: null, flashMessages: service(), - + router: service(), + wizard: service(), saveModel: task(function*() { try { - yield this.get('model').save(); + yield this.model.save(); } catch (err) { // AdapterErrors are handled by the error-message component // in the form @@ -20,7 +21,11 @@ const AuthConfigBase = Component.extend({ } return; } - this.get('flashMessages').success('The configuration was saved successfully.'); + if (this.wizard.currentMachine === 'authentication' && this.wizard.featureState === 'config') { + this.wizard.transitionFeatureMachine(this.wizard.featureState, 'CONTINUE'); + } + this.router.transitionTo('vault.cluster.access.methods').followRedirects(); + this.flashMessages.success('The configuration was saved successfully.'); }), }); diff --git a/ui/app/components/auth-config-form/options.js b/ui/app/components/auth-config-form/options.js index 82f850e1e5b1..528c803c8591 100644 --- a/ui/app/components/auth-config-form/options.js +++ b/ui/app/components/auth-config-form/options.js @@ -1,14 +1,16 @@ import AuthConfigComponent from './config'; +import { inject as service } from '@ember/service'; import { task } from 'ember-concurrency'; import DS from 'ember-data'; export default AuthConfigComponent.extend({ + router: service(), + wizard: service(), saveModel: task(function*() { - const model = this.get('model'); - let data = model.get('config').serialize(); - data.description = model.get('description'); + let data = this.model.config.serialize(); + data.description = this.model.description; try { - yield model.tune(data); + yield this.model.tune(data); } catch (err) { // AdapterErrors are handled by the error-message component // in the form @@ -17,6 +19,10 @@ export default AuthConfigComponent.extend({ } return; } - this.get('flashMessages').success('The configuration options were saved successfully.'); + if (this.wizard.currentMachine === 'authentication' && this.wizard.featureState === 'config') { + this.wizard.transitionFeatureMachine(this.wizard.featureState, 'CONTINUE'); + } + this.router.transitionTo('vault.cluster.access.methods').followRedirects(); + this.flashMessages.success('The configuration was saved successfully.'); }), }); diff --git a/ui/app/components/mount-backend-form.js b/ui/app/components/mount-backend-form.js index fbfb6c6a09dd..80cb02502873 100644 --- a/ui/app/components/mount-backend-form.js +++ b/ui/app/components/mount-backend-form.js @@ -21,7 +21,6 @@ export default Component.extend({ * */ onMountSuccess() {}, - onConfigError() {}, /* * @param String * @public @@ -41,18 +40,18 @@ export default Component.extend({ */ mountModel: null, - showConfig: false, + showEnable: false, init() { this._super(...arguments); - const type = this.get('mountType'); + const type = this.mountType; const modelType = type === 'secret' ? 'secret-engine' : 'auth-method'; - const model = this.get('store').createRecord(modelType); + const model = this.store.createRecord(modelType); this.set('mountModel', model); }, mountTypes: computed('mountType', function() { - return this.get('mountType') === 'secret' ? ENGINES : METHODS; + return this.mountType === 'secret' ? ENGINES : METHODS; }), willDestroy() { @@ -60,44 +59,10 @@ export default Component.extend({ this.get('mountModel').rollbackAttributes(); }, - getConfigModelType(methodType) { - let mountType = this.get('mountType'); - // will be something like secret-aws - // or auth-azure - let key = `${mountType}-${methodType}`; - let noConfig = ['auth-approle', 'auth-alicloud']; - if (mountType === 'secret' || noConfig.includes(key)) { - return; - } - if (methodType === 'aws') { - return 'auth-config/aws/client'; - } - return `auth-config/${methodType}`; - }, - - changeConfigModel(methodType) { - let mount = this.get('mountModel'); - if (this.get('mountType') === 'secret') { - return; - } - let configRef = mount.hasMany('authConfigs').value(); - let currentConfig = configRef && configRef.get('firstObject'); - if (currentConfig) { - // rollbackAttributes here will remove the the config model from the store - // because `isNew` will be true - currentConfig.rollbackAttributes(); - currentConfig.unloadRecord(); - } - let configType = this.getConfigModelType(methodType); - if (!configType) return; - let config = this.get('store').createRecord(configType); - config.set('backend', mount); - }, - checkPathChange(type) { - let mount = this.get('mountModel'); - let currentPath = mount.get('path'); - let list = this.get('mountTypes'); + let mount = this.mountModel; + let currentPath = mount.path; + let list = this.mountTypes; // if the current path matches a type (meaning the user hasn't altered it), // change it here to match the new type let isUnchanged = list.findBy('type', currentPath); @@ -107,7 +72,7 @@ export default Component.extend({ }, mountBackend: task(function*() { - const mountModel = this.get('mountModel'); + const mountModel = this.mountModel; const { type, path } = mountModel.getProperties('type', 'path'); try { yield mountModel.save(); @@ -116,74 +81,27 @@ export default Component.extend({ return; } - let mountType = this.get('mountType'); + let mountType = this.mountType; mountType = mountType === 'secret' ? `${mountType}s engine` : `${mountType} method`; - this.get('flashMessages').success(`Successfully mounted the ${type} ${mountType} at ${path}.`); - if (this.get('mountType') === 'secret') { - yield this.get('onMountSuccess')(type, path); - return; - } - yield this.get('saveConfig').perform(mountModel); - }).drop(), - - advanceWizard() { - this.get('wizard').transitionFeatureMachine( - this.get('wizard.featureState'), - 'CONTINUE', - this.get('mountModel').get('type') - ); - }, - saveConfig: task(function*(mountModel) { - const configRef = mountModel.hasMany('authConfigs').value(); - const { type, path } = mountModel.getProperties('type', 'path'); - if (!configRef) { - this.advanceWizard(); - yield this.get('onMountSuccess')(type, path); - return; - } - const config = configRef.get('firstObject'); - try { - if (config && Object.keys(config.changedAttributes()).length) { - yield config.save(); - this.advanceWizard(); - this.get('flashMessages').success( - `The config for ${type} ${this.get('mountType')} method at ${path} was saved successfully.` - ); - } - yield this.get('onMountSuccess')(type, path); - } catch (err) { - this.get('flashMessages').danger( - `There was an error saving the configuration for ${type} ${this.get( - 'mountType' - )} method at ${path}. ${err.errors.join(' ')}` - ); - yield this.get('onConfigError')(mountModel.id); - } + this.flashMessages.success(`Successfully mounted the ${type} ${mountType} at ${path}.`); + yield this.onMountSuccess(type, path); + return; }).drop(), actions: { onTypeChange(path, value) { if (path === 'type') { - this.get('wizard').set('componentState', value); - this.changeConfigModel(value); + this.wizard.set('componentState', value); this.checkPathChange(value); } }, - toggleShowConfig(value) { - this.set('showConfig', value); - if (value === true && this.get('wizard.featureState') === 'idle') { - this.get('wizard').transitionFeatureMachine( - this.get('wizard.featureState'), - 'CONTINUE', - this.get('mountModel').get('type') - ); + toggleShowEnable(value) { + this.set('showEnable', value); + if (value === true && this.wizard.featureState === 'idle') { + this.wizard.transitionFeatureMachine(this.wizard.featureState, 'CONTINUE', this.mountModel.type); } else { - this.get('wizard').transitionFeatureMachine( - this.get('wizard.featureState'), - 'RESET', - this.get('mountModel').get('type') - ); + this.wizard.transitionFeatureMachine(this.wizard.featureState, 'RESET', this.mountModel.type); } }, }, diff --git a/ui/app/controllers/vault/cluster/settings/auth/enable.js b/ui/app/controllers/vault/cluster/settings/auth/enable.js index d5dd984c3256..9d3153678f6a 100644 --- a/ui/app/controllers/vault/cluster/settings/auth/enable.js +++ b/ui/app/controllers/vault/cluster/settings/auth/enable.js @@ -4,14 +4,10 @@ import Controller from '@ember/controller'; export default Controller.extend({ wizard: service(), actions: { - onMountSuccess: function(type) { - let transition = this.transitionToRoute('vault.cluster.access.methods'); - return transition.followRedirects().then(() => { - this.get('wizard').transitionFeatureMachine(this.get('wizard.featureState'), 'CONTINUE', type); - }); - }, - onConfigError: function(modelId) { - return this.transitionToRoute('vault.cluster.settings.auth.configure', modelId); + onMountSuccess: function(type, path) { + this.wizard.transitionFeatureMachine(this.wizard.featureState, 'CONTINUE', type); + let transition = this.transitionToRoute('vault.cluster.settings.auth.configure', path); + return transition.followRedirects(); }, }, }); diff --git a/ui/app/machines/auth-machine.js b/ui/app/machines/auth-machine.js index cec39a9896ba..6ee82dd08a62 100644 --- a/ui/app/machines/auth-machine.js +++ b/ui/app/machines/auth-machine.js @@ -22,16 +22,16 @@ export default { { type: 'render', level: 'step', component: 'wizard/auth-enable' }, ], on: { - CONTINUE: 'list', + CONTINUE: 'config', }, }, - list: { + config: { onEntry: [ - { type: 'render', level: 'step', component: 'wizard/auth-list' }, { type: 'render', level: 'feature', component: 'wizard/mounts-wizard' }, + { type: 'render', level: 'step', component: 'wizard/auth-config' }, ], on: { - DETAILS: 'details', + CONTINUE: 'details', }, }, details: { diff --git a/ui/app/models/auth-config.js b/ui/app/models/auth-config.js index 88befe73b6ab..3c70bb14ec3d 100644 --- a/ui/app/models/auth-config.js +++ b/ui/app/models/auth-config.js @@ -2,5 +2,8 @@ import DS from 'ember-data'; const { belongsTo } = DS; export default DS.Model.extend({ - backend: belongsTo('auth-method', { readOnly: true, async: false }), + backend: belongsTo('auth-method', { inverse: 'authConfigs', readOnly: true, async: false }), + getHelpUrl: function(backend) { + return `/v1/auth/${backend}/config?help=1`; + }, }); diff --git a/ui/app/models/auth-config/azure.js b/ui/app/models/auth-config/azure.js index 1949d20e8563..6ef8a3f507f0 100644 --- a/ui/app/models/auth-config/azure.js +++ b/ui/app/models/auth-config/azure.js @@ -1,12 +1,13 @@ import { computed } from '@ember/object'; import DS from 'ember-data'; - import AuthConfig from '../auth-config'; +import { combineFieldGroups } from 'vault/utils/openapi-to-attrs'; import fieldToAttrs from 'vault/utils/field-to-attrs'; const { attr } = DS; export default AuthConfig.extend({ + useOpenAPI: true, tenantId: attr('string', { label: 'Tenant ID', helpText: 'The tenant ID for the Azure Active Directory organization', @@ -26,12 +27,16 @@ export default AuthConfig.extend({ googleCertsEndpoint: attr('string'), fieldGroups: computed(function() { - const groups = [ + let groups = [ { default: ['tenantId', 'resource'] }, { 'Azure Options': ['clientId', 'clientSecret'], }, ]; + if (this.newFields) { + groups = combineFieldGroups(groups, this.newFields, []); + } + return fieldToAttrs(this, groups); }), }); diff --git a/ui/app/models/auth-config/gcp.js b/ui/app/models/auth-config/gcp.js index 9185fb297614..25d91d284812 100644 --- a/ui/app/models/auth-config/gcp.js +++ b/ui/app/models/auth-config/gcp.js @@ -1,12 +1,14 @@ import { computed } from '@ember/object'; import DS from 'ember-data'; - import AuthConfig from '../auth-config'; +import { combineFieldGroups } from 'vault/utils/openapi-to-attrs'; import fieldToAttrs from 'vault/utils/field-to-attrs'; const { attr } = DS; export default AuthConfig.extend({ + useOpenAPI: true, + // We have to leave this here because the backend doesn't support the file type yet. credentials: attr('string', { editType: 'file', }), @@ -14,12 +16,15 @@ export default AuthConfig.extend({ googleCertsEndpoint: attr('string'), fieldGroups: computed(function() { - const groups = [ + let groups = [ { default: ['credentials'] }, { 'Google Cloud Options': ['googleCertsEndpoint'], }, ]; + if (this.newFields) { + groups = combineFieldGroups(groups, this.newFields, []); + } return fieldToAttrs(this, groups); }), }); diff --git a/ui/app/models/auth-config/github.js b/ui/app/models/auth-config/github.js index 0c54653836d6..df745af14e86 100644 --- a/ui/app/models/auth-config/github.js +++ b/ui/app/models/auth-config/github.js @@ -1,24 +1,28 @@ import { computed } from '@ember/object'; import DS from 'ember-data'; - import AuthConfig from '../auth-config'; import fieldToAttrs from 'vault/utils/field-to-attrs'; +import { combineFieldGroups } from 'vault/utils/openapi-to-attrs'; const { attr } = DS; export default AuthConfig.extend({ + useOpenAPI: true, organization: attr('string'), baseUrl: attr('string', { label: 'Base URL', }), fieldGroups: computed(function() { - const groups = [ + let groups = [ { default: ['organization'] }, { 'GitHub Options': ['baseUrl'], }, ]; + if (this.newFields) { + groups = combineFieldGroups(groups, this.newFields, []); + } return fieldToAttrs(this, groups); }), diff --git a/ui/app/models/auth-config/kubernetes.js b/ui/app/models/auth-config/kubernetes.js index 4d8da679a767..8eb0b0913e4a 100644 --- a/ui/app/models/auth-config/kubernetes.js +++ b/ui/app/models/auth-config/kubernetes.js @@ -2,36 +2,34 @@ import { computed } from '@ember/object'; import DS from 'ember-data'; import AuthConfig from '../auth-config'; +import { combineFieldGroups } from 'vault/utils/openapi-to-attrs'; import fieldToAttrs from 'vault/utils/field-to-attrs'; const { attr } = DS; export default AuthConfig.extend({ + useOpenAPI: true, kubernetesHost: attr('string', { - label: 'Kubernetes Host', helpText: 'Host must be a host string, a host:port pair, or a URL to the base of the Kubernetes API server', }), kubernetesCaCert: attr('string', { - label: 'Kubernetes CA Certificate', editType: 'file', helpText: 'PEM encoded CA cert for use by the TLS client used to talk with the Kubernetes API', }), tokenReviewerJwt: attr('string', { - label: 'Token Reviewer JWT', helpText: 'A service account JWT used to access the TokenReview API to validate other JWTs during login. If not set the JWT used for login will be used to access the API', }), pemKeys: attr({ - label: 'Service account verification keys', editType: 'stringArray', }), fieldGroups: computed(function() { - const groups = [ + let groups = [ { default: ['kubernetesHost', 'kubernetesCaCert'], }, @@ -39,6 +37,10 @@ export default AuthConfig.extend({ 'Kubernetes Options': ['tokenReviewerJwt', 'pemKeys'], }, ]; + if (this.newFields) { + groups = combineFieldGroups(groups, this.newFields, []); + } + return fieldToAttrs(this, groups); }), }); diff --git a/ui/app/models/auth-config/ldap.js b/ui/app/models/auth-config/ldap.js index 9bb4a14491e3..de51d1489bd1 100644 --- a/ui/app/models/auth-config/ldap.js +++ b/ui/app/models/auth-config/ldap.js @@ -3,97 +3,50 @@ import DS from 'ember-data'; import AuthConfig from '../auth-config'; import fieldToAttrs from 'vault/utils/field-to-attrs'; +import { combineFieldGroups } from 'vault/utils/openapi-to-attrs'; const { attr } = DS; export default AuthConfig.extend({ - url: attr('string', { - label: 'URL', - }), - starttls: attr('boolean', { - defaultValue: false, - label: 'Issue StartTLS command after establishing an unencrypted connection', - }), - tlsMinVersion: attr('string', { - label: 'Minimum TLS Version', - defaultValue: 'tls12', - possibleValues: ['tls10', 'tls11', 'tls12'], - }), - - tlsMaxVersion: attr('string', { - label: 'Maximum TLS Version', - defaultValue: 'tls12', - possibleValues: ['tls10', 'tls11', 'tls12'], - }), - insecureTls: attr('boolean', { - defaultValue: false, - label: 'Skip LDAP server SSL certificate verification', - }), - certificate: attr('string', { - label: 'CA certificate to verify LDAP server certificate', - editType: 'file', - }), - + useOpenAPI: true, binddn: attr('string', { - label: 'Name of Object to bind (binddn)', helpText: 'Used when performing user search. Example: cn=vault,ou=Users,dc=example,dc=com', }), bindpass: attr('string', { - label: 'Password', helpText: 'Used along with binddn when performing user search', sensitive: true, }), - userdn: attr('string', { - label: 'User DN', helpText: 'Base DN under which to perform user search. Example: ou=Users,dc=example,dc=com', }), userattr: attr('string', { - label: 'User Attribute', - defaultValue: 'cn', helpText: 'Attribute on user attribute object matching the username passed when authenticating. Examples: sAMAccountName, cn, uid', }), - discoverdn: attr('boolean', { - defaultValue: false, - label: 'Use anonymous bind to discover the bind DN of a user', - }), - denyNullBind: attr('boolean', { - defaultValue: true, - label: 'Prevent users from bypassing authentication when providing an empty password', - }), upndomain: attr('string', { - label: 'User Principal (UPN) Domain', helpText: 'The userPrincipalDomain used to construct the UPN string for the authenticating user. The constructed UPN will appear as [username]@UPNDomain. Example: example.com, which will cause vault to bind as username@example.com.', }), groupfilter: attr('string', { - label: 'Group Filter', helpText: 'Go template used when constructing the group membership query. The template can access the following context variables: [UserDN, Username]. The default is (|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}})), which is compatible with several common directory schemas. To support nested group resolution for Active Directory, instead use the following query: (&(objectClass=group)(member:1.2.840.113556.1.4.1941:={{.UserDN}}))', }), groupdn: attr('string', { - label: 'Group DN', helpText: 'LDAP search base for group membership search. This can be the root containing either groups or users. Example: ou=Groups,dc=example,dc=com', }), groupattr: attr('string', { - label: 'Group Attribute', - defaultValue: 'cn', - helpText: 'LDAP attribute to follow on objects returned by groupfilter in order to enumerate user group membership. Examples: for groupfilter queries returning group objects, use: cn. For queries returning user objects, use: memberOf. The default is cn.', }), useTokenGroups: attr('boolean', { - defaultValue: false, - label: 'Use Token Groups', helpText: 'Use the Active Directory tokenGroups constructed attribute to find the group memberships. This returns all security groups for the user, including nested groups. In an Active Directory environment with a large number of groups this method offers increased performance. Selecting this will cause Group DN, Attribute, and Filter to be ignored.', }), fieldGroups: computed(function() { - const groups = [ + let groups = [ { default: ['url'], }, @@ -117,6 +70,9 @@ export default AuthConfig.extend({ 'Customize Group Membership Search': ['groupfilter', 'groupattr', 'groupdn', 'useTokenGroups'], }, ]; + if (this.newFields) { + groups = combineFieldGroups(groups, this.newFields, []); + } return fieldToAttrs(this, groups); }), }); diff --git a/ui/app/models/auth-config/okta.js b/ui/app/models/auth-config/okta.js index 8796db7c5bf7..ae76e70777b1 100644 --- a/ui/app/models/auth-config/okta.js +++ b/ui/app/models/auth-config/okta.js @@ -2,32 +2,31 @@ import { computed } from '@ember/object'; import DS from 'ember-data'; import AuthConfig from '../auth-config'; import fieldToAttrs from 'vault/utils/field-to-attrs'; +import { combineFieldGroups } from 'vault/utils/openapi-to-attrs'; const { attr } = DS; export default AuthConfig.extend({ + useOpenAPI: true, orgName: attr('string', { - label: 'Organization Name', helpText: 'Name of the organization to be used in the Okta API', }), apiToken: attr('string', { - label: 'API Token', helpText: 'Okta API token. This is required to query Okta for user group membership. If this is not supplied only locally configured groups will be enabled.', }), baseUrl: attr('string', { - label: 'Base URL', helpText: 'If set, will be used as the base domain for API requests. Examples are okta.com, oktapreview.com, and okta-emea.com', }), bypassOktaMfa: attr('boolean', { defaultValue: false, - label: 'Bypass Okta MFA', helpText: "Useful if Vault's built-in MFA mechanisms. Will also cause certain other statuses to be ignored, such as PASSWORD_EXPIRED", }), + fieldGroups: computed(function() { - const groups = [ + let groups = [ { default: ['orgName'], }, @@ -35,6 +34,10 @@ export default AuthConfig.extend({ Options: ['apiToken', 'baseUrl', 'bypassOktaMfa'], }, ]; + if (this.newFields) { + groups = combineFieldGroups(groups, this.newFields, []); + } + return fieldToAttrs(this, groups); }), }); diff --git a/ui/app/models/auth-config/radius.js b/ui/app/models/auth-config/radius.js index d88088a3f408..7479c5a8b743 100644 --- a/ui/app/models/auth-config/radius.js +++ b/ui/app/models/auth-config/radius.js @@ -1,38 +1,18 @@ import { computed } from '@ember/object'; import DS from 'ember-data'; import AuthConfig from '../auth-config'; +import { combineFieldGroups } from 'vault/utils/openapi-to-attrs'; import fieldToAttrs from 'vault/utils/field-to-attrs'; const { attr } = DS; export default AuthConfig.extend({ + useOpenAPI: true, host: attr('string'), - - port: attr('number', { - defaultValue: 1812, - }), - secret: attr('string'), - unregisteredUserPolicies: attr('string', { - label: 'Policies for unregistered users', - }), - - dialTimeout: attr('number', { - defaultValue: 10, - }), - - nasPort: attr('number', { - defaultValue: 10, - label: 'NAS Port', - }), - - nasIdentifier: attr('string', { - label: 'NAS Identifier', - }), - fieldGroups: computed(function() { - const groups = [ + let groups = [ { default: ['host', 'secret'], }, @@ -40,6 +20,10 @@ export default AuthConfig.extend({ 'RADIUS Options': ['port', 'nasPort', 'nasIdentifier', 'dialTimeout', 'unregisteredUserPolicies'], }, ]; + if (this.newFields) { + groups = combineFieldGroups(groups, this.newFields, []); + } + return fieldToAttrs(this, groups); }), }); diff --git a/ui/app/models/pki-certificate-sign.js b/ui/app/models/pki-certificate-sign.js index 58bf2c35be76..8fa57d31eb41 100644 --- a/ui/app/models/pki-certificate-sign.js +++ b/ui/app/models/pki-certificate-sign.js @@ -2,7 +2,7 @@ import { copy } from 'ember-copy'; import { computed } from '@ember/object'; import DS from 'ember-data'; import Certificate from './pki-certificate'; - +import { combineFieldGroups } from 'vault/utils/openapi-to-attrs'; const { attr } = DS; export default Certificate.extend({ @@ -10,7 +10,7 @@ export default Certificate.extend({ readOnly: true, defaultValue: false, }), - + useOpenAPI: true, csr: attr('string', { label: 'Certificate Signing Request (CSR)', editType: 'textarea', @@ -18,11 +18,14 @@ export default Certificate.extend({ fieldGroups: computed('signVerbatim', function() { const options = { Options: ['altNames', 'ipSans', 'ttl', 'excludeCnFromSans', 'otherSans'] }; - const groups = [ + let groups = [ { default: ['csr', 'commonName', 'format', 'signVerbatim'], }, ]; + if (this.newFields) { + groups = combineFieldGroups(groups, this.newFields, []); + } if (this.get('signVerbatim') === false) { groups.push(options); } diff --git a/ui/app/models/role-aws.js b/ui/app/models/role-aws.js index a5be5a82d7e4..6f740dbc5bc2 100644 --- a/ui/app/models/role-aws.js +++ b/ui/app/models/role-aws.js @@ -29,6 +29,7 @@ export default DS.Model.extend({ fieldValue: 'id', readOnly: true, }), + useOpenAPI: false, // credentialTypes are for backwards compatibility. // we use this to populate "credentialType" in // the serializer. if there is more than one, the @@ -52,17 +53,15 @@ export default DS.Model.extend({ editType: 'json', }), fields: computed('credentialType', function() { - let keys; - let credentialType = this.get('credentialType'); + let credentialType = this.credentialType; let keysForType = { iam_user: ['name', 'credentialType', 'policyArns', 'policyDocument'], assumed_role: ['name', 'credentialType', 'roleArns', 'policyDocument'], federation_token: ['name', 'credentialType', 'policyDocument'], }; - keys = keysForType[credentialType]; - return expandAttributeMeta(this, keys); - }), + return expandAttributeMeta(this, keysForType[credentialType]); + }), updatePath: lazyCapabilities(apiPath`${'backend'}/roles/${'id'}`, 'backend', 'id'), canDelete: alias('updatePath.canDelete'), canEdit: alias('updatePath.canUpdate'), diff --git a/ui/app/models/role-pki.js b/ui/app/models/role-pki.js index 77fe609d7df6..3f4459f55c94 100644 --- a/ui/app/models/role-pki.js +++ b/ui/app/models/role-pki.js @@ -3,7 +3,7 @@ import { computed } from '@ember/object'; import DS from 'ember-data'; import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; import fieldToAttrs from 'vault/utils/field-to-attrs'; - +import { combineFieldGroups } from 'vault/utils/openapi-to-attrs'; const { attr } = DS; export default DS.Model.extend({ @@ -15,101 +15,10 @@ export default DS.Model.extend({ fieldValue: 'id', readOnly: true, }), - keyType: attr('string', { - possibleValues: ['rsa', 'ec'], - }), - ttl: attr({ - label: 'TTL', - editType: 'ttl', - }), - maxTtl: attr({ - label: 'Max TTL', - editType: 'ttl', - }), - allowLocalhost: attr('boolean', {}), - allowedDomains: attr('string', {}), - allowBareDomains: attr('boolean', {}), - allowSubdomains: attr('boolean', {}), - allowGlobDomains: attr('boolean', {}), - allowAnyName: attr('boolean', {}), - enforceHostnames: attr('boolean', {}), - allowIpSans: attr('boolean', { - defaultValue: true, - label: 'Allow clients to request IP Subject Alternative Names (SANs)', - }), - allowedOtherSans: attr({ - editType: 'stringArray', - label: 'Allowed Other SANs', - }), - serverFlag: attr('boolean', { - defaultValue: true, - }), - clientFlag: attr('boolean', { - defaultValue: true, - }), - codeSigningFlag: attr('boolean', {}), - emailProtectionFlag: attr('boolean', {}), - keyBits: attr('number', { - defaultValue: 2048, - }), - keyUsage: attr('string', { - defaultValue: 'DigitalSignature,KeyAgreement,KeyEncipherment', - editType: 'stringArray', - }), - extKeyUsageOids: attr({ - label: 'Custom extended key usage OIDs', - editType: 'stringArray', - }), - requireCn: attr('boolean', { - label: 'Require common name', - defaultValue: true, - }), - useCsrCommonName: attr('boolean', { - label: 'Use CSR common name', - defaultValue: true, - }), - useCsrSans: attr('boolean', { - defaultValue: true, - label: 'Use CSR subject alternative names (SANs)', - }), - ou: attr({ - label: 'OU (OrganizationalUnit)', - editType: 'stringArray', - }), - organization: attr({ - editType: 'stringArray', - }), - country: attr({ - editType: 'stringArray', - }), - locality: attr({ - editType: 'stringArray', - label: 'Locality/City', - }), - province: attr({ - editType: 'stringArray', - label: 'Province/State', - }), - streetAddress: attr({ - editType: 'stringArray', - }), - postalCode: attr({ - editType: 'stringArray', - }), - generateLease: attr('boolean', {}), - noStore: attr('boolean', {}), - policyIdentifiers: attr({ - editType: 'stringArray', - }), - basicConstraintsValidForNonCA: attr('boolean', { - label: 'Mark Basic Constraints valid when issuing non-CA certificates.', - }), - notBeforeDuration: attr({ - label: 'Not Before Duration', - editType: 'ttl', - defaultValue: '30s', - }), - + useOpenAPI: true, + getHelpUrl: function(backend) { + return `/v1/${backend}/roles/example?help=1`; + }, updatePath: lazyCapabilities(apiPath`${'backend'}/roles/${'id'}`, 'backend', 'id'), canDelete: alias('updatePath.canDelete'), canEdit: alias('updatePath.canUpdate'), @@ -125,7 +34,7 @@ export default DS.Model.extend({ canSignVerbatim: alias('signVerbatimPath.canUpdate'), fieldGroups: computed(function() { - const groups = [ + let groups = [ { default: ['name', 'keyType'] }, { Options: [ @@ -167,10 +76,13 @@ export default DS.Model.extend({ ], }, { - Advanced: ['generateLease', 'noStore', 'basicConstraintsValidForNonCA', 'policyIdentifiers'], + Advanced: ['generateLease', 'noStore', 'basicConstraintsValidForNonCa', 'policyIdentifiers'], }, ]; - + let excludedFields = ['extKeyUsage']; + if (this.newFields) { + groups = combineFieldGroups(groups, this.newFields, excludedFields); + } return fieldToAttrs(this, groups); }), }); diff --git a/ui/app/models/role-ssh.js b/ui/app/models/role-ssh.js index 1bb553ab5d6c..fb1d6e052354 100644 --- a/ui/app/models/role-ssh.js +++ b/ui/app/models/role-ssh.js @@ -38,7 +38,12 @@ const CA_FIELDS = [ 'allowUserKeyIds', 'keyIdFormat', ]; + export default DS.Model.extend({ + useOpenAPI: true, + getHelpUrl: function(backend) { + return `/v1/${backend}/roles/example?help=1`; + }, zeroAddress: attr('boolean', { readOnly: true, }), @@ -46,12 +51,12 @@ export default DS.Model.extend({ readOnly: true, }), name: attr('string', { - label: 'Role name', + label: 'Role Name', fieldValue: 'id', readOnly: true, }), keyType: attr('string', { - possibleValues: ['ca', 'otp'], + possibleValues: ['ca', 'otp'], //overriding the API which also lists 'dynamic' as a type though it is deprecated }), adminUser: attr('string', { helpText: 'Username of the admin user at the remote host', @@ -68,25 +73,14 @@ export default DS.Model.extend({ 'List of domains for which a client can request a certificate (e.g. `example.com`, or `*` to allow all)', }), cidrList: attr('string', { - label: 'CIDR list', helpText: 'List of CIDR blocks for which this role is applicable', }), excludeCidrList: attr('string', { - label: 'Exclude CIDR list', helpText: 'List of CIDR blocks that are not accepted by this role', }), port: attr('number', { - defaultValue: 22, helpText: 'Port number for the SSH connection (default is `22`)', }), - ttl: attr({ - label: 'TTL', - editType: 'ttl', - }), - maxTtl: attr({ - label: 'Max TTL', - editType: 'ttl', - }), allowedCriticalOptions: attr('string', { helpText: 'List of critical options that certificates have when signed', }), @@ -114,11 +108,9 @@ export default DS.Model.extend({ 'Specifies if host certificates that are requested are allowed to be subdomains of those listed in Allowed Domains', }), allowUserKeyIds: attr('boolean', { - label: 'Allow user key IDs', helpText: 'Specifies if users can override the key ID for a signed certificate with the "key_id" field', }), keyIdFormat: attr('string', { - label: 'Key ID format', helpText: 'When supplied, this value specifies a custom format for the key id of a signed certificate', }), diff --git a/ui/app/models/ssh-sign.js b/ui/app/models/ssh-sign.js index 6368b9109330..8d8cb76a86ce 100644 --- a/ui/app/models/ssh-sign.js +++ b/ui/app/models/ssh-sign.js @@ -18,7 +18,10 @@ export default DS.Model.extend({ role: attr('object', { readOnly: true, }), - publicKey: attr('string'), + publicKey: attr('string', { + label: 'Public Key', + editType: 'textarea', + }), ttl: attr({ label: 'TTL', editType: 'ttl', diff --git a/ui/app/routes/vault/cluster/secrets/backend.js b/ui/app/routes/vault/cluster/secrets/backend.js index b7964a59b186..9003842e013f 100644 --- a/ui/app/routes/vault/cluster/secrets/backend.js +++ b/ui/app/routes/vault/cluster/secrets/backend.js @@ -2,6 +2,7 @@ import { inject as service } from '@ember/service'; import Route from '@ember/routing/route'; export default Route.extend({ flashMessages: service(), + oldModel: null, model(params) { let { backend } = params; return this.store diff --git a/ui/app/routes/vault/cluster/secrets/backend/credentials.js b/ui/app/routes/vault/cluster/secrets/backend/credentials.js index 655577ca90c0..958f0cb9ccb3 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/credentials.js +++ b/ui/app/routes/vault/cluster/secrets/backend/credentials.js @@ -1,15 +1,28 @@ import { resolve } from 'rsvp'; import Route from '@ember/routing/route'; +import { getOwner } from '@ember/application'; +import { inject as service } from '@ember/service'; const SUPPORTED_DYNAMIC_BACKENDS = ['ssh', 'aws', 'pki']; export default Route.extend({ templateName: 'vault/cluster/secrets/backend/credentials', + pathHelp: service('path-help'), backendModel() { return this.modelFor('vault.cluster.secrets.backend'); }, + beforeModel() { + const { backend } = this.paramsFor('vault.cluster.secrets.backend'); + if (backend != 'ssh') { + return; + } + let modelType = 'ssh-otp-credential'; + let owner = getOwner(this); + return this.pathHelp.getNewModel(modelType, backend, owner); + }, + model(params) { let role = params.secret; let backendModel = this.backendModel(); diff --git a/ui/app/routes/vault/cluster/secrets/backend/list.js b/ui/app/routes/vault/cluster/secrets/backend/list.js index 3f1a876fd368..cdf5e6ac1f86 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/list.js +++ b/ui/app/routes/vault/cluster/secrets/backend/list.js @@ -1,7 +1,9 @@ import { set } from '@ember/object'; import { hash, all } from 'rsvp'; import Route from '@ember/routing/route'; +import { getOwner } from '@ember/application'; import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; +import { inject as service } from '@ember/service'; const SUPPORTED_BACKENDS = supportedSecretBackends(); @@ -19,24 +21,29 @@ export default Route.extend({ }, templateName: 'vault/cluster/secrets/backend/list', + pathHelp: service('path-help'), beforeModel() { - let { backend } = this.paramsFor('vault.cluster.secrets.backend'); + let owner = getOwner(this); let { secret } = this.paramsFor(this.routeName); - let backendModel = this.store.peekRecord('secret-engine', backend); - let type = backendModel && backendModel.get('engineType'); + let { backend, tab } = this.paramsFor('vault.cluster.secrets.backend'); + let secretEngine = this.store.peekRecord('secret-engine', backend); + let type = secretEngine && secretEngine.get('engineType'); if (!type || !SUPPORTED_BACKENDS.includes(type)) { return this.transitionTo('vault.cluster.secrets'); } if (this.routeName === 'vault.cluster.secrets.backend.list' && !secret.endsWith('/')) { return this.replaceWith('vault.cluster.secrets.backend.list', secret + '/'); } - this.store.unloadAll('capabilities'); + let modelType = this.getModelType(backend, tab); + return this.pathHelp.getNewModel(modelType, owner, backend).then(() => { + this.store.unloadAll('capabilities'); + }); }, getModelType(backend, tab) { - let backendModel = this.store.peekRecord('secret-engine', backend); - let type = backendModel.get('engineType'); + let secretEngine = this.store.peekRecord('secret-engine', backend); + let type = secretEngine.get('engineType'); let types = { transit: 'transit-key', ssh: 'role-ssh', @@ -44,8 +51,8 @@ export default Route.extend({ pki: tab === 'certs' ? 'pki-certificate' : 'role-pki', // secret or secret-v2 cubbyhole: 'secret', - kv: backendModel.get('modelTypeForKV'), - generic: backendModel.get('modelTypeForKV'), + kv: secretEngine.get('modelTypeForKV'), + generic: secretEngine.get('modelTypeForKV'), }; return types[type]; }, diff --git a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js index 943ad2a54a0a..f87573a5dee1 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js +++ b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js @@ -1,11 +1,14 @@ import { set } from '@ember/object'; import { hash, resolve } from 'rsvp'; +import { inject as service } from '@ember/service'; +import DS from 'ember-data'; import Route from '@ember/routing/route'; import utils from 'vault/lib/key-utils'; +import { getOwner } from '@ember/application'; import UnloadModelRoute from 'vault/mixins/unload-model-route'; -import DS from 'ember-data'; export default Route.extend(UnloadModelRoute, { + pathHelp: service('path-help'), capabilities(secret) { const { backend } = this.paramsFor('vault.cluster.secrets.backend'); let backendModel = this.modelFor('vault.cluster.secrets.backend'); @@ -35,15 +38,27 @@ export default Route.extend(UnloadModelRoute, { // perhaps in the future we could recurse _for_ users, but for now, just kick them // back to the list const { secret } = this.paramsFor(this.routeName); - const parentKey = utils.parentKeyForKey(secret); - const mode = this.routeName.split('.').pop(); - if (mode === 'edit' && utils.keyIsFolder(secret)) { - if (parentKey) { - return this.transitionTo('vault.cluster.secrets.backend.list', parentKey); - } else { - return this.transitionTo('vault.cluster.secrets.backend.list-root'); + return this.buildModel(secret).then(() => { + const parentKey = utils.parentKeyForKey(secret); + const mode = this.routeName.split('.').pop(); + if (mode === 'edit' && utils.keyIsFolder(secret)) { + if (parentKey) { + return this.transitionTo('vault.cluster.secrets.backend.list', parentKey); + } else { + return this.transitionTo('vault.cluster.secrets.backend.list-root'); + } } + }); + }, + + buildModel(secret) { + const { backend } = this.paramsFor('vault.cluster.secrets.backend'); + let modelType = this.modelType(backend, secret); + if (['secret', 'secret-v2'].includes(modelType)) { + return resolve(); } + let owner = getOwner(this); + return this.pathHelp.getNewModel(modelType, owner, backend); }, modelType(backend, secret) { diff --git a/ui/app/routes/vault/cluster/secrets/backend/sign.js b/ui/app/routes/vault/cluster/secrets/backend/sign.js index 8c33298b01fc..c187a089d183 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/sign.js +++ b/ui/app/routes/vault/cluster/secrets/backend/sign.js @@ -14,6 +14,10 @@ export default Route.extend(UnloadModel, { }; }, + pathForType() { + return 'sign'; + }, + model(params) { const role = params.secret; const backendModel = this.backendModel(); diff --git a/ui/app/routes/vault/cluster/settings/auth/configure/section.js b/ui/app/routes/vault/cluster/settings/auth/configure/section.js index cc7c358d0992..6ee745bebf45 100644 --- a/ui/app/routes/vault/cluster/settings/auth/configure/section.js +++ b/ui/app/routes/vault/cluster/settings/auth/configure/section.js @@ -4,10 +4,12 @@ import Route from '@ember/routing/route'; import RSVP from 'rsvp'; import DS from 'ember-data'; import UnloadModelRoute from 'vault/mixins/unload-model-route'; +import { getOwner } from '@ember/application'; export default Route.extend(UnloadModelRoute, { modelPath: 'model.model', - wizard: service(), + pathHelp: service('path-help'), + modelType(backendType, section) { const MODELS = { 'aws-client': 'auth-config/aws/client', @@ -25,15 +27,22 @@ export default Route.extend(UnloadModelRoute, { return MODELS[`${backendType}-${section}`]; }, + beforeModel() { + const { section_name } = this.paramsFor(this.routeName); + if (section_name === 'options') { + return; + } + const { method } = this.paramsFor('vault.cluster.settings.auth.configure'); + const backend = this.modelFor('vault.cluster.settings.auth.configure'); + const modelType = this.modelType(backend.type, section_name); + let owner = getOwner(this); + return this.pathHelp.getNewModel(modelType, owner, method); + }, + model(params) { const backend = this.modelFor('vault.cluster.settings.auth.configure'); const { section_name: section } = params; if (section === 'options') { - this.get('wizard').transitionFeatureMachine( - this.get('wizard.featureState'), - 'EDIT', - backend.get('type') - ); return RSVP.hash({ model: backend, section, @@ -47,11 +56,6 @@ export default Route.extend(UnloadModelRoute, { } const model = this.store.peekRecord(modelType, backend.id); if (model) { - this.get('wizard').transitionFeatureMachine( - this.get('wizard.featureState'), - 'EDIT', - backend.get('type') - ); return RSVP.hash({ model, section, @@ -60,11 +64,6 @@ export default Route.extend(UnloadModelRoute, { return this.store .findRecord(modelType, backend.id) .then(config => { - this.get('wizard').transitionFeatureMachine( - this.get('wizard.featureState'), - 'EDIT', - backend.get('type') - ); config.set('backend', backend); return RSVP.hash({ model: config, diff --git a/ui/app/services/path-help.js b/ui/app/services/path-help.js new file mode 100644 index 000000000000..e7fa3883b2fd --- /dev/null +++ b/ui/app/services/path-help.js @@ -0,0 +1,59 @@ +/* + This service is used to pull an OpenAPI document describing the + shape of data at a specific path to hydrate a model with attrs it + has less (or no) information about. +*/ +import Service from '@ember/service'; + +import { getOwner } from '@ember/application'; +import { expandOpenApiProps, combineAttributes } from 'vault/utils/openapi-to-attrs'; +import { resolve } from 'rsvp'; + +export function sanitizePath(path) { + //remove whitespace + remove trailing and leading slashes + return path.trim().replace(/^\/+|\/+$/g, ''); +} + +export default Service.extend({ + attrs: null, + ajax(url, options = {}) { + let appAdapter = getOwner(this).lookup(`adapter:application`); + let { data } = options; + return appAdapter.ajax(url, 'GET', { + data, + }); + }, + + //Makes a call to grab the OpenAPI document. + //Returns relevant information from OpenAPI + //as determined by the expandOpenApiProps util + getProps(helpUrl, backend) { + return this.ajax(helpUrl, backend).then(help => { + let path = Object.keys(help.openapi.paths)[0]; + let props = help.openapi.paths[path].post.requestBody.content['application/json'].schema.properties; + return expandOpenApiProps(props); + }); + }, + + getNewModel(modelType, owner, backend) { + let name = `model:${modelType}`; + let newModel = owner.factoryFor(name).class; + if (newModel.merged || newModel.prototype.useOpenAPI !== true) { + return resolve(); + } + let helpUrl = newModel.prototype.getHelpUrl(backend); + + return this.getProps(helpUrl, backend).then(props => { + if (owner.hasRegistration(name) && !newModel.merged) { + let { attrs, newFields } = combineAttributes(newModel.attributes, props); + newModel = newModel.extend(attrs, { newFields }); + } else { + //generate a whole new model + } + + newModel.reopenClass({ merged: true }); + owner.unregister(name); + owner.register(name, newModel); + }); + }, +}); diff --git a/ui/app/templates/components/auth-config-form/config.hbs b/ui/app/templates/components/auth-config-form/config.hbs index 769d98251295..4f9d55fd6079 100644 --- a/ui/app/templates/components/auth-config-form/config.hbs +++ b/ui/app/templates/components/auth-config-form/config.hbs @@ -11,8 +11,13 @@ {{/if}}
-
- + \ No newline at end of file diff --git a/ui/app/templates/components/auth-config-form/options.hbs b/ui/app/templates/components/auth-config-form/options.hbs index 3c7cc9b0bb84..b7630d2856e3 100644 --- a/ui/app/templates/components/auth-config-form/options.hbs +++ b/ui/app/templates/components/auth-config-form/options.hbs @@ -7,8 +7,13 @@ {{/each}}
-
- + \ No newline at end of file diff --git a/ui/app/templates/components/form-field.hbs b/ui/app/templates/components/form-field.hbs index c84e51cb77e6..f2d363158fa7 100644 --- a/ui/app/templates/components/form-field.hbs +++ b/ui/app/templates/components/form-field.hbs @@ -1,7 +1,19 @@ {{#unless (or - (and attr.options.editType (not-eq attr.options.editType "textarea")) (eq attr.type "boolean") + (contains + attr.options.editType + (array + "boolean" + "searchSelect" + "mountAccessor" + "kv" + "file" + "ttl" + "stringArray" + "json" + ) + ) ) }}