Enable generated items for more auth methods (#7513)

* enable auth method item configuration in go code

* properly parse and list generated items

* make sure we only set name on attrs if a label comes from openAPI

* correctly construct paths object for method index route

* set sensitive property on password for userpass

* remove debugger statements

* pass method model to list route template to use paths on model for tabs

* update tab generation in generated item list, undo enabling userpass users

* enable openapi generated itams for certs and userpass, update ldap to no longer have action on list endpoint

* add editType to DisplayAttributes, pull tokenutil fields into field group

* show sensitive message for sensitive fields displayed in fieldGroupShow component

* grab sensitive and editType fields from displayAttrs in openapi-to-attrs util

* make sure we don't ask for paths for secret backends since that isn't setup yet

* fix styling of sensitive text for fieldGroupShow component

* update openapi-to-attrs util test to no longer include label by default, change debugger to console.err in path-help, remove dynamic ui auth methods from tab count test

* properly log errors to the console

* capitalize This value is sensitive...

* get rid of extra padding on bottom of fieldgroupshow

* make auth methods clickable and use new confirm ux

* Update sdk/framework/path.go

Co-Authored-By: Jim Kalafut <jkalafut@hashicorp.com>

* Update sdk/framework/path.go

Co-Authored-By: Jim Kalafut <jkalafut@hashicorp.com>

* add whitespace

* return intErr instead of err

* uncomment out helpUrl because we need it

* remove extra box class

* use const instead of let

* remove extra conditional since we already split the pathName later on

* ensure we request the correct url when listing generated items

* use const

* link to list and show pages

* remove dead code

* show nested item name instead of id

* add comments

* show tooltip for text-file inputs

* fix storybook

* remove extra filter

* add TODOs

* add comments

* comment out unused variables but leave them in function signature

* only link to auth methods that can be fully managed in the ui

* clean up comments

* only render tooltip if there is helpText

* rename id authMethodPath

* remove optionsForQuery since we don't need it

* add indentation

* standardize ConfirmMessage and show model name instead of id when editing

* standardize ConfirmMessage and show model name instead of id when editing

* add comments

* post to the correct updateUrl so we can edit users and groups

* use pop instead of slice

* add TODO for finding a better way to store ids

* ensure ids are handled the same way on list and show pages; fix editing and deleting

* add comment about difference between list and show urls

* use model.id instead of name since we do not need it

* remove dead code

* ensure list pages have page headers

* standardize using authMethodPath instead of method and remove dead code

* i love indentation

* remove more dead code

* use new Confirm

* show correct flash message when deleting an item

* update flash message for creating and updating

* use plus icon for creating group/user instead of an arrow
This commit is contained in:
Madalyn 2019-10-17 19:19:14 -04:00 committed by Noelle Daley
parent 0272c964c1
commit 8f4530b904
35 changed files with 445 additions and 230 deletions

View File

@ -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)

View File

@ -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",
},
}
}

View File

@ -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",
},
}
}

View File

@ -87,6 +87,9 @@ func pathConfig(b *backend) *framework.Path {
ExistenceCheck: b.pathConfigExistenceCheck,
HelpSynopsis: pathConfigHelp,
DisplayAttrs: &framework.DisplayAttributes{
Action: "Configure",
},
}
tokenutil.AddTokenFields(p.Fields)

View File

@ -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",
},
}
}

View File

@ -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",
},
}
}

View File

@ -85,6 +85,9 @@ func pathConfig(b *backend) *framework.Path {
HelpSynopsis: pathConfigHelpSyn,
HelpDescription: pathConfigHelpDesc,
DisplayAttrs: &framework.DisplayAttributes{
Action: "Configure",
},
}
tokenutil.AddTokenFields(p.Fields)

View File

@ -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",
},
}
}

View File

@ -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)

View File

@ -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.

View File

@ -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",
},
},
}

View File

@ -2,7 +2,8 @@
<meta name="kmip/config/environment" content="%7B%22modulePrefix%22%3A%22kmip%22%2C%22environment%22%3A%22development%22%7D" />
<meta name="open-api-explorer/config/environment" content="%7B%22modulePrefix%22%3A%22open-api-explorer%22%2C%22environment%22%3A%22development%22%2C%22APP%22%3A%7B%22NAMESPACE_ROOT_URLS%22%3A%5B%22sys/health%22%2C%22sys/seal-status%22%2C%22sys/license/features%22%5D%7D%7D" />
<meta name="replication/config/environment" content="%7B%22modulePrefix%22%3A%22replication%22%2C%22environment%22%3A%22development%22%7D" />
<meta name="vault/config/asset-manifest" content="%7B%22bundles%22%3A%7B%22kmip%22%3A%7B%22assets%22%3A%5B%7B%22uri%22%3A%22/ui/engines-dist/kmip/assets/engine-vendor.js%22%2C%22type%22%3A%22js%22%7D%2C%7B%22uri%22%3A%22/ui/engines-dist/kmip/assets/engine.js%22%2C%22type%22%3A%22js%22%7D%5D%7D%2C%22open-api-explorer%22%3A%7B%22assets%22%3A%5B%7B%22uri%22%3A%22/ui/engines-dist/open-api-explorer/assets/engine-vendor.css%22%2C%22type%22%3A%22css%22%7D%2C%7B%22uri%22%3A%22/ui/engines-dist/open-api-explorer/assets/engine-vendor.js%22%2C%22type%22%3A%22js%22%7D%2C%7B%22uri%22%3A%22/ui/engines-dist/open-api-explorer/assets/engine.css%22%2C%22type%22%3A%22css%22%7D%2C%7B%22uri%22%3A%22/ui/engines-dist/open-api-explorer/assets/engine.js%22%2C%22type%22%3A%22js%22%7D%5D%7D%2C%22replication%22%3A%7B%22assets%22%3A%5B%7B%22uri%22%3A%22/ui/engines-dist/replication/assets/engine-vendor.js%22%2C%22type%22%3A%22js%22%7D%2C%7B%22uri%22%3A%22/ui/engines-dist/replication/assets/engine.js%22%2C%22type%22%3A%22js%22%7D%5D%7D%7D%7D" />
<meta name="vault/config/asset-manifest"
content="%7B%22bundles%22%3A%7B%22kmip%22%3A%7B%22assets%22%3A%5B%7B%22uri%22%3A%22/ui/engines-dist/kmip/assets/engine-vendor.js%22%2C%22type%22%3A%22js%22%7D%2C%7B%22uri%22%3A%22/ui/engines-dist/kmip/assets/engine.js%22%2C%22type%22%3A%22js%22%7D%5D%7D%2C%22open-api-explorer%22%3A%7B%22assets%22%3A%5B%7B%22uri%22%3A%22/ui/engines-dist/open-api-explorer/assets/engine-vendor.css%22%2C%22type%22%3A%22css%22%7D%2C%7B%22uri%22%3A%22/ui/engines-dist/open-api-explorer/assets/engine-vendor.js%22%2C%22type%22%3A%22js%22%7D%2C%7B%22uri%22%3A%22/ui/engines-dist/open-api-explorer/assets/engine.css%22%2C%22type%22%3A%22css%22%7D%2C%7B%22uri%22%3A%22/ui/engines-dist/open-api-explorer/assets/engine.js%22%2C%22type%22%3A%22js%22%7D%5D%7D%2C%22replication%22%3A%7B%22assets%22%3A%5B%7B%22uri%22%3A%22/ui/engines-dist/replication/assets/engine-vendor.js%22%2C%22type%22%3A%22js%22%7D%2C%7B%22uri%22%3A%22/ui/engines-dist/replication/assets/engine.js%22%2C%22type%22%3A%22js%22%7D%5D%7D%7D%7D" />
<link rel="stylesheet" href="/assets/vendor.css" />
<link rel="stylesheet" href="/assets/vault.css" />
<link rel="icon" href="/favicon.png" />
@ -23,4 +24,4 @@
</script>
<script>runningTests = true;</script>
<script src="/assets/vendor.js"></script>
<script src="/assets/vault.js"></script>
<script src="/assets/vault.js"></script>

View File

@ -4,29 +4,25 @@ import ApplicationAdapter from './application';
export default ApplicationAdapter.extend({
namespace: 'v1',
urlForItem() {},
optionsForQuery(id) {
let data = {};
if (!id) {
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 => {
fetchByQuery(store, query, isList) {
const { id } = query;
let data = {};
if (isList) {
data.list = true;
}
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) {

View File

@ -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}.`);
});
},
},

View File

@ -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);

View File

@ -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 {

View File

@ -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);
},

View File

@ -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)
);
});
},
});

View File

@ -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);
},
});

View File

@ -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))
);
});
},
});

View File

@ -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) {

View File

@ -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));
},
});

View File

@ -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];
const pathDetails = currentPath[1];
const displayAttrs = pathDetails['x-vault-displayAttrs'];
//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
if (!displayAttrs) {
return pathInfo;
}
//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 });
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('~*');
}
if (pathInfo.delete) {
paths.delete.push({ path: pathName });
if (itemType && !pathInfo.itemTypes.includes(itemType)) {
pathInfo.itemTypes.push(itemType);
}
//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,
});
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');
}
if (pathInfo['x-vault-displayAttrs'] && pathInfo['x-vault-displayAttrs'].navigation) {
paths.navPaths.push({ path: pathName });
}
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;
},
getPaths(apiPath, backend) {
debug(`Fetching relevant paths for ${backend} from ${apiPath}`);
filterPathsByItemType(pathInfo, itemType) {
if (!itemType) {
return pathInfo.paths;
}
return pathInfo.paths.filter(path => {
return itemType === path.itemType;
});
},
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);
}
});

View File

@ -13,14 +13,36 @@
</p.top>
<p.levelLeft>
<h1 class="title is-3">
{{method}}
{{methodModel.id}}
</h1>
</p.levelLeft>
</PageHeader>
{{section-tabs model "authShow" paths}}
{{#with (tabs-for-auth-section methodModel 'authConfig' paths) as |tabs|}}
{{#if tabs.length}}
<div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless">
<nav class="tabs">
<ul>
{{#each tabs as |tab|}}
{{#link-to
params=tab.routeParams
tagName="li"
data-test-auth-section-tab=true
}}
{{#link-to params=tab.routeParams}}
{{tab.label}}
{{/link-to}}
{{/link-to}}
{{/each}}
</ul>
</nav>
</div>
{{/if}}
{{/with}}
<Toolbar>
<ToolbarActions>
<ToolbarLink @params={{array
<ToolbarLink
@type="add"
@params={{array
"vault.cluster.access.method.item.create"
itemType
}}>
@ -47,7 +69,7 @@
<Item.content>
<Icon @glyph="folder-outline" class="has-text-grey-light" @size="l" />{{list.item.id}}
</Item.content>
<Item.menu>
<Item.menu as |Menu|>
<li class="action">
{{#link-to "vault.cluster.access.method.item.show" list.item.id class="is-block"}}
View {{singularize itemType}}
@ -59,22 +81,24 @@
{{/link-to}}
</li>
<li>
<ConfirmAction @buttonClasses="link is-destroy" @onConfirmAction={{action
(perform
Item.callMethod
"destroyRecord"
list.item
(concat "Successfully deleted " (singularize itemType) " " list.item.id)
(concat "There was an error deleting this " (singularize itemType))
(action "refreshItemList")
)
}} @confirmMessage={{concat "Are you sure you want to delete " list.item.id "?"}}
@cancelButtonText="Cancel" data-test-secret-delete="true">
Delete
{{singularize itemType}}
</ConfirmAction>
<Menu.Message
@id={{list.item.id}}
@buttonClasses="link is-destroy"
@onConfirm={{action
(perform
Item.callMethod
"destroyRecord"
list.item
(concat "Successfully deleted " (singularize itemType) " " list.item.id ".")
(concat "There was an error deleting this " (singularize itemType))
(action "refreshItemList")
)
}}
@message={{concat "Are you sure you want to delete " (singularize itemType) " " list.item.id "?"}}
data-test-secret-delete="true"
@triggerText={{concat "Delete " (singularize itemType)}}/>
</li>
</Item.menu>
</ListItem>
{{/if}}
</ListView>
</ListView>

View File

@ -36,7 +36,7 @@
<Toolbar>
<ToolbarActions>
<ConfirmAction @buttonClasses="toolbar-link" @onConfirmAction={{action "deleteItem"}}
@confirmMessage={{concat "Are you sure you want to delete " model.id "?"}} @cancelButtonText="Cancel"
@confirmMessage={{concat "Are you sure you want to delete " itemType " " model.id "?"}} @cancelButtonText="Cancel"
data-test-secret-delete="true">
Delete
{{itemType}}
@ -75,4 +75,4 @@
</div>
</div>
</form>
{{/if}}
{{/if}}

View File

@ -4,6 +4,13 @@
<label class="is-label" data-test-text-label=true>
{{#if label}}
{{label}}
{{#if helpText}}
{{#info-tooltip}}
<span data-test-help-text>
{{helpText}}
</span>
{{/info-tooltip}}
{{/if}}
{{else}}
File
{{/if}}

View File

@ -1 +1,4 @@
<GeneratedItem @model={{model}} @mode="edit" @itemType={{itemType}} />
<GeneratedItem
@model={{model}}
@mode="edit"
@itemType={{itemType}} />

View File

@ -1 +1,6 @@
<GeneratedItemList @model={{model}} @method={{method}} @itemType={{itemType}} @paths={{paths}} />
<GeneratedItemList
@model={{model}}
@method={{method}}
@itemType={{itemType}}
@paths={{paths}}
@methodModel={{methodModel}} />

View File

@ -1 +1,4 @@
<GeneratedItem @model={{model}} @itemType={{itemType}} @mode="show"/>
<GeneratedItem
@model={{model}}
@itemType={{itemType}}
@mode="show"/>

View File

@ -15,7 +15,7 @@
</Toolbar>
{{#each (sort-by "path" model) as |method|}}
{{#if (eq method.methodType "ldap")}}
{{#if (contains method.methodType (supported-managed-auth-backends))}}
<LinkedBlock @params={{array
"vault.cluster.access.method"
method.id}} class="list-item-row" data-test-auth-backend-link={{method.id}}>
@ -51,6 +51,7 @@
<div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item">
{{#popup-menu name="auth-backend-nav"}}
<Confirm as |c|>
<nav class="menu">
<ul class="menu-list">
<li>
@ -68,15 +69,18 @@
{{#if (and (not-eq method.methodType 'token') method.canDisable)}}
<li class="action">
<ConfirmAction @buttonClasses="link is-destroy" @confirmTitle="Disable method?"
@confirmMessage="This may affect access to Vault data." @confirmButtonText="Disable"
@onConfirmAction={{perform disableMethod method}}>
Disable
</ConfirmAction>
<c.Message
@id={{method.id}}
@title="Disable method?"
@message="This may affect access to Vault data."
@triggerText="Disable"
@onConfirm={{perform disableMethod method}}>
</c.Message>
</li>
{{/if}}
</ul>
</nav>
</Confirm>
{{/popup-menu}}
</div>
</div>
@ -90,13 +94,13 @@
<ToolTip @horizontalPosition="left" as |T|>
<T.trigger>
<Icon @glyph={{if
(or
(find-by "type" method.methodType (mountable-auth-methods))
(eq method.methodType "token")
)
method.methodType
"auth"
}} @size="l" class="has-text-grey-light" />
(or
(find-by "type" method.methodType (mountable-auth-methods))
(eq method.methodType "token")
)
method.methodType
"auth"
}} @size="l" class="has-text-grey-light" />
</T.trigger>
<T.content @class="tool-tip">
<div class="box">
@ -116,36 +120,37 @@
<div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item">
{{#popup-menu name="auth-backend-nav"}}
<nav class="menu">
<ul class="menu-list">
<li>
{{#link-to "vault.cluster.access.method.section" method.id "configuration"}}
View configuration
{{/link-to}}
</li>
{{#if method.canEdit}}
<Confirm as |c|>
<nav class="menu">
<ul class="menu-list">
<li>
{{#link-to "vault.cluster.settings.auth.configure" method.id}}
Edit configuration
{{#link-to "vault.cluster.access.method.section" method.id "configuration"}}
View configuration
{{/link-to}}
</li>
{{/if}}
{{#if method.canEdit}}
<li>
{{#link-to "vault.cluster.settings.auth.configure" method.id}}
Edit configuration
{{/link-to}}
</li>
{{/if}}
{{#if (and (not-eq method.methodType 'token') method.canDisable)}}
<li class="action">
<ConfirmAction @buttonClasses="link is-destroy" @confirmTitle="Disable method?"
@confirmMessage="This may affect access to Vault data." @confirmButtonText="Disable"
@onConfirmAction={{perform disableMethod method}}>
Disable
</ConfirmAction>
</li>
{{/if}}
</ul>
</nav>
{{#if (and (not-eq method.methodType 'token') method.canDisable)}}
<li class="action">
<c.Message @id={{method.id}} @title="Disable method?"
@message="This may affect access to Vault data." @triggerText="Disable"
@onConfirm={{perform disableMethod method}}>
</c.Message>
</li>
{{/if}}
</ul>
</nav>
</Confirm>
{{/popup-menu}}
</div>
</div>
</div>
</div>
{{/if}}
{{/each}}
{{/if}}
{{/each}}

View File

@ -12,12 +12,13 @@ export const expandOpenApiProps = function(props) {
if (deprecated === true) {
continue;
}
let { name, value, group, sensitive } = prop['x-vault-displayAttrs'] || {};
let { name, value, group, sensitive, editType } = prop['x-vault-displayAttrs'] || {};
if (type === 'integer') {
type = 'number';
}
let editType = type;
editType = editType || type;
if (format === 'seconds') {
editType = 'ttl';
@ -28,7 +29,6 @@ export const expandOpenApiProps = function(props) {
let attrDefn = {
editType,
helpText: description,
sensitive: sensitive,
possibleValues: prop['enum'],
fieldValue: isId ? 'id' : null,
fieldGroup: group || 'default',
@ -36,13 +36,22 @@ export const expandOpenApiProps = function(props) {
defaultValue: value || null,
};
attrDefn.label = capitalize(name || propName);
if (sensitive) {
attrDefn.sensitive = true;
}
//only set a label if we have one from OpenAPI
//otherwise the propName will be humanized by the form-field component
if (name) {
attrDefn.label = name;
}
// ttls write as a string and read as a number
// so setting type on them runs the wrong transform
if (editType !== 'ttl' && type !== 'array') {
attrDefn.type = type;
}
// loop to remove empty vals
for (let attrProp in attrDefn) {
if (attrDefn[attrProp] == null) {

View File

@ -1,26 +1,36 @@
<div class="box is-shadowless is-sideless is-fullwidth is-marginless">
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
{{#each @model.fieldGroups as |fieldGroup|}}
{{#each-in fieldGroup as |group fields|}}
{{#if (or (eq group "default") (eq group "Options"))}}
{{#each fields as |attr|}}
{{#if (not-eq attr.options.fieldValue "id")}}
<InfoTableRow @alwaysRender={{showAllFields}}
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get @model attr.name}} />
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}>
{{#if attr.options.sensitive}}
<span class="is-help">This value is sensitive and cannot be shown.</span>
{{else}}
{{get @model attr.name}}
{{/if}}
</InfoTableRow>
{{/if}}
{{/each}}
{{else}}
<div class="box {{unless showAllFields 'is-shadowless'}} is-sideless is-fullwidth is-marginless">
<div class="box {{unless showAllFields 'is-shadowless'}} is-fullwidth is-sideless is-marginless">
<h2 class="title is-5">
{{group}}
</h2>
{{#each fields as |attr|}}
<InfoTableRow @alwaysRender={{showAllFields}}
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get @model attr.name}} />
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}>
{{#if attr.options.sensitive}}
<span class="is-help">This value is sensitive and cannot be shown.</span>
{{else}}
{{get @model attr.name}}
{{/if}}
</InfoTableRow>
{{/each}}
</div>
{{/if}}
{{/each-in}}
{{/each}}
</div>
</div>

View File

@ -89,6 +89,7 @@
{{else if (eq attr.options.editType "file")}}
{{text-file
index=""
helpText=attr.options.helpText
file=file
onChange=(action "setFile")
warning=attr.options.warning
@ -179,4 +180,4 @@
value=(if (get model valuePath) (stringify (get model valuePath)) emptyData)
valueUpdated=(action "codemirrorUpdated" attr.name false)
}}
{{/if}}
{{/if}}

View File

@ -41,7 +41,7 @@ module('Acceptance | settings/auth/configure/section', function(hooks) {
assert.ok(keys.includes('max_lease_ttl'), 'passes max_lease_ttl on tune');
});
for (let type of ['aws', 'azure', 'gcp', 'github', 'kubernetes', 'ldap', 'okta', 'radius']) {
for (let type of ['aws', 'azure', 'gcp', 'github', 'kubernetes']) {
test(`it shows tabs for auth method: ${type}`, async assert => {
let path = `${type}-${Date.now()}`;
await cli.consoleInput(`write sys/auth/${path} type=${type}`);

View File

@ -58,28 +58,24 @@ module('Unit | Util | OpenAPI Data Utilities', function() {
editType: 'stringArray',
defaultValue: 'Grace Hopper,Lady Ada',
fieldGroup: 'default',
label: 'Awesome-people',
},
favoriteIceCream: {
editType: 'string',
type: 'string',
possibleValues: ['vanilla', 'chocolate', 'strawberry'],
fieldGroup: 'default',
label: 'Favorite-ice-cream',
},
defaultValue: {
editType: 'number',
type: 'number',
defaultValue: 300,
fieldGroup: 'default',
label: 'Default-value',
},
default: {
editType: 'number',
type: 'number',
defaultValue: 30,
fieldGroup: 'default',
label: 'Default',
},
superSecret: {
type: 'string',
@ -87,7 +83,6 @@ module('Unit | Util | OpenAPI Data Utilities', function() {
sensitive: true,
helpText: 'A really secret thing',
fieldGroup: 'default',
label: 'Super-secret',
},
};