diff --git a/builtin/credential/cert/path_certs.go b/builtin/credential/cert/path_certs.go index 0a57f347be8e..fc1876632339 100644 --- a/builtin/credential/cert/path_certs.go +++ b/builtin/credential/cert/path_certs.go @@ -23,6 +23,10 @@ func pathListCerts(b *backend) *framework.Path { HelpSynopsis: pathCertHelpSyn, HelpDescription: pathCertHelpDesc, + DisplayAttrs: &framework.DisplayAttributes{ + Navigation: true, + ItemType: "Certificate", + }, } } @@ -39,6 +43,9 @@ func pathCerts(b *backend) *framework.Path { Type: framework.TypeString, Description: `The public certificate that should be trusted. Must be x509 PEM encoded.`, + DisplayAttrs: &framework.DisplayAttributes{ + EditType: "file", + }, }, "allowed_names": &framework.FieldSchema{ @@ -47,36 +54,57 @@ Must be x509 PEM encoded.`, At least one must exist in either the Common Name or SANs. Supports globbing. This parameter is deprecated, please use allowed_common_names, allowed_dns_sans, allowed_email_sans, allowed_uri_sans.`, + DisplayAttrs: &framework.DisplayAttributes{ + Group: "Constraints", + }, }, "allowed_common_names": &framework.FieldSchema{ Type: framework.TypeCommaStringSlice, Description: `A comma-separated list of names. At least one must exist in the Common Name. Supports globbing.`, + DisplayAttrs: &framework.DisplayAttributes{ + Group: "Constraints", + }, }, "allowed_dns_sans": &framework.FieldSchema{ Type: framework.TypeCommaStringSlice, Description: `A comma-separated list of DNS names. At least one must exist in the SANs. Supports globbing.`, + DisplayAttrs: &framework.DisplayAttributes{ + Name: "Allowed DNS SANs", + Group: "Constraints", + }, }, "allowed_email_sans": &framework.FieldSchema{ Type: framework.TypeCommaStringSlice, Description: `A comma-separated list of Email Addresses. At least one must exist in the SANs. Supports globbing.`, + DisplayAttrs: &framework.DisplayAttributes{ + Name: "Allowed Email SANs", + Group: "Constraints", + }, }, "allowed_uri_sans": &framework.FieldSchema{ Type: framework.TypeCommaStringSlice, Description: `A comma-separated list of URIs. At least one must exist in the SANs. Supports globbing.`, + DisplayAttrs: &framework.DisplayAttributes{ + Name: "Allowed URI SANs", + Group: "Constraints", + }, }, "allowed_organizational_units": &framework.FieldSchema{ Type: framework.TypeCommaStringSlice, Description: `A comma-separated list of Organizational Units names. At least one must exist in the OU field.`, + DisplayAttrs: &framework.DisplayAttributes{ + Group: "Constraints", + }, }, "required_extensions": &framework.FieldSchema{ @@ -137,6 +165,10 @@ certificate.`, HelpSynopsis: pathCertHelpSyn, HelpDescription: pathCertHelpDesc, + DisplayAttrs: &framework.DisplayAttributes{ + Action: "Create", + ItemType: "Certificate", + }, } tokenutil.AddTokenFields(p.Fields) diff --git a/builtin/credential/ldap/path_groups.go b/builtin/credential/ldap/path_groups.go index 76e64c4505a0..b39691cf8174 100644 --- a/builtin/credential/ldap/path_groups.go +++ b/builtin/credential/ldap/path_groups.go @@ -21,6 +21,7 @@ func pathGroupsList(b *backend) *framework.Path { HelpDescription: pathGroupHelpDesc, DisplayAttrs: &framework.DisplayAttributes{ Navigation: true, + ItemType: "Group", }, } } @@ -49,7 +50,8 @@ func pathGroups(b *backend) *framework.Path { HelpSynopsis: pathGroupHelpSyn, HelpDescription: pathGroupHelpDesc, DisplayAttrs: &framework.DisplayAttributes{ - Action: "Create", + Action: "Create", + ItemType: "Group", }, } } diff --git a/builtin/credential/ldap/path_users.go b/builtin/credential/ldap/path_users.go index 60276cf64e5e..2cfd34d267a1 100644 --- a/builtin/credential/ldap/path_users.go +++ b/builtin/credential/ldap/path_users.go @@ -22,7 +22,7 @@ func pathUsersList(b *backend) *framework.Path { HelpDescription: pathUserHelpDesc, DisplayAttrs: &framework.DisplayAttributes{ Navigation: true, - Action: "Create", + ItemType: "User", }, } } @@ -56,7 +56,8 @@ func pathUsers(b *backend) *framework.Path { HelpSynopsis: pathUserHelpSyn, HelpDescription: pathUserHelpDesc, DisplayAttrs: &framework.DisplayAttributes{ - Action: "Create", + Action: "Create", + ItemType: "User", }, } } diff --git a/builtin/credential/okta/path_config.go b/builtin/credential/okta/path_config.go index 3cbf1041e2ba..5c5dd3a68cf5 100644 --- a/builtin/credential/okta/path_config.go +++ b/builtin/credential/okta/path_config.go @@ -87,6 +87,9 @@ func pathConfig(b *backend) *framework.Path { ExistenceCheck: b.pathConfigExistenceCheck, HelpSynopsis: pathConfigHelp, + DisplayAttrs: &framework.DisplayAttributes{ + Action: "Configure", + }, } tokenutil.AddTokenFields(p.Fields) diff --git a/builtin/credential/okta/path_groups.go b/builtin/credential/okta/path_groups.go index 83742035035f..9ba36b282af8 100644 --- a/builtin/credential/okta/path_groups.go +++ b/builtin/credential/okta/path_groups.go @@ -19,6 +19,10 @@ func pathGroupsList(b *backend) *framework.Path { HelpSynopsis: pathGroupHelpSyn, HelpDescription: pathGroupHelpDesc, + DisplayAttrs: &framework.DisplayAttributes{ + Navigation: true, + ItemType: "Group", + }, } } @@ -45,6 +49,10 @@ func pathGroups(b *backend) *framework.Path { HelpSynopsis: pathGroupHelpSyn, HelpDescription: pathGroupHelpDesc, + DisplayAttrs: &framework.DisplayAttributes{ + Action: "Create", + ItemType: "Group", + }, } } diff --git a/builtin/credential/okta/path_users.go b/builtin/credential/okta/path_users.go index d928b56e72b7..d3408ee42f97 100644 --- a/builtin/credential/okta/path_users.go +++ b/builtin/credential/okta/path_users.go @@ -17,6 +17,10 @@ func pathUsersList(b *backend) *framework.Path { HelpSynopsis: pathUserHelpSyn, HelpDescription: pathUserHelpDesc, + DisplayAttrs: &framework.DisplayAttributes{ + Navigation: true, + ItemType: "User", + }, } } @@ -48,6 +52,10 @@ func pathUsers(b *backend) *framework.Path { HelpSynopsis: pathUserHelpSyn, HelpDescription: pathUserHelpDesc, + DisplayAttrs: &framework.DisplayAttributes{ + Action: "Create", + ItemType: "User", + }, } } diff --git a/builtin/credential/radius/path_config.go b/builtin/credential/radius/path_config.go index 28476e337a2f..aa0e730486c3 100644 --- a/builtin/credential/radius/path_config.go +++ b/builtin/credential/radius/path_config.go @@ -85,6 +85,9 @@ func pathConfig(b *backend) *framework.Path { HelpSynopsis: pathConfigHelpSyn, HelpDescription: pathConfigHelpDesc, + DisplayAttrs: &framework.DisplayAttributes{ + Action: "Configure", + }, } tokenutil.AddTokenFields(p.Fields) diff --git a/builtin/credential/radius/path_users.go b/builtin/credential/radius/path_users.go index 3a48788cb8b6..470513f4db41 100644 --- a/builtin/credential/radius/path_users.go +++ b/builtin/credential/radius/path_users.go @@ -20,6 +20,10 @@ func pathUsersList(b *backend) *framework.Path { HelpSynopsis: pathUserHelpSyn, HelpDescription: pathUserHelpDesc, + DisplayAttrs: &framework.DisplayAttributes{ + Navigation: true, + ItemType: "User", + }, } } @@ -49,6 +53,10 @@ func pathUsers(b *backend) *framework.Path { HelpSynopsis: pathUserHelpSyn, HelpDescription: pathUserHelpDesc, + DisplayAttrs: &framework.DisplayAttributes{ + Action: "Create", + ItemType: "User", + }, } } diff --git a/builtin/credential/userpass/path_users.go b/builtin/credential/userpass/path_users.go index 140025acc8e3..1c43e97d2d7f 100644 --- a/builtin/credential/userpass/path_users.go +++ b/builtin/credential/userpass/path_users.go @@ -22,6 +22,10 @@ func pathUsersList(b *backend) *framework.Path { HelpSynopsis: pathUserHelpSyn, HelpDescription: pathUserHelpDesc, + DisplayAttrs: &framework.DisplayAttributes{ + Navigation: true, + ItemType: "User", + }, } } @@ -37,6 +41,9 @@ func pathUsers(b *backend) *framework.Path { "password": &framework.FieldSchema{ Type: framework.TypeString, Description: "Password for this user.", + DisplayAttrs: &framework.DisplayAttributes{ + Sensitive: true, + }, }, "policies": &framework.FieldSchema{ @@ -75,6 +82,10 @@ func pathUsers(b *backend) *framework.Path { HelpSynopsis: pathUserHelpSyn, HelpDescription: pathUserHelpDesc, + DisplayAttrs: &framework.DisplayAttributes{ + Action: "Create", + ItemType: "User", + }, } tokenutil.AddTokenFields(p.Fields) diff --git a/sdk/framework/path.go b/sdk/framework/path.go index a8e5f56ffffb..d6fc6e935732 100644 --- a/sdk/framework/path.go +++ b/sdk/framework/path.go @@ -173,11 +173,18 @@ type DisplayAttributes struct { // Navigation indicates that the path should be available as a navigation tab Navigation bool `json:"navigation,omitempty"` + // ItemType is the type of item this path operates on + ItemType string `json:"itemType,omitempty"` + // Group is the suggested UI group to place this field in. Group string `json:"group,omitempty"` // Action is the verb to use for the operation. Action string `json:"action,omitempty"` + + // EditType is the type of form field needed for a property + // e.g. "textarea" or "file" + EditType string `json:"editType,omitempty"` } // RequestExample is example of request data. diff --git a/sdk/helper/tokenutil/tokenutil.go b/sdk/helper/tokenutil/tokenutil.go index e225f866192d..67fcba2bcd67 100644 --- a/sdk/helper/tokenutil/tokenutil.go +++ b/sdk/helper/tokenutil/tokenutil.go @@ -76,6 +76,7 @@ func TokenFields() map[string]*framework.FieldSchema { Description: `Comma separated string or JSON list of CIDR blocks. If set, specifies the blocks of IP addresses which are allowed to use the generated token.`, DisplayAttrs: &framework.DisplayAttributes{ Name: "Generated Token's Bound CIDRs", + Group: "Tokens", }, }, @@ -84,6 +85,7 @@ func TokenFields() map[string]*framework.FieldSchema { Description: tokenExplicitMaxTTLHelp, DisplayAttrs: &framework.DisplayAttributes{ Name: "Generated Token's Explicit Maximum TTL", + Group: "Tokens", }, }, @@ -92,6 +94,7 @@ func TokenFields() map[string]*framework.FieldSchema { Description: "The maximum lifetime of the generated token", DisplayAttrs: &framework.DisplayAttributes{ Name: "Generated Token's Maximum TTL", + Group: "Tokens", }, }, @@ -100,6 +103,7 @@ func TokenFields() map[string]*framework.FieldSchema { Description: "If true, the 'default' policy will not automatically be added to generated tokens", DisplayAttrs: &framework.DisplayAttributes{ Name: "Do Not Attach 'default' Policy To Generated Tokens", + Group: "Tokens", }, }, @@ -108,6 +112,7 @@ func TokenFields() map[string]*framework.FieldSchema { Description: tokenPeriodHelp, DisplayAttrs: &framework.DisplayAttributes{ Name: "Generated Token's Period", + Group: "Tokens", }, }, @@ -116,6 +121,7 @@ func TokenFields() map[string]*framework.FieldSchema { Description: "Comma-separated list of policies", DisplayAttrs: &framework.DisplayAttributes{ Name: "Generated Token's Policies", + Group: "Tokens", }, }, @@ -125,6 +131,7 @@ func TokenFields() map[string]*framework.FieldSchema { Description: "The type of token to generate, service or batch", DisplayAttrs: &framework.DisplayAttributes{ Name: "Generated Token's Type", + Group: "Tokens", }, }, @@ -133,6 +140,7 @@ func TokenFields() map[string]*framework.FieldSchema { Description: "The initial ttl of the token to generate", DisplayAttrs: &framework.DisplayAttributes{ Name: "Generated Token's Initial TTL", + Group: "Tokens", }, }, @@ -141,6 +149,7 @@ func TokenFields() map[string]*framework.FieldSchema { Description: "The maximum number of times a token may be used, a value of zero means unlimited", DisplayAttrs: &framework.DisplayAttributes{ Name: "Maximum Uses of Generated Tokens", + Group: "Tokens", }, }, } diff --git a/ui/.storybook/preview-head.html b/ui/.storybook/preview-head.html index 1fbfe45032c4..d35e63834d9b 100644 --- a/ui/.storybook/preview-head.html +++ b/ui/.storybook/preview-head.html @@ -2,7 +2,8 @@ - + @@ -23,4 +24,4 @@ - \ No newline at end of file + diff --git a/ui/app/adapters/generated-item-list.js b/ui/app/adapters/generated-item-list.js index 313985be9d78..215631c175a2 100644 --- a/ui/app/adapters/generated-item-list.js +++ b/ui/app/adapters/generated-item-list.js @@ -4,29 +4,25 @@ import ApplicationAdapter from './application'; export default ApplicationAdapter.extend({ namespace: 'v1', urlForItem() {}, - optionsForQuery(id) { + + fetchByQuery(store, query, isList) { + const { id } = query; let data = {}; - if (!id) { - data['list'] = true; + if (isList) { + data.list = true; } - return { data }; - }, - fetchByQuery(store, query) { - const { id, method, type } = query; - return this.ajax(this.urlForItem(method, id, type), 'GET', this.optionsForQuery(id)).then(resp => { + return this.ajax(this.urlForItem(id, isList), 'GET', { data }).then(resp => { const data = { id, - name: id, - method, + method: id, }; - return assign({}, resp, data); }); }, query(store, type, query) { - return this.fetchByQuery(store, query); + return this.fetchByQuery(store, query, true); }, queryRecord(store, type, query) { diff --git a/ui/app/components/generated-item.js b/ui/app/components/generated-item.js index ccf815c8806d..e8ea6c0349ce 100644 --- a/ui/app/components/generated-item.js +++ b/ui/app/components/generated-item.js @@ -39,13 +39,13 @@ export default Component.extend({ return; } this.router.transitionTo('vault.cluster.access.method.item.list').followRedirects(); - this.flashMessages.success(`The ${this.itemType} configuration was saved successfully.`); + this.flashMessages.success(`Successfully saved ${this.itemType} ${this.model.id}.`); }).withTestWaiter(), actions: { deleteItem() { this.model.destroyRecord().then(() => { this.router.transitionTo('vault.cluster.access.method.item.list').followRedirects(); - this.flashMessages.success(`${this.model.id} ${this.itemType} was deleted successfully.`); + this.flashMessages.success(`Successfully deleted ${this.itemType} ${this.model.id}.`); }); }, }, diff --git a/ui/app/helpers/supported-managed-auth-backends.js b/ui/app/helpers/supported-managed-auth-backends.js new file mode 100644 index 000000000000..ee668c24b882 --- /dev/null +++ b/ui/app/helpers/supported-managed-auth-backends.js @@ -0,0 +1,9 @@ +import { helper as buildHelper } from '@ember/component/helper'; + +const MANAGED_AUTH_BACKENDS = ['okta', 'radius', 'ldap', 'cert', 'userpass']; + +export function supportedManagedAuthBackends() { + return MANAGED_AUTH_BACKENDS; +} + +export default buildHelper(supportedManagedAuthBackends); diff --git a/ui/app/helpers/tabs-for-auth-section.js b/ui/app/helpers/tabs-for-auth-section.js index c66ed62220d6..84a77fd04a30 100644 --- a/ui/app/helpers/tabs-for-auth-section.js +++ b/ui/app/helpers/tabs-for-auth-section.js @@ -85,12 +85,17 @@ export function tabsForAuthSection([model, sectionType = 'authSettings', paths]) }); return tabs; } - if (paths) { - tabs = paths.map(path => { - let itemName = path.slice(1); //get rid of leading slash + if (paths || model.paths) { + if (model.paths) { + paths = model.paths.paths.filter(path => path.navigation); + } + + // TODO: we're unsure if we actually need compact here + // but are leaving it just in case OpenAPI ever returns an empty thing + tabs = paths.compact().map(path => { return { - label: capitalize(pluralize(itemName)), - routeParams: ['vault.cluster.access.method.item.list', itemName], + label: capitalize(pluralize(path.itemName)), + routeParams: ['vault.cluster.access.method.item.list', path.itemType], }; }); } else { diff --git a/ui/app/routes/vault/cluster/access/method/index.js b/ui/app/routes/vault/cluster/access/method/index.js index a3bff252cb83..b97b96de37aa 100644 --- a/ui/app/routes/vault/cluster/access/method/index.js +++ b/ui/app/routes/vault/cluster/access/method/index.js @@ -3,7 +3,7 @@ import { tabsForAuthSection } from 'vault/helpers/tabs-for-auth-section'; export default Route.extend({ beforeModel() { let { methodType, paths } = this.modelFor('vault.cluster.access.method'); - paths = paths ? paths.navPaths.reduce((acc, cur) => acc.concat(cur.path), []) : null; + paths = paths ? paths.paths.filter(path => path.navigation === true) : null; const activeTab = tabsForAuthSection([methodType, 'authConfig', paths])[0].routeParams; return this.transitionTo(...activeTab); }, diff --git a/ui/app/routes/vault/cluster/access/method/item.js b/ui/app/routes/vault/cluster/access/method/item.js index 45b52698d7a9..e3532fbd694a 100644 --- a/ui/app/routes/vault/cluster/access/method/item.js +++ b/ui/app/routes/vault/cluster/access/method/item.js @@ -7,26 +7,29 @@ export default Route.extend({ pathHelp: service('path-help'), beforeModel() { - const { apiPath, type, method, itemType } = this.getMethodAndModelInfo(); + const { apiPath, type, authMethodPath, itemType } = this.getMethodAndModelInfo(); let modelType = `generated-${singularize(itemType)}-${type}`; - return this.pathHelp.getNewModel(modelType, method, apiPath, itemType); + return this.pathHelp.getNewModel(modelType, authMethodPath, apiPath, itemType); }, getMethodAndModelInfo() { const { item_type: itemType } = this.paramsFor(this.routeName); - const { path: method } = this.paramsFor('vault.cluster.access.method'); + const { path: authMethodPath } = this.paramsFor('vault.cluster.access.method'); const methodModel = this.modelFor('vault.cluster.access.method'); const { apiPath, type } = methodModel; - return { apiPath, type, method, itemType }; + return { apiPath, type, authMethodPath, itemType }; }, setupController(controller) { this._super(...arguments); - const { apiPath, method, itemType } = this.getMethodAndModelInfo(); + const { apiPath, authMethodPath, itemType } = this.getMethodAndModelInfo(); controller.set('itemType', itemType); - controller.set('method', method); - this.pathHelp.getPaths(apiPath, method, itemType).then(paths => { - controller.set('paths', Array.from(paths.list, pathInfo => pathInfo.path)); + this.pathHelp.getPaths(apiPath, authMethodPath, itemType).then(paths => { + let navigationPaths = paths.paths.filter(path => path.navigation); + controller.set( + 'paths', + navigationPaths.filter(path => path.itemType.includes(itemType)).map(path => path.path) + ); }); }, }); diff --git a/ui/app/routes/vault/cluster/access/method/item/edit.js b/ui/app/routes/vault/cluster/access/method/item/edit.js index 8d9126f2a9e0..6841e03b1a7d 100644 --- a/ui/app/routes/vault/cluster/access/method/item/edit.js +++ b/ui/app/routes/vault/cluster/access/method/item/edit.js @@ -5,21 +5,16 @@ import { singularize } from 'ember-inflector'; export default Route.extend(UnloadModelRoute, UnsavedModelRoute, { model(params) { - const methodModel = this.modelFor('vault.cluster.access.method'); - const { type } = methodModel; + const id = params.item_id; const { item_type: itemType } = this.paramsFor('vault.cluster.access.method.item'); - let modelType = `generated-${singularize(itemType)}-${type}`; - return this.store.findRecord(modelType, params.item_id); + const methodModel = this.modelFor('vault.cluster.access.method'); + const modelType = `generated-${singularize(itemType)}-${methodModel.type}`; + return this.store.queryRecord(modelType, { id, authMethodPath: methodModel.id }); }, setupController(controller) { this._super(...arguments); const { item_type: itemType } = this.paramsFor('vault.cluster.access.method.item'); - const { path: method } = this.paramsFor('vault.cluster.access.method'); - const { item_id: itemName } = this.paramsFor(this.routeName); controller.set('itemType', singularize(itemType)); - controller.set('mode', 'edit'); - controller.set('method', method); - controller.set('itemName', itemName); }, }); diff --git a/ui/app/routes/vault/cluster/access/method/item/list.js b/ui/app/routes/vault/cluster/access/method/item/list.js index bbb732dbcf83..2b7eb447db23 100644 --- a/ui/app/routes/vault/cluster/access/method/item/list.js +++ b/ui/app/routes/vault/cluster/access/method/item/list.js @@ -9,14 +9,14 @@ export default Route.extend(ListRoute, { getMethodAndModelInfo() { const { item_type: itemType } = this.paramsFor('vault.cluster.access.method.item'); - const { path: method } = this.paramsFor('vault.cluster.access.method'); + const { path: authMethodPath } = this.paramsFor('vault.cluster.access.method'); const methodModel = this.modelFor('vault.cluster.access.method'); const { apiPath, type } = methodModel; - return { apiPath, type, method, itemType }; + return { apiPath, type, authMethodPath, itemType, methodModel }; }, model() { - const { type, method, itemType } = this.getMethodAndModelInfo(); + const { type, authMethodPath, itemType } = this.getMethodAndModelInfo(); const { page, pageFilter } = this.paramsFor(this.routeName); let modelType = `generated-${singularize(itemType)}-${type}`; @@ -26,7 +26,7 @@ export default Route.extend(ListRoute, { page: page, pageFilter: pageFilter, type: itemType, - method: method, + id: authMethodPath, }) .catch(err => { if (err.httpStatus === 404) { @@ -51,11 +51,14 @@ export default Route.extend(ListRoute, { }, setupController(controller) { this._super(...arguments); - const { apiPath, method, itemType } = this.getMethodAndModelInfo(); + const { apiPath, authMethodPath, itemType, methodModel } = this.getMethodAndModelInfo(); controller.set('itemType', itemType); - controller.set('method', method); - this.pathHelp.getPaths(apiPath, method, itemType).then(paths => { - controller.set('paths', paths.navPaths.reduce((acc, cur) => acc.concat(cur.path), [])); + controller.set('methodModel', methodModel); + this.pathHelp.getPaths(apiPath, authMethodPath, itemType).then(paths => { + controller.set( + 'paths', + paths.paths.filter(path => path.navigation && path.itemType.includes(itemType)) + ); }); }, }); diff --git a/ui/app/routes/vault/cluster/access/method/item/show.js b/ui/app/routes/vault/cluster/access/method/item/show.js index a80bc2e9af49..cc6d2995dad6 100644 --- a/ui/app/routes/vault/cluster/access/method/item/show.js +++ b/ui/app/routes/vault/cluster/access/method/item/show.js @@ -4,16 +4,12 @@ import Route from '@ember/routing/route'; export default Route.extend({ pathHelp: service('path-help'), - model() { - const { item_id: itemName } = this.paramsFor(this.routeName); + model(params) { + const id = params.item_id; const { item_type: itemType } = this.paramsFor('vault.cluster.access.method.item'); - const { path: method } = this.paramsFor('vault.cluster.access.method'); const methodModel = this.modelFor('vault.cluster.access.method'); - const { type } = methodModel; - const modelType = `generated-${singularize(itemType)}-${type}`; - return this.store.findRecord(modelType, itemName, { - adapterOptions: { path: `${method}/${itemType}` }, - }); + const modelType = `generated-${singularize(itemType)}-${methodModel.type}`; + return this.store.queryRecord(modelType, { id, authMethodPath: methodModel.id }); }, setupController(controller) { diff --git a/ui/app/routes/vault/cluster/access/method/section.js b/ui/app/routes/vault/cluster/access/method/section.js index ee58779d6ecc..30346bb18de7 100644 --- a/ui/app/routes/vault/cluster/access/method/section.js +++ b/ui/app/routes/vault/cluster/access/method/section.js @@ -24,7 +24,6 @@ export default Route.extend({ this._super(...arguments); controller.set('section', section); let method = this.modelFor('vault.cluster.access.method'); - let paths = method.paths.navPaths.map(pathInfo => pathInfo.path); - controller.set('paths', paths); + controller.set('paths', method.paths.paths.filter(path => path.navigation)); }, }); diff --git a/ui/app/services/path-help.js b/ui/app/services/path-help.js index 60a8b6883f65..9a0aa55270af 100644 --- a/ui/app/services/path-help.js +++ b/ui/app/services/path-help.js @@ -10,12 +10,14 @@ import { getOwner } from '@ember/application'; import { assign } from '@ember/polyfills'; import { expandOpenApiProps, combineAttributes } from 'vault/utils/openapi-to-attrs'; import fieldToAttrs from 'vault/utils/field-to-attrs'; -import { resolve } from 'rsvp'; +import { resolve, reject } from 'rsvp'; import { debug } from '@ember/debug'; +import { dasherize, capitalize } from '@ember/string'; +import { singularize } from 'ember-inflector'; import generatedItemAdapter from 'vault/adapters/generated-item-list'; export function sanitizePath(path) { - //remove whitespace + remove trailing and leading slashes + // remove whitespace + remove trailing and leading slashes return path.trim().replace(/^\/+|\/+$/g, ''); } @@ -34,7 +36,7 @@ export default Service.extend({ const modelName = `model:${modelType}`; const modelFactory = owner.factoryFor(modelName); let newModel, helpUrl; - //if we have a factory, we need to take the existing model into account + // if we have a factory, we need to take the existing model into account if (modelFactory) { debug(`Model factory found for ${modelType}`); newModel = modelFactory.class; @@ -42,111 +44,151 @@ export default Service.extend({ if (newModel.merged || modelProto.useOpenAPI !== true) { return resolve(); } + helpUrl = modelProto.getHelpUrl(backend); return this.registerNewModelWithProps(helpUrl, backend, newModel, modelName); } else { debug(`Creating new Model for ${modelType}`); newModel = DS.Model.extend({}); - //use paths to dynamically create our openapi help url - //if we have a brand new model - return this.getPaths(apiPath, backend, itemType).then(paths => { + } + + // we don't have an apiPath for dynamic secrets + // and we don't need paths for them yet + if (!apiPath) { + helpUrl = newModel.proto().getHelpUrl(backend); + return this.registerNewModelWithProps(helpUrl, backend, newModel, modelName); + } + + // use paths to dynamically create our openapi help url + // if we have a brand new model + return this.getPaths(apiPath, backend, itemType) + .then(pathInfo => { const adapterFactory = owner.factoryFor(`adapter:${modelType}`); - //if we have an adapter already use that, otherwise create one + // if we have an adapter already use that, otherwise create one if (!adapterFactory) { debug(`Creating new adapter for ${modelType}`); - const adapter = this.getNewAdapter(paths, itemType); + const adapter = this.getNewAdapter(pathInfo, itemType); owner.register(`adapter:${modelType}`, adapter); } - //if we have an item we want the create info for that itemType - let path; - if (itemType) { - const createPath = paths.create.find(path => path.path.includes(itemType)); - path = createPath.path; - path = path.slice(0, path.indexOf('{') - 1) + '/example'; - } else { - //we need the mount config - path = paths.configPath[0].path; + let path, paths; + // if we have an item we want the create info for that itemType + paths = itemType ? this.filterPathsByItemType(pathInfo, itemType) : pathInfo.paths; + const createPath = paths.find(path => path.operations.includes('post') && path.action !== 'Delete'); + path = createPath.path; + path = path.includes('{') ? path.slice(0, path.indexOf('{') - 1) + '/example' : path; + if (!path) { + // TODO: we don't know if path will ever be falsey + // if it is never falsey we can remove this. + return reject(); } - helpUrl = `/v1/${apiPath}${path.slice(1)}?help=true`; + + helpUrl = `/v1/${apiPath}${path.slice(1)}?help=true` || newModel.proto().getHelpUrl(backend); + pathInfo.paths = paths; + newModel = newModel.extend({ paths: pathInfo }); return this.registerNewModelWithProps(helpUrl, backend, newModel, modelName); + }) + .catch(err => { + // TODO: we should handle the error better here + console.error(err); }); - } }, - reducePaths(paths, currentPath) { + reducePathsByPathName(pathInfo, currentPath) { const pathName = currentPath[0]; - const pathInfo = currentPath[1]; - - //config is a get/post endpoint that doesn't take route params - //and isn't also a list endpoint and has an Action of Configure - if ( - pathInfo.post && - pathInfo.get && - (pathInfo['x-vault-displayAttrs'] && pathInfo['x-vault-displayAttrs'].action === 'Configure') - ) { - paths.configPath.push({ path: pathName }); - return paths; //config path should only be config path - } + const pathDetails = currentPath[1]; + const displayAttrs = pathDetails['x-vault-displayAttrs']; - //list endpoints all have { name: "list" } in their get parameters - if (pathInfo.get && pathInfo.get.parameters && pathInfo.get.parameters[0].name === 'list') { - paths.list.push({ path: pathName }); + if (!displayAttrs) { + return pathInfo; } - if (pathInfo.delete) { - paths.delete.push({ path: pathName }); + let itemType, itemName; + if (displayAttrs.itemType) { + itemType = displayAttrs.itemType; + let items = itemType.split(':'); + itemName = items[items.length - 1]; + items = items.map(item => dasherize(singularize(item.toLowerCase()))); + itemType = items.join('~*'); } - //create endpoints have path an action (e.g. "Create" or "Generate") - if (pathInfo.post && pathInfo['x-vault-displayAttrs'] && pathInfo['x-vault-displayAttrs'].action) { - paths.create.push({ - path: pathName, - action: pathInfo['x-vault-displayAttrs'].action, - }); + if (itemType && !pathInfo.itemTypes.includes(itemType)) { + pathInfo.itemTypes.push(itemType); } - if (pathInfo['x-vault-displayAttrs'] && pathInfo['x-vault-displayAttrs'].navigation) { - paths.navPaths.push({ path: pathName }); + const operations = []; + if (pathDetails.get) { + operations.push('get'); + } + if (pathDetails.post) { + operations.push('post'); + } + if (pathDetails.delete) { + operations.push('delete'); } + if (pathDetails.get && pathDetails.get.parameters && pathDetails.get.parameters[0].name === 'list') { + operations.push('list'); + } + + pathInfo.paths.push({ + path: pathName, + itemType: itemType || displayAttrs.itemType, + itemName: itemName || pathInfo.itemType || displayAttrs.itemType, + operations, + action: displayAttrs.action, + navigation: displayAttrs.navigation === true, + param: pathName.includes('{') ? pathName.split('{')[1].split('}')[0] : false, + }); - return paths; + return pathInfo; + }, + + filterPathsByItemType(pathInfo, itemType) { + if (!itemType) { + return pathInfo.paths; + } + return pathInfo.paths.filter(path => { + return itemType === path.itemType; + }); }, - getPaths(apiPath, backend) { - debug(`Fetching relevant paths for ${backend} from ${apiPath}`); + getPaths(apiPath, backend, itemType, itemID) { + let debugString = + itemID && itemType + ? `Fetching relevant paths for ${backend} ${itemType} ${itemID} from ${apiPath}` + : `Fetching relevant paths for ${backend} ${itemType} from ${apiPath}`; + debug(debugString); return this.ajax(`/v1/${apiPath}?help=1`, backend).then(help => { const pathInfo = help.openapi.paths; let paths = Object.entries(pathInfo); - return paths.reduce(this.reducePaths, { - apiPath: apiPath, - configPath: [], - list: [], - create: [], - delete: [], - navPaths: [], + return paths.reduce(this.reducePathsByPathName, { + apiPath, + itemType, + itemTypes: [], + paths: [], + itemID, }); }); }, - //Makes a call to grab the OpenAPI document. - //Returns relevant information from OpenAPI - //as determined by the expandOpenApiProps util + // Makes a call to grab the OpenAPI document. + // Returns relevant information from OpenAPI + // as determined by the expandOpenApiProps util getProps(helpUrl, backend) { debug(`Fetching schema properties for ${backend} from ${helpUrl}`); return this.ajax(helpUrl, backend).then(help => { - //paths is an array but it will have a single entry + // paths is an array but it will have a single entry // for the scope we're in const path = Object.keys(help.openapi.paths)[0]; const pathInfo = help.openapi.paths[path]; const params = pathInfo.parameters; let paramProp = {}; - //include url params + // include url params if (params) { const { name, schema, description } = params[0]; - let label = name.split('_').join(' '); + let label = capitalize(name.split('_').join(' ')); paramProp[name] = { 'x-vault-displayAttrs': { @@ -159,51 +201,63 @@ export default Service.extend({ }; } - //TODO: handle post endpoints without requestBody + // TODO: handle post endpoints without requestBody const props = pathInfo.post.requestBody.content['application/json'].schema.properties; - //put url params (e.g. {name}, {role}) - //at the front of the props list + // put url params (e.g. {name}, {role}) + // at the front of the props list const newProps = assign({}, paramProp, props); return expandOpenApiProps(newProps); }); }, - getNewAdapter(paths, itemType) { - //we need list and create paths to set the correct urls for actions - const { list, create, apiPath } = paths; - const createPath = create.find(path => path.path.includes(itemType)); - const listPath = list.find(pathInfo => pathInfo.path.includes(itemType)); - const deletePath = paths.delete.find(path => path.path.includes(itemType)); + getNewAdapter(pathInfo, itemType) { + // we need list and create paths to set the correct urls for actions + let paths = this.filterPathsByItemType(pathInfo, itemType); + let { apiPath } = pathInfo; + const getPath = paths.find(path => path.operations.includes('get')); + + // the action might be "Generate" or something like that so we'll grab the first post endpoint if there + // isn't one with "Create" + // TODO: look into a more sophisticated way to determine the create endpoint + const createPath = paths.find(path => path.action === 'Create' || path.operations.includes('post')); + const deletePath = paths.find(path => path.operations.includes('delete')); + return generatedItemAdapter.extend({ - urlForItem(method, id) { - let { path } = listPath; - let url = `${this.buildURL()}/${apiPath}${path.slice(1)}/`; - if (id) { - url = url + encodePath(id); + urlForItem(id, isList) { + const itemType = getPath.path.slice(1); + let url; + id = encodePath(id); + + // isList indicates whether we are viewing the list page + // of a top-level item such as userpass + if (isList) { + url = `${this.buildURL()}/${apiPath}${itemType}/`; + } else { + // build the URL for the show page of a nested item + // such as a userpass group + url = `${this.buildURL()}/${apiPath}${itemType}/${id}`; } + return url; }, - urlForFindRecord(id, modelName, snapshot) { - return this.urlForItem(modelName, id, snapshot); + urlForQueryRecord(id, modelName) { + return this.urlForItem(id, modelName); }, urlForUpdateRecord(id) { - let { path } = createPath; - path = path.slice(1, path.indexOf('{') - 1); - return `${this.buildURL()}/${apiPath}${path}/${id}`; + const itemType = createPath.path.slice(1, createPath.path.indexOf('{') - 1); + return `${this.buildURL()}/${apiPath}${itemType}/${id}`; }, urlForCreateRecord(modelType, snapshot) { const { id } = snapshot; - let { path } = createPath; - path = path.slice(1, path.indexOf('{') - 1); + const path = createPath.path.slice(1, createPath.path.indexOf('{') - 1); return `${this.buildURL()}/${apiPath}${path}/${id}`; }, urlForDeleteRecord(id) { - let { path } = deletePath; - path = path.slice(1, path.indexOf('{') - 1); + const path = deletePath.path.slice(1, deletePath.path.indexOf('{') - 1); return `${this.buildURL()}/${apiPath}${path}/${id}`; }, }); @@ -214,8 +268,8 @@ export default Service.extend({ const { attrs, newFields } = combineAttributes(newModel.attributes, props); let owner = getOwner(this); newModel = newModel.extend(attrs, { newFields }); - //if our newModel doesn't have fieldGroups already - //we need to create them + // if our newModel doesn't have fieldGroups already + // we need to create them try { let fieldGroups = newModel.proto().fieldGroups; if (!fieldGroups) { @@ -224,7 +278,7 @@ export default Service.extend({ newModel = newModel.extend({ fieldGroups }); } } catch (err) { - //eat the error, fieldGroups is computed in the model definition + // eat the error, fieldGroups is computed in the model definition } newModel.reopenClass({ merged: true }); owner.unregister(modelName); @@ -237,8 +291,8 @@ export default Service.extend({ }; let fieldGroups = []; newModel.attributes.forEach(attr => { - //if the attr comes in with a fieldGroup from OpenAPI, - //add it to that group + // if the attr comes in with a fieldGroup from OpenAPI, + // add it to that group if (attr.options.fieldGroup) { if (groups[attr.options.fieldGroup]) { groups[attr.options.fieldGroup].push(attr.name); @@ -246,7 +300,7 @@ export default Service.extend({ groups[attr.options.fieldGroup] = [attr.name]; } } else { - //otherwise just add that attr to the default group + // otherwise just add that attr to the default group groups.default.push(attr.name); } }); diff --git a/ui/app/templates/components/generated-item-list.hbs b/ui/app/templates/components/generated-item-list.hbs index bbdedac69aa5..a9557d6af50e 100644 --- a/ui/app/templates/components/generated-item-list.hbs +++ b/ui/app/templates/components/generated-item-list.hbs @@ -13,14 +13,36 @@