From 242df4f4d7c46e85fc793870fe32265d735670f9 Mon Sep 17 00:00:00 2001 From: Vishal Nayak Date: Thu, 18 Oct 2018 07:53:12 -0700 Subject: [PATCH 01/50] Remove lookup check during alias removal (#5524) * Possible fix for 5348 * Fix compilation --- vault/identity_store_util.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/vault/identity_store_util.go b/vault/identity_store_util.go index 2df22e9ea5..6fe3d0daa0 100644 --- a/vault/identity_store_util.go +++ b/vault/identity_store_util.go @@ -1072,14 +1072,7 @@ func (i *IdentityStore) deleteAliasesInEntityInTxn(txn *memdb.Txn, entity *ident // Remove identity indices from aliases table for those that needs to // be removed for _, alias := range removeList { - aliasToBeRemoved, err := i.MemDBAliasByIDInTxn(txn, alias.ID, false, false) - if err != nil { - return err - } - if aliasToBeRemoved == nil { - return fmt.Errorf("alias was not indexed") - } - err = i.MemDBDeleteAliasByIDInTxn(txn, aliasToBeRemoved.ID, false) + err := i.MemDBDeleteAliasByIDInTxn(txn, alias.ID, false) if err != nil { return err } From 70d35c8d1dafd7b5cca1bdad0836abb687cec61e Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Thu, 18 Oct 2018 10:54:27 -0400 Subject: [PATCH 02/50] changelog++ --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ef8eed4b4..7ef231cbf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ BUG FIXES: * secret/pki: Fix regression in 0.11.2+ causing the NotBefore value of generated certificates to be set to the Unix epoch if the role value was not set, instead of using the default of 30 seconds [GH-5481] + * identity: Remove lookup check during alias removal from entity [GH-5524] ## 0.11.3 (October 8th, 2018) From 46f1a3e2f02d9364b63a8c1119a7481e81c126f7 Mon Sep 17 00:00:00 2001 From: Pouyan Azari Date: Thu, 18 Oct 2018 17:51:07 +0200 Subject: [PATCH 03/50] Added link to list of all extensions for the ssh. (#5542) Added a link to the OpenSSH extension list, this is not documented anywhere in vault documentation website. --- website/source/api/secret/ssh/index.html.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/source/api/secret/ssh/index.html.md b/website/source/api/secret/ssh/index.html.md index 090ef7feef..b8fe4d6328 100644 --- a/website/source/api/secret/ssh/index.html.md +++ b/website/source/api/secret/ssh/index.html.md @@ -164,6 +164,10 @@ This endpoint creates or updates a named role. - `allowed_extensions` `(string: "")` – Specifies a comma-separated list of extensions that certificates can have when signed. To allow any critical options, set this to an empty string. Will default to allowing any extensions. + For the list of extensions, take a look at + [sshd manual](https://man.openbsd.org/sshd#AUTHORIZED_KEYS_FILE_FORMAT) + `AUTHORIZED_KEYS FILE FORMAT` part. You should add a `permit-` before the + name of extension to allow it. - `default_critical_options` `(map: "")` – Specifies a map of critical options certificates should have if none are provided when signing. From 51bb46302a34db4902f2d6e0b71230e00885a987 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 18 Oct 2018 13:32:42 -0400 Subject: [PATCH 04/50] Update some grammar --- website/source/api/secret/ssh/index.html.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/source/api/secret/ssh/index.html.md b/website/source/api/secret/ssh/index.html.md index b8fe4d6328..bdc0b110b8 100644 --- a/website/source/api/secret/ssh/index.html.md +++ b/website/source/api/secret/ssh/index.html.md @@ -163,10 +163,10 @@ This endpoint creates or updates a named role. - `allowed_extensions` `(string: "")` – Specifies a comma-separated list of extensions that certificates can have when signed. To allow any critical - options, set this to an empty string. Will default to allowing any extensions. - For the list of extensions, take a look at - [sshd manual](https://man.openbsd.org/sshd#AUTHORIZED_KEYS_FILE_FORMAT) - `AUTHORIZED_KEYS FILE FORMAT` part. You should add a `permit-` before the + options, set this to an empty string. Will default to allowing any + extensions. For the list of extensions, take a look at the [sshd + manual's](https://man.openbsd.org/sshd#AUTHORIZED_KEYS_FILE_FORMAT) + `AUTHORIZED_KEYS FILE FORMAT` section. You should add a `permit-` before the name of extension to allow it. - `default_critical_options` `(map: "")` – Specifies a map of From e8d435340d8a051180b2a5810ccf75a5f34af254 Mon Sep 17 00:00:00 2001 From: dle-fr Date: Thu, 18 Oct 2018 19:35:04 +0200 Subject: [PATCH 05/50] [5529] Mysql HA: table creation error using InnoDB and utf8 charset (#5543) --- physical/mysql/mysql.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/physical/mysql/mysql.go b/physical/mysql/mysql.go index db078af5d2..de63d1307b 100644 --- a/physical/mysql/mysql.go +++ b/physical/mysql/mysql.go @@ -150,7 +150,7 @@ func NewMySQLBackend(conf map[string]string, logger log.Logger) (physical.Backen // Create the required table if it doesn't exists. if !lockTableExist { create_query := "CREATE TABLE IF NOT EXISTS " + dbLockTable + - " (node_job varchar(512), current_leader varchar(512), PRIMARY KEY (node_job))" + " (node_job varbinary(512), current_leader varbinary(512), PRIMARY KEY (node_job))" if _, err := db.Exec(create_query); err != nil { return nil, errwrap.Wrapf("failed to create mysql table: {{err}}", err) } From c15276c136b88517810655eddbe77b4553ea769c Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 18 Oct 2018 13:36:27 -0400 Subject: [PATCH 06/50] changelog++ --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ef231cbf6..793cd5195a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,10 +19,12 @@ BUG FIXES: * agent: Fix potential hang during agent shutdown [GH-5026] * core: Fix generate-root operations requiring empty `otp` to be provided instead of an empty body [GH-5495] + * identity: Remove lookup check during alias removal from entity [GH-5524] * secret/pki: Fix regression in 0.11.2+ causing the NotBefore value of generated certificates to be set to the Unix epoch if the role value was not set, instead of using the default of 30 seconds [GH-5481] - * identity: Remove lookup check during alias removal from entity [GH-5524] + * storage/mysql: Use `varbinary` instead of `varchar` when creating HA tables + [GH-5529] ## 0.11.3 (October 8th, 2018) From bad2f6d073055ed0a7ca12af0596ee2e684d7960 Mon Sep 17 00:00:00 2001 From: Evgeniy Zakharochkin Date: Thu, 18 Oct 2018 20:41:14 +0300 Subject: [PATCH 07/50] ability to add NAS Identifier header to radius request (#5465) --- builtin/credential/radius/path_config.go | 14 ++++++++++++++ builtin/credential/radius/path_login.go | 3 +++ ui/app/models/auth-config/radius.js | 6 +++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/builtin/credential/radius/path_config.go b/builtin/credential/radius/path_config.go index 4a3f72e33b..be31877d71 100644 --- a/builtin/credential/radius/path_config.go +++ b/builtin/credential/radius/path_config.go @@ -46,6 +46,11 @@ func pathConfig(b *backend) *framework.Path { Default: 10, Description: "RADIUS NAS port field (default: 10)", }, + "nas_identifier": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "", + Description: "RADIUS NAS Identifier field (optional)", + }, }, ExistenceCheck: b.configExistenceCheck, @@ -110,6 +115,7 @@ func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, d *f "dial_timeout": cfg.DialTimeout, "read_timeout": cfg.ReadTimeout, "nas_port": cfg.NasPort, + "nas_identifier": cfg.NasIdentifier, }, } return resp, nil @@ -190,6 +196,13 @@ func (b *backend) pathConfigCreateUpdate(ctx context.Context, req *logical.Reque cfg.NasPort = d.Get("nas_port").(int) } + nasIdentifier, ok := d.GetOk("nas_identifier") + if ok { + cfg.NasIdentifier = nasIdentifier.(string) + } else if req.Operation == logical.CreateOperation { + cfg.NasIdentifier = d.Get("nas_identifier").(string) + } + entry, err := logical.StorageEntryJSON("config", cfg) if err != nil { return nil, err @@ -209,6 +222,7 @@ type ConfigEntry struct { DialTimeout int `json:"dial_timeout" structs:"dial_timeout" mapstructure:"dial_timeout"` ReadTimeout int `json:"read_timeout" structs:"read_timeout" mapstructure:"read_timeout"` NasPort int `json:"nas_port" structs:"nas_port" mapstructure:"nas_port"` + NasIdentifier string `json:"nas_identifier" structs:"nas_identifier" mapstructure:"nas_identifier"` } const pathConfigHelpSyn = ` diff --git a/builtin/credential/radius/path_login.go b/builtin/credential/radius/path_login.go index ef0c185d88..2cf2fa2f61 100644 --- a/builtin/credential/radius/path_login.go +++ b/builtin/credential/radius/path_login.go @@ -144,6 +144,9 @@ func (b *backend) RadiusLogin(ctx context.Context, req *logical.Request, usernam packet := radius.New(radius.CodeAccessRequest, []byte(cfg.Secret)) UserName_SetString(packet, username) UserPassword_SetString(packet, password) + if cfg.NasIdentifier != "" { + NASIdentifier_AddString(packet, cfg.NasIdentifier) + } packet.Add(5, radius.NewInteger(uint32(cfg.NasPort))) client := radius.Client{ diff --git a/ui/app/models/auth-config/radius.js b/ui/app/models/auth-config/radius.js index c47351f3d5..d88088a3f4 100644 --- a/ui/app/models/auth-config/radius.js +++ b/ui/app/models/auth-config/radius.js @@ -27,13 +27,17 @@ export default AuthConfig.extend({ label: 'NAS Port', }), + nasIdentifier: attr('string', { + label: 'NAS Identifier', + }), + fieldGroups: computed(function() { const groups = [ { default: ['host', 'secret'], }, { - 'RADIUS Options': ['port', 'nasPort', 'dialTimeout', 'unregisteredUserPolicies'], + 'RADIUS Options': ['port', 'nasPort', 'nasIdentifier', 'dialTimeout', 'unregisteredUserPolicies'], }, ]; return fieldToAttrs(this, groups); From d725421bc29287afb338314a4d2b473b5247385e Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 18 Oct 2018 13:41:54 -0400 Subject: [PATCH 08/50] changelog++ --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 793cd5195a..4964f6c476 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ IMPROVEMENTS: * secret/database: Allow Cassandra user to be non-superuser so long as it has role creation permissions [GH-5402] + * secret/radius: Allow setting the NAS Identifier value in the generated + packet [GH-5465] BUG FIXES: From 34c815b37a3c187b93a04fcf0b9050a4c7081b71 Mon Sep 17 00:00:00 2001 From: madalynrose Date: Thu, 18 Oct 2018 15:19:50 -0400 Subject: [PATCH 09/50] UI: onboarding wizard progress bar (#5516) Onboarding will now display your progress through the chosen tutorials --- ui/app/components/i-con.js | 2 + ui/app/components/wizard-content.js | 89 ++++++- .../components/wizard/features-selection.js | 21 +- ui/app/helpers/wizard-constants.js | 27 +++ ui/app/machines/tutorial-machine.js | 2 +- ui/app/routes/vault/cluster/tools/tool.js | 7 +- ui/app/services/wizard.js | 217 +++++++++++------- ui/app/styles/components/ui-wizard.scss | 92 +++++++- ui/app/styles/core/buttons.scss | 13 +- ui/app/styles/utils/_bulma_variables.scss | 4 + .../templates/components/wizard-content.hbs | 21 ++ .../templates/components/wizard-progress.hbs | 12 + .../components/wizard/auth-details.hbs | 4 +- .../components/wizard/features-selection.hbs | 9 +- .../components/wizard/policies-others.hbs | 2 +- .../components/wizard/replication-details.hbs | 2 +- .../components/wizard/secrets-display.hbs | 4 +- .../components/wizard/tools-unwrapped.hbs | 2 +- .../components/wizard/tutorial-error.hbs | 4 +- .../templates/svg/icons/check-circle-fill.hbs | 3 + ui/app/templates/svg/icons/check-plain.hbs | 3 + .../unit/machines/tutorial-machine-test.js | 1 + ui/tests/unit/services/wizard-test.js | 83 +++++++ 23 files changed, 521 insertions(+), 103 deletions(-) create mode 100644 ui/app/templates/components/wizard-progress.hbs create mode 100644 ui/app/templates/svg/icons/check-circle-fill.hbs create mode 100644 ui/app/templates/svg/icons/check-plain.hbs diff --git a/ui/app/components/i-con.js b/ui/app/components/i-con.js index e57f1fe447..4482fe1c84 100644 --- a/ui/app/components/i-con.js +++ b/ui/app/components/i-con.js @@ -22,6 +22,8 @@ const GLYPHS_WITH_SVG_TAG = [ 'control-lock', 'edition-enterprise', 'edition-oss', + 'check-plain', + 'check-circle-fill', ]; export default Component.extend({ diff --git a/ui/app/components/wizard-content.js b/ui/app/components/wizard-content.js index 7d1112fd88..bf2e036212 100644 --- a/ui/app/components/wizard-content.js +++ b/ui/app/components/wizard-content.js @@ -1,13 +1,100 @@ import { inject as service } from '@ember/service'; import Component from '@ember/component'; +import { computed } from '@ember/object'; +import { FEATURE_MACHINE_STEPS, INIT_STEPS } from 'vault/helpers/wizard-constants'; + export default Component.extend({ wizard: service(), classNames: ['ui-wizard'], glyph: null, headerText: null, + selectProgress: null, + currentMachine: computed.alias('wizard.currentMachine'), + tutorialState: computed.alias('wizard.currentState'), + tutorialComponent: computed.alias('wizard.tutorialComponent'), + showProgress: computed('wizard.featureComponent', 'tutorialState', function() { + return ( + this.tutorialComponent.includes('active') && + (this.tutorialState.includes('init.active') || + (this.wizard.featureComponent && this.wizard.featureMachineHistory)) + ); + }), + featureMachineHistory: computed.alias('wizard.featureMachineHistory'), + totalFeatures: computed('wizard.featureList', function() { + return this.wizard.featureList.length; + }), + completedFeatures: computed('wizard.currentMachine', function() { + return this.wizard.getCompletedFeatures(); + }), + currentFeatureProgress: computed('featureMachineHistory.[]', function() { + if (this.tutorialState.includes('active.feature')) { + let totalSteps = FEATURE_MACHINE_STEPS[this.currentMachine]; + if (this.currentMachine === 'secrets') { + if (this.featureMachineHistory.includes('secret')) { + totalSteps = totalSteps['secret']['secret']; + } + if (this.featureMachineHistory.includes('list')) { + totalSteps = totalSteps['secret']['list']; + } + if (this.featureMachineHistory.includes('encryption')) { + totalSteps = totalSteps['encryption']; + } + if (this.featureMachineHistory.includes('role') || typeof totalSteps === 'object') { + totalSteps = totalSteps['role']; + } + } + return { + percentage: (this.featureMachineHistory.length / totalSteps) * 100, + feature: this.currentMachine, + text: `Step ${this.featureMachineHistory.length} of ${totalSteps}`, + }; + } + return null; + }), + currentTutorialProgress: computed('tutorialState', function() { + if (this.tutorialState.includes('init.active')) { + let currentStepName = this.tutorialState.split('.')[2]; + let currentStepNumber = INIT_STEPS.indexOf(currentStepName) + 1; + return { + percentage: (currentStepNumber / INIT_STEPS.length) * 100, + text: `Step ${currentStepNumber} of ${INIT_STEPS.length}`, + }; + } + return null; + }), + progressBar: computed('currentFeatureProgress', 'currentFeature', 'currentTutorialProgress', function() { + let bar = []; + if (this.currentTutorialProgress) { + bar.push({ + style: `width:${this.currentTutorialProgress.percentage}%;`, + completed: false, + showIcon: true, + }); + } else { + if (this.currentFeatureProgress) { + this.completedFeatures.forEach(feature => { + bar.push({ style: 'width:100%;', completed: true, feature: feature, showIcon: true }); + }); + this.wizard.featureList.forEach(feature => { + if (feature === this.currentMachine) { + bar.push({ + style: `width:${this.currentFeatureProgress.percentage}%;`, + completed: this.currentFeatureProgress.percentage == 100 ? true : false, + feature: feature, + showIcon: true, + }); + } else { + bar.push({ style: 'width:0%;', completed: false, feature: feature, showIcon: true }); + } + }); + } + } + return bar; + }), + actions: { dismissWizard() { - this.get('wizard').transitionTutorialMachine(this.get('wizard.currentState'), 'DISMISS'); + this.wizard.transitionTutorialMachine(this.wizard.currentState, 'DISMISS'); }, }, }); diff --git a/ui/app/components/wizard/features-selection.js b/ui/app/components/wizard/features-selection.js index 6e6f7f590f..f43bd4b456 100644 --- a/ui/app/components/wizard/features-selection.js +++ b/ui/app/components/wizard/features-selection.js @@ -1,10 +1,12 @@ import { inject as service } from '@ember/service'; import Component from '@ember/component'; import { computed } from '@ember/object'; +import { FEATURE_MACHINE_TIME } from 'vault/helpers/wizard-constants'; export default Component.extend({ wizard: service(), version: service(), + init() { this._super(...arguments); this.maybeHideFeatures(); @@ -16,7 +18,24 @@ export default Component.extend({ feature.show = false; } }, - + estimatedTime: computed('selectedFeatures', function() { + let time = 0; + for (let feature of Object.keys(FEATURE_MACHINE_TIME)) { + if (this.selectedFeatures.includes(feature)) { + time += FEATURE_MACHINE_TIME[feature]; + } + } + return time; + }), + selectProgress: computed('selectedFeatures', function() { + let bar = this.selectedFeatures.map(feature => { + return { style: 'width:0%;', completed: false, showIcon: true, feature: feature }; + }); + if (bar.length === 0) { + bar = [{ style: 'width:0%;', showIcon: false }]; + } + return bar; + }), allFeatures: computed(function() { return [ { diff --git a/ui/app/helpers/wizard-constants.js b/ui/app/helpers/wizard-constants.js index f3b1e13b23..93b070887f 100644 --- a/ui/app/helpers/wizard-constants.js +++ b/ui/app/helpers/wizard-constants.js @@ -9,6 +9,7 @@ export const STORAGE_KEYS = { TUTORIAL_STATE: 'vault:ui-tutorial-state', FEATURE_LIST: 'vault:ui-feature-list', FEATURE_STATE: 'vault:ui-feature-state', + FEATURE_STATE_HISTORY: 'vault:ui-feature-state-history', COMPLETED_FEATURES: 'vault:ui-completed-list', COMPONENT_STATE: 'vault:ui-component-state', RESUME_URL: 'vault:ui-tutorial-resume-url', @@ -36,4 +37,30 @@ export const DEFAULTS = { componentState: null, nextFeature: null, nextStep: null, + featureMachineHistory: null, +}; + +export const FEATURE_MACHINE_STEPS = { + secrets: { + encryption: 5, + secret: { + list: 4, + secret: 5, + }, + role: 7, + }, + policies: 5, + replication: 2, + tools: 8, + authentication: 4, +}; + +export const INIT_STEPS = ['setup', 'save', 'unseal', 'login']; + +export const FEATURE_MACHINE_TIME = { + secrets: 7, + policies: 5, + replication: 5, + tools: 8, + authentication: 5, }; diff --git a/ui/app/machines/tutorial-machine.js b/ui/app/machines/tutorial-machine.js index 8ed5ca5adf..9c66d5e9c1 100644 --- a/ui/app/machines/tutorial-machine.js +++ b/ui/app/machines/tutorial-machine.js @@ -16,7 +16,7 @@ export default { { type: 'render', level: 'tutorial', component: 'wizard/tutorial-idle' }, { type: 'render', level: 'feature', component: null }, ], - onExit: ['showTutorialWhenAuthenticated'], + onExit: ['showTutorialWhenAuthenticated', 'clearFeatureData'], states: { idle: { on: { diff --git a/ui/app/routes/vault/cluster/tools/tool.js b/ui/app/routes/vault/cluster/tools/tool.js index 98b0f6d72a..2d7fb47782 100644 --- a/ui/app/routes/vault/cluster/tools/tool.js +++ b/ui/app/routes/vault/cluster/tools/tool.js @@ -26,11 +26,8 @@ export default Route.extend({ actions: { didTransition() { const params = this.paramsFor(this.routeName); - if (this.get('wizard.currentMachine') === 'tools') { - this.get('wizard').transitionFeatureMachine( - this.get('wizard.featureState'), - params.selectedAction.toUpperCase() - ); + if (this.wizard.currentMachine === 'tools') { + this.wizard.transitionFeatureMachine(this.wizard.featureState, params.selected_action.toUpperCase()); } this.controller.setProperties(params); return true; diff --git a/ui/app/services/wizard.js b/ui/app/services/wizard.js index 4b892f9857..56e96ceefe 100644 --- a/ui/app/services/wizard.js +++ b/ui/app/services/wizard.js @@ -5,56 +5,73 @@ import { Machine } from 'xstate'; import getStorage from 'vault/lib/token-storage'; import { STORAGE_KEYS, DEFAULTS, MACHINES } from 'vault/helpers/wizard-constants'; +const { + TUTORIAL_STATE, + COMPONENT_STATE, + FEATURE_STATE, + FEATURE_LIST, + FEATURE_STATE_HISTORY, + COMPLETED_FEATURES, + RESUME_URL, + RESUME_ROUTE, +} = STORAGE_KEYS; const TutorialMachine = Machine(MACHINES.tutorial); let FeatureMachine = null; export default Service.extend(DEFAULTS, { router: service(), showWhenUnauthenticated: false, - + featureMachineHistory: null, init() { this._super(...arguments); this.initializeMachines(); }, initializeMachines() { - if (!this.storageHasKey(STORAGE_KEYS.TUTORIAL_STATE)) { + if (!this.storageHasKey(TUTORIAL_STATE)) { let state = TutorialMachine.initialState; this.saveState('currentState', state.value); - this.saveExtState(STORAGE_KEYS.TUTORIAL_STATE, state.value); + this.saveExtState(TUTORIAL_STATE, state.value); } - this.saveState('currentState', this.getExtState(STORAGE_KEYS.TUTORIAL_STATE)); - if (this.storageHasKey(STORAGE_KEYS.COMPONENT_STATE)) { - this.set('componentState', this.getExtState(STORAGE_KEYS.COMPONENT_STATE)); + this.saveState('currentState', this.getExtState(TUTORIAL_STATE)); + if (this.storageHasKey(COMPONENT_STATE)) { + this.set('componentState', this.getExtState(COMPONENT_STATE)); } - let stateNodes = TutorialMachine.getStateNodes(this.get('currentState')); + let stateNodes = TutorialMachine.getStateNodes(this.currentState); this.executeActions(stateNodes.reduce((acc, node) => acc.concat(node.onEntry), []), null, 'tutorial'); - if (this.storageHasKey(STORAGE_KEYS.FEATURE_LIST)) { - this.set('featureList', this.getExtState(STORAGE_KEYS.FEATURE_LIST)); - if (this.storageHasKey(STORAGE_KEYS.FEATURE_STATE)) { - this.saveState('featureState', this.getExtState(STORAGE_KEYS.FEATURE_STATE)); - } else { - if (FeatureMachine != null) { - this.saveState('featureState', FeatureMachine.initialState); - this.saveExtState(STORAGE_KEYS.FEATURE_STATE, this.get('featureState')); - } + + if (this.storageHasKey(FEATURE_LIST)) { + this.set('featureList', this.getExtState(FEATURE_LIST)); + if (this.storageHasKey(FEATURE_STATE_HISTORY)) { + this.set('featureMachineHistory', this.getExtState(FEATURE_STATE_HISTORY)); } + this.saveState( + 'featureState', + this.getExtState(FEATURE_STATE) || (FeatureMachine ? FeatureMachine.initialState : null) + ); + this.saveExtState(FEATURE_STATE, this.featureState); this.buildFeatureMachine(); } }, - restartGuide() { + clearFeatureData() { let storage = this.storage(); // empty storage - [ - STORAGE_KEYS.TUTORIAL_STATE, - STORAGE_KEYS.FEATURE_LIST, - STORAGE_KEYS.FEATURE_STATE, - STORAGE_KEYS.COMPLETED_FEATURES, - STORAGE_KEYS.COMPONENT_STATE, - STORAGE_KEYS.RESUME_URL, - STORAGE_KEYS.RESUME_ROUTE, - ].forEach(key => storage.removeItem(key)); + [FEATURE_LIST, FEATURE_STATE, FEATURE_STATE_HISTORY, COMPLETED_FEATURES].forEach(key => + storage.removeItem(key) + ); + + this.set('currentMachine', null); + this.set('featureMachineHistory', null); + this.set('featureState', null); + this.set('featureList', null); + }, + + restartGuide() { + this.clearFeatureData(); + let storage = this.storage(); + // empty storage + [TUTORIAL_STATE, COMPONENT_STATE, RESUME_URL, RESUME_ROUTE].forEach(key => storage.removeItem(key)); // reset wizard state this.setProperties(DEFAULTS); // restart machines from blank state @@ -63,6 +80,32 @@ export default Service.extend(DEFAULTS, { this.transitionTutorialMachine('idle', 'AUTH'); }, + saveFeatureHistory(state) { + if ( + this.getCompletedFeatures().length === 0 && + this.featureMachineHistory === null && + (state === 'idle' || state === 'wrap') + ) { + let newHistory = [state]; + this.set('featureMachineHistory', newHistory); + } else { + if (this.featureMachineHistory) { + if (!this.featureMachineHistory.includes(state)) { + let newHistory = this.featureMachineHistory.addObject(state); + this.set('featureMachineHistory', newHistory); + } else { + //we're repeating steps + let stepIndex = this.featureMachineHistory.indexOf(state); + let newHistory = this.featureMachineHistory.splice(0, stepIndex + 1); + this.set('featureMachineHistory', newHistory); + } + } + } + if (this.featureMachineHistory) { + this.saveExtState(FEATURE_STATE_HISTORY, this.featureMachineHistory); + } + }, + saveState(stateType, state) { if (state.value) { state = state.value; @@ -75,40 +118,44 @@ export default Service.extend(DEFAULTS, { } stateKey += state; this.set(stateType, stateKey); + if (stateType === 'featureState') { + //only track progress if we are on the first step of the first feature + this.saveFeatureHistory(state); + } }, transitionTutorialMachine(currentState, event, extendedState) { if (extendedState) { this.set('componentState', extendedState); - this.saveExtState(STORAGE_KEYS.COMPONENT_STATE, extendedState); + this.saveExtState(COMPONENT_STATE, extendedState); } let { actions, value } = TutorialMachine.transition(currentState, event); this.saveState('currentState', value); - this.saveExtState(STORAGE_KEYS.TUTORIAL_STATE, this.get('currentState')); + this.saveExtState(TUTORIAL_STATE, this.currentState); this.executeActions(actions, event, 'tutorial'); }, transitionFeatureMachine(currentState, event, extendedState) { - if (!FeatureMachine || !this.get('currentState').includes('active')) { + if (!FeatureMachine || !this.currentState.includes('active')) { return; } if (extendedState) { this.set('componentState', extendedState); - this.saveExtState(STORAGE_KEYS.COMPONENT_STATE, extendedState); + this.saveExtState(COMPONENT_STATE, extendedState); } let { actions, value } = FeatureMachine.transition(currentState, event, this.get('componentState')); this.saveState('featureState', value); - this.saveExtState(STORAGE_KEYS.FEATURE_STATE, value); + this.saveExtState(FEATURE_STATE, value); this.executeActions(actions, event, 'feature'); // if all features were completed, the FeatureMachine gets nulled // out and won't exist here as there is no next step if (FeatureMachine) { let next; - if (this.get('currentMachine') === 'secrets' && value === 'display') { - next = FeatureMachine.transition(value, 'REPEAT', this.get('componentState')); + if (this.currentMachine === 'secrets' && value === 'display') { + next = FeatureMachine.transition(value, 'REPEAT', this.componentState); } else { - next = FeatureMachine.transition(value, 'CONTINUE', this.get('componentState')); + next = FeatureMachine.transition(value, 'CONTINUE', this.componentState); } this.saveState('nextStep', next.value); } @@ -129,7 +176,7 @@ export default Service.extend(DEFAULTS, { executeActions(actions, event, machineType) { let transitionURL; let expectedRouteName; - let router = this.get('router'); + let router = this.router; for (let action of actions) { let type = action; @@ -168,8 +215,11 @@ export default Service.extend(DEFAULTS, { case 'showTutorialAlways': this.set('showWhenUnauthenticated', true); break; + case 'clearFeatureData': + this.clearFeatureData(); + break; case 'continueFeature': - this.transitionFeatureMachine(this.get('featureState'), 'CONTINUE', this.get('componentState')); + this.transitionFeatureMachine(this.featureState, 'CONTINUE', this.componentState); break; default: break; @@ -192,94 +242,107 @@ export default Service.extend(DEFAULTS, { }, handlePaused() { - let expected = this.get('expectedURL'); + let expected = this.expectedURL; if (expected) { - this.saveExtState(STORAGE_KEYS.RESUME_URL, this.get('expectedURL')); - this.saveExtState(STORAGE_KEYS.RESUME_ROUTE, this.get('expectedRouteName')); + this.saveExtState(RESUME_URL, this.expectedURL); + this.saveExtState(RESUME_ROUTE, this.expectedRouteName); } }, handleResume() { - let resumeURL = this.storage().getItem(STORAGE_KEYS.RESUME_URL); + let resumeURL = this.storage().getItem(RESUME_URL); if (!resumeURL) { return; } - this.get('router').transitionTo(resumeURL).followRedirects().then(() => { - this.set('expectedRouteName', this.storage().getItem(STORAGE_KEYS.RESUME_ROUTE)); - this.set('expectedURL', resumeURL); - this.initializeMachines(); - this.storage().removeItem(STORAGE_KEYS.RESUME_URL); - }); + this.get('router') + .transitionTo(resumeURL) + .followRedirects() + .then(() => { + this.set('expectedRouteName', this.storage().getItem(RESUME_ROUTE)); + this.set('expectedURL', resumeURL); + this.initializeMachines(); + this.storage().removeItem(RESUME_URL); + }); }, handleDismissed() { - this.storage().removeItem(STORAGE_KEYS.FEATURE_STATE); - this.storage().removeItem(STORAGE_KEYS.FEATURE_LIST); - this.storage().removeItem(STORAGE_KEYS.COMPONENT_STATE); + this.storage().removeItem(FEATURE_STATE); + this.storage().removeItem(FEATURE_LIST); + this.storage().removeItem(FEATURE_STATE_HISTORY); + this.storage().removeItem(COMPONENT_STATE); }, saveFeatures(features) { this.set('featureList', features); - this.saveExtState(STORAGE_KEYS.FEATURE_LIST, this.get('featureList')); + this.saveExtState(FEATURE_LIST, this.featureList); this.buildFeatureMachine(); }, buildFeatureMachine() { - if (this.get('featureList') === null) { + if (this.featureList === null) { return; } this.startFeature(); - if (this.storageHasKey(STORAGE_KEYS.FEATURE_STATE)) { - this.saveState('featureState', this.getExtState(STORAGE_KEYS.FEATURE_STATE)); - } - this.saveExtState(STORAGE_KEYS.FEATURE_STATE, this.get('featureState')); - let nextFeature = - this.get('featureList').length > 1 - ? this.get('featureList') - .objectAt(1) - .capitalize() - : 'Finish'; + let nextFeature = this.featureList.length > 1 ? this.featureList.objectAt(1).capitalize() : 'Finish'; this.set('nextFeature', nextFeature); let next; - if (this.get('currentMachine') === 'secrets' && this.get('featureState') === 'display') { - next = FeatureMachine.transition(this.get('featureState'), 'REPEAT', this.get('componentState')); + if (this.currentMachine === 'secrets' && this.featureState === 'display') { + next = FeatureMachine.transition(this.featureState, 'REPEAT', this.componentState); } else { - next = FeatureMachine.transition(this.get('featureState'), 'CONTINUE', this.get('componentState')); + next = FeatureMachine.transition(this.featureState, 'CONTINUE', this.componentState); } this.saveState('nextStep', next.value); - let stateNodes = FeatureMachine.getStateNodes(this.get('featureState')); + let stateNodes = FeatureMachine.getStateNodes(this.featureState); this.executeActions(stateNodes.reduce((acc, node) => acc.concat(node.onEntry), []), null, 'feature'); }, startFeature() { - const FeatureMachineConfig = MACHINES[this.get('featureList').objectAt(0)]; + const FeatureMachineConfig = MACHINES[this.featureList.objectAt(0)]; FeatureMachine = Machine(FeatureMachineConfig); - this.set('currentMachine', this.get('featureList').objectAt(0)); - this.saveState('featureState', FeatureMachine.initialState); + this.set('currentMachine', this.featureList.objectAt(0)); + if (this.storageHasKey(FEATURE_STATE)) { + this.saveState('featureState', this.getExtState(FEATURE_STATE)); + } else { + this.saveState('featureState', FeatureMachine.initialState); + } + this.saveExtState(FEATURE_STATE, this.featureState); + }, + + getCompletedFeatures() { + if (this.storageHasKey(COMPLETED_FEATURES)) { + return this.getExtState(COMPLETED_FEATURES).toArray(); + } + return []; }, completeFeature() { - let features = this.get('featureList'); + let features = this.featureList; let done = features.shift(); - if (!this.getExtState(STORAGE_KEYS.COMPLETED_FEATURES)) { + if (!this.getExtState(COMPLETED_FEATURES)) { let completed = []; completed.push(done); - this.saveExtState(STORAGE_KEYS.COMPLETED_FEATURES, completed); + this.saveExtState(COMPLETED_FEATURES, completed); } else { this.saveExtState( - STORAGE_KEYS.COMPLETED_FEATURES, - this.getExtState(STORAGE_KEYS.COMPLETED_FEATURES).toArray().addObject(done) + COMPLETED_FEATURES, + this.getExtState(COMPLETED_FEATURES) + .toArray() + .addObject(done) ); } - this.saveExtState(STORAGE_KEYS.FEATURE_LIST, features.length ? features : null); - this.storage().removeItem(STORAGE_KEYS.FEATURE_STATE); + this.saveExtState(FEATURE_LIST, features.length ? features : null); + this.storage().removeItem(FEATURE_STATE); + if (this.featureMachineHistory) { + this.set('featureMachineHistory', []); + this.saveExtState(FEATURE_STATE_HISTORY, []); + } if (features.length > 0) { this.buildFeatureMachine(); } else { - this.storage().removeItem(STORAGE_KEYS.FEATURE_LIST); + this.storage().removeItem(FEATURE_LIST); FeatureMachine = null; - this.transitionTutorialMachine(this.get('currentState'), 'DONE'); + this.transitionTutorialMachine(this.currentState, 'DONE'); } }, diff --git a/ui/app/styles/components/ui-wizard.scss b/ui/app/styles/components/ui-wizard.scss index 7a5a8c7c86..8648f8bd78 100644 --- a/ui/app/styles/components/ui-wizard.scss +++ b/ui/app/styles/components/ui-wizard.scss @@ -67,7 +67,7 @@ .wizard-header { border-bottom: $light-border; - padding: $size-8 $size-4 $size-8 2rem; + padding: 0 $size-4 $size-8 2rem; margin: $size-4 0; position: relative; @@ -79,7 +79,7 @@ .title .icon { left: 0; position: absolute; - top: 0.7rem; + top: 0; } } @@ -173,3 +173,91 @@ .wizard-instructions { margin: $size-4 0; } + +.selection-summary { + display: flex; + align-items: center; + width: 100%; + justify-content: space-between; +} + +.time-estimate { + align-items: center; + color: $grey; + display: flex; + font-size: 12px; +} + +.progress-container { + align-items: center; + background: $white; + bottom: 0; + height: $wizard-progress-bar-height; + display: flex; + left: 0; + padding: 0; + position: absolute; + right: 0; + transform: translateY(50%); + width: 100%; +} + +.progress-bar { + background: $ui-gray-100; + box-shadow: inset 0 0 0 1px $ui-gray-200; + display: flex; + height: $wizard-progress-bar-height; + position: relative; + width: 100%; +} + +.feature-progress-container { + align-items: center; + flex: 1 0 auto; + padding: 0 ($wizard-progress-check-size / 4); + position: relative; +} + +.feature-progress { + background: $green; + border-radius: $wizard-progress-bar-height; + height: $wizard-progress-bar-height; +} + +.feature-check { + height: $wizard-progress-check-size; + left: $wizard-progress-check-size / 2; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + width: $wizard-progress-check-size; + z-index: 10; +} + +.feature-progress-container .feature-check { + left: 100%; +} + +.feature-progress-container:first-child { + padding-left: 0; + + .progress-bar, + .feature-progress { + border-radius: $wizard-progress-bar-height 0 0 $wizard-progress-bar-height; + } +} + +.feature-progress-container:first-child:last-child { + .progress-bar, + .feature-progress { + border-radius: $wizard-progress-bar-height; + } +} + +.incomplete-check svg { + fill: $ui-gray-200; +} + +.completed-check svg { + fill: $green; +} diff --git a/ui/app/styles/core/buttons.scss b/ui/app/styles/core/buttons.scss index d317eaa902..f802a935f6 100644 --- a/ui/app/styles/core/buttons.scss +++ b/ui/app/styles/core/buttons.scss @@ -217,14 +217,17 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12); } .button.next-feature-step { - width: 100%; - text-align: left; background: $white; - color: $blue; - box-shadow: none; - display: block; border: 1px solid $grey-light; border-radius: $radius; + box-shadow: none; + color: $blue; + display: flex; height: auto; + line-height: 1.2; + justify-content: space-between; + text-align: left; + white-space: normal; padding: $size-8; + width: 100%; } diff --git a/ui/app/styles/utils/_bulma_variables.scss b/ui/app/styles/utils/_bulma_variables.scss index af61e9b863..16852bf611 100644 --- a/ui/app/styles/utils/_bulma_variables.scss +++ b/ui/app/styles/utils/_bulma_variables.scss @@ -84,3 +84,7 @@ $box-link-hover-shadow: 0 0 0 1px $grey-light; // animations $speed: 150ms; $speed-slow: $speed * 2; + +// Wizard +$wizard-progress-bar-height: 6px; +$wizard-progress-check-size: 16px; diff --git a/ui/app/templates/components/wizard-content.hbs b/ui/app/templates/components/wizard-content.hbs index d69cb6b750..47001a2a44 100644 --- a/ui/app/templates/components/wizard-content.hbs +++ b/ui/app/templates/components/wizard-content.hbs @@ -15,5 +15,26 @@

{{headerText}}

+ {{#if showProgress}} + + + + + +
+ {{#if currentTutorialProgress}} + {{currentTutorialProgress.text}} + {{else}} +

{{capitalize currentFeatureProgress.feature}}

+ {{currentFeatureProgress.text}} + {{/if}} +
+
+
+ {{else}} + {{#if selectProgress}} + + {{/if}} + {{/if}} {{yield}} diff --git a/ui/app/templates/components/wizard-progress.hbs b/ui/app/templates/components/wizard-progress.hbs new file mode 100644 index 0000000000..fcef853292 --- /dev/null +++ b/ui/app/templates/components/wizard-progress.hbs @@ -0,0 +1,12 @@ +
+ {{#each progressBar as |bar|}} +
+ + + + {{#if bar.showIcon}} + + {{/if}} +
+ {{/each}} +
diff --git a/ui/app/templates/components/wizard/auth-details.hbs b/ui/app/templates/components/wizard/auth-details.hbs index 955280f2ae..1d22d91c64 100644 --- a/ui/app/templates/components/wizard/auth-details.hbs +++ b/ui/app/templates/components/wizard/auth-details.hbs @@ -12,9 +12,9 @@ @class="wizard-details" > diff --git a/ui/app/templates/components/wizard/features-selection.hbs b/ui/app/templates/components/wizard/features-selection.hbs index db99f44b36..c3b4356422 100644 --- a/ui/app/templates/components/wizard/features-selection.hbs +++ b/ui/app/templates/components/wizard/features-selection.hbs @@ -1,4 +1,4 @@ - +

Choosing where to go

@@ -36,6 +36,11 @@ {{/if}} {{/each}} - + + + {{#if selectedFeatures}} + About {{estimatedTime}} minutes + {{/if}} +
diff --git a/ui/app/templates/components/wizard/policies-others.hbs b/ui/app/templates/components/wizard/policies-others.hbs index 25eac94450..66fbeb9a5e 100644 --- a/ui/app/templates/components/wizard/policies-others.hbs +++ b/ui/app/templates/components/wizard/policies-others.hbs @@ -13,7 +13,7 @@ @class="wizard-details" >
diff --git a/ui/app/templates/components/wizard/replication-details.hbs b/ui/app/templates/components/wizard/replication-details.hbs index 6baf51649f..ffff094549 100644 --- a/ui/app/templates/components/wizard/replication-details.hbs +++ b/ui/app/templates/components/wizard/replication-details.hbs @@ -13,7 +13,7 @@ Ready to move on? diff --git a/ui/app/templates/components/wizard/secrets-display.hbs b/ui/app/templates/components/wizard/secrets-display.hbs index c69c7cc309..371d14ac2a 100644 --- a/ui/app/templates/components/wizard/secrets-display.hbs +++ b/ui/app/templates/components/wizard/secrets-display.hbs @@ -30,9 +30,9 @@ {{/if}} \ No newline at end of file diff --git a/ui/app/templates/components/wizard/tools-unwrapped.hbs b/ui/app/templates/components/wizard/tools-unwrapped.hbs index b605be3421..aefa155e2a 100644 --- a/ui/app/templates/components/wizard/tools-unwrapped.hbs +++ b/ui/app/templates/components/wizard/tools-unwrapped.hbs @@ -9,6 +9,6 @@

diff --git a/ui/app/templates/components/wizard/tutorial-error.hbs b/ui/app/templates/components/wizard/tutorial-error.hbs index 5e76e9c249..f15d898152 100644 --- a/ui/app/templates/components/wizard/tutorial-error.hbs +++ b/ui/app/templates/components/wizard/tutorial-error.hbs @@ -7,9 +7,9 @@ diff --git a/ui/app/templates/svg/icons/check-circle-fill.hbs b/ui/app/templates/svg/icons/check-circle-fill.hbs new file mode 100644 index 0000000000..77922e3055 --- /dev/null +++ b/ui/app/templates/svg/icons/check-circle-fill.hbs @@ -0,0 +1,3 @@ + + + diff --git a/ui/app/templates/svg/icons/check-plain.hbs b/ui/app/templates/svg/icons/check-plain.hbs new file mode 100644 index 0000000000..1373b01c81 --- /dev/null +++ b/ui/app/templates/svg/icons/check-plain.hbs @@ -0,0 +1,3 @@ + + + diff --git a/ui/tests/unit/machines/tutorial-machine-test.js b/ui/tests/unit/machines/tutorial-machine-test.js index aa4e33c469..f589d23eb5 100644 --- a/ui/tests/unit/machines/tutorial-machine-test.js +++ b/ui/tests/unit/machines/tutorial-machine-test.js @@ -119,6 +119,7 @@ module('Unit | Machine | tutorial-machine', function() { }, actions: [ 'showTutorialWhenAuthenticated', + 'clearFeatureData', { type: 'render', level: 'tutorial', component: 'wizard/tutorial-active' }, { type: 'render', level: 'feature', component: 'wizard/features-selection' }, ], diff --git a/ui/tests/unit/services/wizard-test.js b/ui/tests/unit/services/wizard-test.js index a78e8ba5f1..a3734d7e81 100644 --- a/ui/tests/unit/services/wizard-test.js +++ b/ui/tests/unit/services/wizard-test.js @@ -146,6 +146,7 @@ module('Unit | Service | wizard', function(hooks) { ], storage: [ { key: STORAGE_KEYS.FEATURE_STATE, value: undefined }, + { key: STORAGE_KEYS.FEATURE_STATE_HISTORY, value: undefined }, { key: STORAGE_KEYS.FEATURE_LIST, value: undefined }, { key: STORAGE_KEYS.COMPONENT_STATE, value: undefined }, { key: STORAGE_KEYS.TUTORIAL_STATE, value: 'active.select' }, @@ -155,6 +156,19 @@ module('Unit | Service | wizard', function(hooks) { ], }, }, + { + method: 'clearFeatureData', + args: [], + expectedResults: { + props: [{ prop: 'currentMachine', value: null }, { prop: 'featureMachineHistory', value: null }], + storage: [ + { key: STORAGE_KEYS.FEATURE_STATE, value: undefined }, + { key: STORAGE_KEYS.FEATURE_STATE_HISTORY, value: undefined }, + { key: STORAGE_KEYS.FEATURE_LIST, value: undefined }, + { key: STORAGE_KEYS.COMPONENT_STATE, value: undefined }, + ], + }, + }, { method: 'saveState', args: [ @@ -194,6 +208,75 @@ module('Unit | Service | wizard', function(hooks) { props: [{ prop: 'currentState', value: 'login' }], }, }, + { + method: 'saveFeatureHistory', + args: ['idle'], + properties: { featureList: ['policies', 'tools'] }, + storage: [{ key: STORAGE_KEYS.COMPLETED_FEATURES, value: ['secrets'] }], + expectedResults: { + props: [{ prop: 'featureMachineHistory', value: null }], + }, + }, + { + method: 'saveFeatureHistory', + args: ['idle'], + properties: { featureList: ['policies', 'tools'] }, + storage: [], + expectedResults: { + props: [{ prop: 'featureMachineHistory', value: ['idle'] }], + }, + }, + { + method: 'saveFeatureHistory', + args: ['idle'], + properties: { featureList: ['policies', 'tools'] }, + storage: [], + expectedResults: { + props: [{ prop: 'featureMachineHistory', value: ['idle'] }], + }, + }, + { + method: 'saveFeatureHistory', + args: ['idle'], + properties: { featureMachineHistory: [], featureList: ['policies', 'tools'] }, + storage: [{ key: STORAGE_KEYS.COMPLETED_FEATURES, value: ['secrets'] }], + expectedResults: { + props: [{ prop: 'featureMachineHistory', value: ['idle'] }], + storage: [{ key: STORAGE_KEYS.FEATURE_STATE_HISTORY, value: ['idle'] }], + }, + }, + { + method: 'saveFeatureHistory', + args: ['idle'], + properties: { featureMachineHistory: null, featureList: ['policies', 'tools'] }, + storage: [{ key: STORAGE_KEYS.COMPLETED_FEATURES, value: ['secrets'] }], + expectedResults: { + props: [{ prop: 'featureMachineHistory', value: null }], + }, + }, + { + method: 'saveFeatureHistory', + args: ['create'], + properties: { featureMachineHistory: ['idle'], featureList: ['policies', 'tools'] }, + storage: [{ key: STORAGE_KEYS.COMPLETED_FEATURES, value: ['secrets'] }], + expectedResults: { + props: [{ prop: 'featureMachineHistory', value: ['idle', 'create'] }], + storage: [{ key: STORAGE_KEYS.FEATURE_STATE_HISTORY, value: ['idle', 'create'] }], + }, + }, + { + method: 'saveFeatureHistory', + args: ['create'], + properties: { featureMachineHistory: ['idle'], featureList: ['policies', 'tools'] }, + storage: [ + { key: STORAGE_KEYS.COMPLETED_FEATURES, value: ['secrets'] }, + { key: STORAGE_KEYS.FEATURE_STATE_HISTORY, value: ['idle', 'create'] }, + ], + expectedResults: { + props: [{ prop: 'featureMachineHistory', value: ['idle', 'create'] }], + storage: [{ key: STORAGE_KEYS.FEATURE_STATE_HISTORY, value: ['idle', 'create'] }], + }, + }, { method: 'startFeature', args: [], From 11d671fab1aa5886aaa09b914f123735a50ec290 Mon Sep 17 00:00:00 2001 From: madalynrose Date: Thu, 18 Oct 2018 15:24:59 -0400 Subject: [PATCH 10/50] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4964f6c476..269f8dc057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ IMPROVEMENTS: role creation permissions [GH-5402] * secret/radius: Allow setting the NAS Identifier value in the generated packet [GH-5465] + * ui: Allow viewing and updating Vault license via the UI + * ui: Onboarding will now display your progress through the chosen tutorials BUG FIXES: From 89c6ab6dd6dee1c62ce1a09b59cef5700a3a5e9d Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Wed, 3 Oct 2018 23:31:23 -0500 Subject: [PATCH 11/50] don't leave new lines on the end of files --- ui/.eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js index ea6c16123d..e2ea8c32ec 100644 --- a/ui/.eslintrc.js +++ b/ui/.eslintrc.js @@ -15,6 +15,7 @@ module.exports = { }, rules: { 'no-unused-vars': ['error', { ignoreRestSiblings: true }], + 'eol-last': 'never', }, globals: { TextEncoderLite: true, From cae9f6fffe804ce8ff68706531f8566bf8773402 Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Wed, 3 Oct 2018 23:32:55 -0500 Subject: [PATCH 12/50] new model things for secret-v2 and secret-v2 versions: get list, queryRecord, and version find working --- ui/app/adapters/secret-v2-version.js | 23 +++++++++ ui/app/adapters/secret-v2.js | 47 +++++++++++-------- ui/app/models/secret-v2-version.js | 8 ++++ ui/app/models/secret-v2.js | 14 +++++- .../cluster/secrets/backend/secret-edit.js | 9 +++- ui/app/serializers/secret-v2-version.js | 25 ++++++++++ ui/app/serializers/secret-v2.js | 41 ++++++++++++++-- 7 files changed, 142 insertions(+), 25 deletions(-) create mode 100644 ui/app/adapters/secret-v2-version.js create mode 100644 ui/app/models/secret-v2-version.js create mode 100644 ui/app/serializers/secret-v2-version.js diff --git a/ui/app/adapters/secret-v2-version.js b/ui/app/adapters/secret-v2-version.js new file mode 100644 index 0000000000..3927748bb4 --- /dev/null +++ b/ui/app/adapters/secret-v2-version.js @@ -0,0 +1,23 @@ +import { isEmpty } from '@ember/utils'; +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + namespace: 'v1', + _url(backend, id) { + let url = `${this.buildURL()}/${backend}/data/`; + if (!isEmpty(id)) { + url = url + id; + } + return url; + }, + + urlForFindRecord(id) { + let [backend, path, version] = JSON.parse(id); + return this._url(backend, path) + `?version=${version}`; + }, + + deleteRecord(store, type, snapshot) { + // use adapterOptions to determine if it's delete or destroy for the version + return this._super(...arguments); + }, +}); diff --git a/ui/app/adapters/secret-v2.js b/ui/app/adapters/secret-v2.js index d76fb2438c..aa12c2169f 100644 --- a/ui/app/adapters/secret-v2.js +++ b/ui/app/adapters/secret-v2.js @@ -1,34 +1,41 @@ +/* eslint-disable */ import { isEmpty } from '@ember/utils'; -import SecretAdapter from './secret'; +import ApplicationAdapter from './application'; -export default SecretAdapter.extend({ - createOrUpdate(store, type, snapshot) { - const serializer = store.serializerFor(type.modelName); - const data = serializer.serialize(snapshot); - const { id } = snapshot; - - return this.ajax(this.urlForSecret(snapshot.attr('backend'), id), 'POST', { - data: { data }, - }); - }, - - urlForSecret(backend, id, infix = 'data') { - let url = `${this.buildURL()}/${backend}/${infix}/`; +export default ApplicationAdapter.extend({ + namespace: 'v1', + _url(backend, id) { + let url = `${this.buildURL()}/${backend}/metadata/`; if (!isEmpty(id)) { url = url + id; } return url; }, - fetchByQuery(query, methodCall) { + // we override query here because the query object has a bunch of client-side + // concerns and we only want to send "list" to the server + query(store, type, query) { + let { backend, id } = query; + return this.ajax(this._url(backend, id), 'GET', { data: { list: true } }); + }, + + urlForQueryRecord(query) { let { id, backend } = query; - let args = [backend, id]; - if (methodCall === 'query') { - args.push('metadata'); - } - return this.ajax(this.urlForSecret(...args), 'GET', this.optionsForQuery(id, methodCall)).then(resp => { + return this._url(backend) + id; + }, + + queryRecord(store, type, query) { + let { backend, id } = query; + return this._super(...arguments).then(resp => { resp.id = id; + resp.backend = backend; return resp; }); }, + + urlForDeleteRecord(store, type, snapshot) { + let backend = snapshot.belongsTo('secret-engine', { id: true }); + let { id } = snapshot; + return this.urlForQueryRecord({ id, backend }); + }, }); diff --git a/ui/app/models/secret-v2-version.js b/ui/app/models/secret-v2-version.js new file mode 100644 index 0000000000..3c7ca467ac --- /dev/null +++ b/ui/app/models/secret-v2-version.js @@ -0,0 +1,8 @@ +import Secret from './secret'; +import DS from 'ember-data'; + +const { attr } = DS; + +export default Secret.extend({ + version: attr('number'), +}); diff --git a/ui/app/models/secret-v2.js b/ui/app/models/secret-v2.js index fcbf908caf..cfaef24fd2 100644 --- a/ui/app/models/secret-v2.js +++ b/ui/app/models/secret-v2.js @@ -1,3 +1,15 @@ import Secret from './secret'; +import DS from 'ember-data'; -export default Secret.extend(); +const { attr, hasMany, belongsTo, Model } = DS; + +export default Model.extend({ + engine: belongsTo('secret-engine'), + versions: hasMany('secret-v2-version', { async: false }), + createdTime: attr(), + updatedTime: attr(), + currentVersion: attr('number'), + oldestVersion: attr('number'), + maxVersions: attr('number'), + casRequired: attr('boolean'), +}); 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 fb0cbb64ac..67aec2bf6c 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js +++ b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js @@ -72,7 +72,14 @@ export default Route.extend(UnloadModelRoute, { secret = secret.replace('cert/', ''); } return hash({ - secret: this.store.queryRecord(modelType, { id: secret, backend }), + secret: this.store.queryRecord(modelType, { id: secret, backend }).then(resp => { + if (modelType === 'secret-v2') { + // TODO, find by query param to enable viewing versions + let version = resp.versions.findBy('version', resp.currentVersion); + version.reload(); + } + return resp; + }), capabilities: this.capabilities(secret), }); }, diff --git a/ui/app/serializers/secret-v2-version.js b/ui/app/serializers/secret-v2-version.js new file mode 100644 index 0000000000..31ffc9c902 --- /dev/null +++ b/ui/app/serializers/secret-v2-version.js @@ -0,0 +1,25 @@ +import { get } from '@ember/object'; +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + secretDataPath: 'data.data', + normalizeItems(payload, requestType) { + let path = this.secretDataPath; + // move response that is the contents of the secret from the dataPath + // to `secret_data` so it will be `secretData` in the model + payload.secret_data = get(payload, path); + payload = Object.assign({}, payload, payload.data.metadata); + delete payload.data; + // return the payload if it's expecting a single object or wrap + // it as an array if not + return payload; + }, + serialize(snapshot) { + return { + data: snapshot.attr('secretData'), + options: { + cas: snapshot.attr('currentVerion'), + }, + }; + }, +}); diff --git a/ui/app/serializers/secret-v2.js b/ui/app/serializers/secret-v2.js index 9a43c82b08..4dbbfd305d 100644 --- a/ui/app/serializers/secret-v2.js +++ b/ui/app/serializers/secret-v2.js @@ -1,5 +1,40 @@ -import SecretSerializer from './secret'; +import ApplicationSerializer from './application'; +import DS from 'ember-data'; -export default SecretSerializer.extend({ - secretDataPath: 'data.data', +export default ApplicationSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + versions: { embedded: 'always' }, + }, + secretDataPath: 'data', + normalizeItems(payload, requestType) { + if (payload.data.keys && Array.isArray(payload.data.keys)) { + // if we have data.keys, it's a list of ids, so we map over that + // and create objects with id's + return payload.data.keys.map(secret => { + // secrets don't have an id in the response, so we need to concat the full + // path of the secret here - the id in the payload is added + // in the adapter after making the request + let fullSecretPath = payload.id ? payload.id + secret : secret; + + // if there is no path, it's a "top level" secret, so add + // a unicode space for the id + // https://github.com/hashicorp/vault/issues/3348 + if (!fullSecretPath) { + fullSecretPath = '\u0020'; + } + return { id: fullSecretPath }; + }); + } + if (payload.data.versions) { + payload.data.versions = Object.keys(payload.data.versions).map(version => { + let body = payload.data.versions[version]; + body.version = version; + body.id = JSON.stringify([payload.backend, payload.id, version]); + return body; + }); + console.log(payload); + } + payload.data.id = payload.id; + return requestType === 'queryRecord' ? payload.data : [payload.data]; + }, }); From 3bd0250fc803e96e595ca96f0215b7dc88d3cf59 Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Thu, 4 Oct 2018 01:06:01 -0500 Subject: [PATCH 13/50] get rid of v2 incomplete warning --- ui/app/routes/vault/cluster/secrets/backend.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/ui/app/routes/vault/cluster/secrets/backend.js b/ui/app/routes/vault/cluster/secrets/backend.js index c7769c9f5a..b7964a59b1 100644 --- a/ui/app/routes/vault/cluster/secrets/backend.js +++ b/ui/app/routes/vault/cluster/secrets/backend.js @@ -16,16 +16,8 @@ export default Route.extend({ }, afterModel(model, transition) { - let target = transition.targetName; let path = model && model.get('path'); - let type = model && model.get('type'); - if (type === 'kv' && model.get('options.version') === 2) { - this.get('flashMessages').stickyInfo( - `"${path}" is a newer version of the KV backend. The Vault UI does not currently support the additional versioning features. All actions taken through the UI in this engine will operate on the most recent version of a secret.` - ); - } - - if (target === this.routeName) { + if (transition.targetName === this.routeName) { return this.replaceWith('vault.cluster.secrets.backend.list-root', path); } }, From 7e09c692b4489ab503c55e58a49d42c8195b8397 Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Fri, 5 Oct 2018 21:58:54 -0500 Subject: [PATCH 14/50] move to es5 getter usage for csp service --- ui/app/services/csp-event.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/app/services/csp-event.js b/ui/app/services/csp-event.js index 7cfceae0f4..df1a5779d2 100644 --- a/ui/app/services/csp-event.js +++ b/ui/app/services/csp-event.js @@ -12,19 +12,19 @@ export default Service.extend({ connectionViolations: filterBy('events', 'violatedDirective', 'connect-src'), attach() { - this.get('monitor').perform(); + this.monitor.perform(); }, remove() { - this.get('monitor').cancelAll(); + this.monitor.cancelAll(); }, monitor: task(function*() { - this.get('events').clear(); + this.events.clear(); while (true) { let event = yield waitForEvent(window.document, 'securitypolicyviolation'); - this.get('events').addObject(event); + this.events.addObject(event); } }), }); From f31b7f9ade33bd7cff78c7a2b40c7593a75459f3 Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Fri, 5 Oct 2018 21:59:35 -0500 Subject: [PATCH 15/50] user correct rule for eol --- ui/.eslintrc.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js index e2ea8c32ec..ea6c16123d 100644 --- a/ui/.eslintrc.js +++ b/ui/.eslintrc.js @@ -15,7 +15,6 @@ module.exports = { }, rules: { 'no-unused-vars': ['error', { ignoreRestSiblings: true }], - 'eol-last': 'never', }, globals: { TextEncoderLite: true, From 14a410ecf40e1ee1170324b47a2e363c1ce569c1 Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Fri, 5 Oct 2018 22:01:02 -0500 Subject: [PATCH 16/50] fix navigate component --- ui/app/components/navigate-input.js | 9 ++++---- .../templates/components/navigate-input.hbs | 21 ++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/ui/app/components/navigate-input.js b/ui/app/components/navigate-input.js index f642b6d1b1..cf774ecf98 100644 --- a/ui/app/components/navigate-input.js +++ b/ui/app/components/navigate-input.js @@ -163,24 +163,25 @@ export default Component.extend(FocusOnInsertMixin, { }, actions: { - handleInput: function(event) { - var filter = event.target.value; + handleInput: function(filter) { this.get('filterDidChange')(filter); debounce(this, 'filterUpdated', filter, 200); }, setFilterFocused: function(isFocused) { this.get('filterFocusDidChange')(isFocused); + console.log(isFocused); }, - handleKeyPress: function(val, event) { + handleKeyPress: function(event) { if (event.keyCode === keys.TAB) { this.onTab(event); } }, - handleKeyUp: function(val, event) { + handleKeyUp: function(event) { var keyCode = event.keyCode; + let val = event.target.value; if (keyCode === keys.ENTER) { this.onEnter(val); } diff --git a/ui/app/templates/components/navigate-input.hbs b/ui/app/templates/components/navigate-input.hbs index e1d1874bef..3b7adf392e 100644 --- a/ui/app/templates/components/navigate-input.hbs +++ b/ui/app/templates/components/navigate-input.hbs @@ -1,16 +1,17 @@

- {{input - value=filter - placeholder=(or placeholder "Filter keys") + {{i-con glyph="ios-search-strong" class="is-left has-text-grey" size=18}}

From 7e295b9b7732285d8144e03ff5d3bdffa20e858a Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Fri, 5 Oct 2018 22:02:23 -0500 Subject: [PATCH 17/50] finish v2 model layer and add some unit tests for adapters --- ui/app/adapters/secret-v2-version.js | 34 +++++++- ui/app/adapters/secret-v2.js | 21 +++-- ui/app/models/key-mixin.js | 31 +++++--- ui/app/models/secret-v2-version.js | 6 +- ui/app/models/secret-v2.js | 6 +- ui/app/serializers/secret-v2-version.js | 15 ++-- ui/app/serializers/secret-v2.js | 3 +- ui/tests/unit/adapters/secret-v2-test.js | 76 ++++++++++++++---- .../unit/adapters/secret-v2-version-test.js | 79 +++++++++++++++++++ 9 files changed, 227 insertions(+), 44 deletions(-) create mode 100644 ui/tests/unit/adapters/secret-v2-version-test.js diff --git a/ui/app/adapters/secret-v2-version.js b/ui/app/adapters/secret-v2-version.js index 3927748bb4..daae091768 100644 --- a/ui/app/adapters/secret-v2-version.js +++ b/ui/app/adapters/secret-v2-version.js @@ -1,10 +1,11 @@ +/* eslint-disable */ import { isEmpty } from '@ember/utils'; import ApplicationAdapter from './application'; export default ApplicationAdapter.extend({ namespace: 'v1', - _url(backend, id) { - let url = `${this.buildURL()}/${backend}/data/`; + _url(backend, id, infix = 'data') { + let url = `${this.buildURL()}/${backend}/${infix}/`; if (!isEmpty(id)) { url = url + id; } @@ -16,8 +17,37 @@ export default ApplicationAdapter.extend({ return this._url(backend, path) + `?version=${version}`; }, + urlForCreateRecord(modelName, snapshot) { + let backend = snapshot.belongsTo('secret').belongsTo('engine').id; + let path = snapshot.attr('path'); + return this._url(backend, path); + }, + + createRecord(store, modelName, snapshot) { + let backend = snapshot.belongsTo('secret').belongsTo('engine').id; + let path = snapshot.attr('path'); + return this._super(...arguments).then(resp => { + resp.id = JSON.stringify([backend, path, resp.version]); + return resp; + }); + }, + + urlForUpdateRecord(id) { + let [backend, path] = JSON.parse(id); + return this._url(backend, path); + }, + deleteRecord(store, type, snapshot) { // use adapterOptions to determine if it's delete or destroy for the version + // deleteType should be 'delete', 'destroy', 'undelete' + let infix = snapshot.adapterOptions.deleteType; + let [backend, path, version] = JSON.parse(snapshot.id); + + return this.ajax(this._url(backend, path, infix), 'POST', { data: { versions: [version] } }); + }, + + handleResponse(/*status, headers, payload, requestData*/) { + // the body of the 404 will have some relevant information return this._super(...arguments); }, }); diff --git a/ui/app/adapters/secret-v2.js b/ui/app/adapters/secret-v2.js index aa12c2169f..4b7937ffac 100644 --- a/ui/app/adapters/secret-v2.js +++ b/ui/app/adapters/secret-v2.js @@ -16,26 +16,35 @@ export default ApplicationAdapter.extend({ // concerns and we only want to send "list" to the server query(store, type, query) { let { backend, id } = query; - return this.ajax(this._url(backend, id), 'GET', { data: { list: true } }); + return this.ajax(this._url(backend, id), 'GET', { data: { list: true } }).then(resp => { + resp.id = id; + return resp; + }); }, urlForQueryRecord(query) { let { id, backend } = query; - return this._url(backend) + id; + return this._url(backend, id); }, queryRecord(store, type, query) { let { backend, id } = query; - return this._super(...arguments).then(resp => { + return this.ajax(this._url(backend, id), 'GET').then(resp => { resp.id = id; resp.backend = backend; return resp; }); }, - urlForDeleteRecord(store, type, snapshot) { - let backend = snapshot.belongsTo('secret-engine', { id: true }); + urlForUpdateRecord(store, type, snapshot) { + let backend = snapshot.belongsTo('engine', { id: true }); let { id } = snapshot; - return this.urlForQueryRecord({ id, backend }); + return this._url(backend, id); + }, + + urlForDeleteRecord(store, type, snapshot) { + let backend = snapshot.belongsTo('engine', { id: true }); + let { id } = snapshot; + return this._url(backend, id); }, }); diff --git a/ui/app/models/key-mixin.js b/ui/app/models/key-mixin.js index 8b505ece50..ec0c1dc035 100644 --- a/ui/app/models/key-mixin.js +++ b/ui/app/models/key-mixin.js @@ -3,6 +3,9 @@ import Mixin from '@ember/object/mixin'; import utils from '../lib/key-utils'; export default Mixin.create({ + // what attribute has the path for the key + // will.be 'path' for v2 or 'id' v1 + pathAttr: 'id', flags: null, initialParentKey: null, @@ -11,33 +14,39 @@ export default Mixin.create({ return this.get('initialParentKey') != null; }), - isFolder: computed('id', function() { - return utils.keyIsFolder(this.get('id')); + pathVal() { + return this.get(this.pathAttr); + }, + + // rather than using defineProperty for all of these, + // we're just going to hardcode the known keys for the path ('id' and 'path') + isFolder: computed('id', 'path', function() { + return utils.keyIsFolder(this.pathVal()); }), - keyParts: computed('id', function() { - return utils.keyPartsForKey(this.get('id')); + keyParts: computed('id', 'path', function() { + return utils.keyPartsForKey(this.pathVal()); }), - parentKey: computed('id', 'isCreating', { + parentKey: computed('id', 'path', 'isCreating', { get: function() { - return this.get('isCreating') ? this.get('initialParentKey') : utils.parentKeyForKey(this.get('id')); + return this.isCreating ? this.initialParentKey : utils.parentKeyForKey(this.pathVal()); }, set: function(_, value) { return value; }, }), - keyWithoutParent: computed('id', 'parentKey', { + keyWithoutParent: computed('id', 'path', 'parentKey', { get: function() { - var key = this.get('id'); - return key ? key.replace(this.get('parentKey'), '') : null; + var key = this.pathVal(); + return key ? key.replace(this.parentKey, '') : null; }, set: function(_, value) { if (value && value.trim()) { - this.set('id', this.get('parentKey') + value); + this.set(this.pathAttr, this.parentKey + value); } else { - this.set('id', null); + this.set(this.pathAttr, null); } return value; }, diff --git a/ui/app/models/secret-v2-version.js b/ui/app/models/secret-v2-version.js index 3c7ca467ac..19665fc063 100644 --- a/ui/app/models/secret-v2-version.js +++ b/ui/app/models/secret-v2-version.js @@ -1,8 +1,12 @@ import Secret from './secret'; import DS from 'ember-data'; -const { attr } = DS; +const { attr, belongsTo } = DS; export default Secret.extend({ + pathAttr: 'path', version: attr('number'), + secret: belongsTo('secret-v2'), + path: attr('string'), + currentVersion: attr('number'), }); diff --git a/ui/app/models/secret-v2.js b/ui/app/models/secret-v2.js index cfaef24fd2..2f2924a267 100644 --- a/ui/app/models/secret-v2.js +++ b/ui/app/models/secret-v2.js @@ -1,15 +1,17 @@ -import Secret from './secret'; import DS from 'ember-data'; +import { match } from '@ember/object/computed'; const { attr, hasMany, belongsTo, Model } = DS; export default Model.extend({ engine: belongsTo('secret-engine'), - versions: hasMany('secret-v2-version', { async: false }), + versions: hasMany('secret-v2-version', { async: false, inverse: null }), + selectedVersion: belongsTo('secret-v2-version', { inverse: 'secret' }), createdTime: attr(), updatedTime: attr(), currentVersion: attr('number'), oldestVersion: attr('number'), maxVersions: attr('number'), casRequired: attr('boolean'), + isFolder: match('id', /\/$/), }); diff --git a/ui/app/serializers/secret-v2-version.js b/ui/app/serializers/secret-v2-version.js index 31ffc9c902..68b3354b0c 100644 --- a/ui/app/serializers/secret-v2-version.js +++ b/ui/app/serializers/secret-v2-version.js @@ -3,23 +3,28 @@ import ApplicationSerializer from './application'; export default ApplicationSerializer.extend({ secretDataPath: 'data.data', - normalizeItems(payload, requestType) { + normalizeItems(payload) { let path = this.secretDataPath; // move response that is the contents of the secret from the dataPath // to `secret_data` so it will be `secretData` in the model payload.secret_data = get(payload, path); payload = Object.assign({}, payload, payload.data.metadata); delete payload.data; + payload.path = payload.id; // return the payload if it's expecting a single object or wrap // it as an array if not return payload; }, serialize(snapshot) { - return { + let data = { data: snapshot.attr('secretData'), - options: { - cas: snapshot.attr('currentVerion'), - }, }; + if (snapshot.attr('currentVersion')) { + data.options = { + cas: snapshot.attr('currentVerion'), + }; + } + + return data; }, }); diff --git a/ui/app/serializers/secret-v2.js b/ui/app/serializers/secret-v2.js index 4dbbfd305d..8afabceed5 100644 --- a/ui/app/serializers/secret-v2.js +++ b/ui/app/serializers/secret-v2.js @@ -25,14 +25,15 @@ export default ApplicationSerializer.extend(DS.EmbeddedRecordsMixin, { return { id: fullSecretPath }; }); } + // transform versions to an array with composite IDs if (payload.data.versions) { payload.data.versions = Object.keys(payload.data.versions).map(version => { let body = payload.data.versions[version]; body.version = version; + body.path = payload.id; body.id = JSON.stringify([payload.backend, payload.id, version]); return body; }); - console.log(payload); } payload.data.id = payload.id; return requestType === 'queryRecord' ? payload.data : [payload.data]; diff --git a/ui/tests/unit/adapters/secret-v2-test.js b/ui/tests/unit/adapters/secret-v2-test.js index 3ddd45c23c..9502c4c7d8 100644 --- a/ui/tests/unit/adapters/secret-v2-test.js +++ b/ui/tests/unit/adapters/secret-v2-test.js @@ -1,26 +1,70 @@ -import { resolve } from 'rsvp'; import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; +import apiStub from 'vault/tests/helpers/noop-all-api-requests'; module('Unit | Adapter | secret-v2', function(hooks) { setupTest(hooks); - test('secret api urls', function(assert) { - let url, method, options; - let adapter = this.owner.factoryFor('adapter:secret-v2').create({ - ajax: (...args) => { - [url, method, options] = args; - return resolve({}); + hooks.beforeEach(function() { + this.server = apiStub(); + }); + + hooks.afterEach(function() { + this.server.shutdown(); + }); + + [ + ['query', null, {}, { id: '', backend: 'secret' }, 'GET', '/v1/secret/metadata/?list=true'], + ['queryRecord', null, {}, { id: 'foo', backend: 'secret' }, 'GET', '/v1/secret/metadata/foo'], + [ + 'updateRecord', + { + serializerFor() { + return { + serializeIntoHash() {}, + }; + }, }, + {}, + { + id: 'foo', + belongsTo() { + return 'secret'; + }, + }, + 'PUT', + '/v1/secret/metadata/foo', + ], + [ + 'deleteRecord', + { + serializerFor() { + return { + serializeIntoHash() {}, + }; + }, + }, + {}, + { + id: 'foo', + belongsTo() { + return 'secret'; + }, + }, + 'DELETE', + '/v1/secret/metadata/foo', + ], + ].forEach(([adapterMethod, store, type, queryOrSnapshot, expectedHttpVerb, expectedURL]) => { + test(`secret-v2: ${adapterMethod}`, function(assert) { + let adapter = this.owner.lookup('adapter:secret-v2'); + adapter[adapterMethod](store, type, queryOrSnapshot); + let { url, method } = this.server.handledRequests[0]; + assert.equal(url, expectedURL, `${adapterMethod} calls the correct url: ${expectedURL}`); + assert.equal( + method, + expectedHttpVerb, + `${adapterMethod} uses the correct http verb: ${expectedHttpVerb}` + ); }); - - adapter.query({}, 'secret', { id: '', backend: 'secret' }); - assert.equal(url, '/v1/secret/metadata/', 'query generic url OK'); - assert.equal('GET', method, 'query generic method OK'); - assert.deepEqual(options, { data: { list: true } }, 'query generic url OK'); - - adapter.queryRecord({}, 'secret', { id: 'foo', backend: 'secret' }); - assert.equal(url, '/v1/secret/data/foo', 'queryRecord generic url OK'); - assert.equal('GET', method, 'queryRecord generic method OK'); }); }); diff --git a/ui/tests/unit/adapters/secret-v2-version-test.js b/ui/tests/unit/adapters/secret-v2-version-test.js new file mode 100644 index 0000000000..bb81074dd3 --- /dev/null +++ b/ui/tests/unit/adapters/secret-v2-version-test.js @@ -0,0 +1,79 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import apiStub from 'vault/tests/helpers/noop-all-api-requests'; + +module('Unit | Adapter | secret-v2-version', function(hooks) { + setupTest(hooks); + + hooks.beforeEach(function() { + this.server = apiStub(); + }); + + hooks.afterEach(function() { + this.server.shutdown(); + }); + + [ + [ + 'findRecord with version', + 'findRecord', + [null, {}, JSON.stringify(['secret', 'foo', '2']), {}], + 'GET', + '/v1/secret/data/foo?version=2', + ], + [ + 'deleteRecord with delete', + 'deleteRecord', + [null, {}, { id: JSON.stringify(['secret', 'foo', '2']), adapterOptions: { deleteType: 'delete' } }], + 'POST', + '/v1/secret/delete/foo', + { versions: ['2'] }, + ], + [ + 'deleteRecord with destroy', + 'deleteRecord', + [null, {}, { id: JSON.stringify(['secret', 'foo', '2']), adapterOptions: { deleteType: 'destroy' } }], + 'POST', + '/v1/secret/destroy/foo', + { versions: ['2'] }, + ], + [ + 'deleteRecord with destroy', + 'deleteRecord', + [null, {}, { id: JSON.stringify(['secret', 'foo', '2']), adapterOptions: { deleteType: 'undelete' } }], + 'POST', + '/v1/secret/undelete/foo', + { versions: ['2'] }, + ], + [ + 'updateRecord makes calls to correct url', + 'updateRecord', + [ + { + serializerFor() { + return { serializeIntoHash() {} }; + }, + }, + {}, + { id: JSON.stringify(['secret', 'foo', '2']) }, + ], + 'PUT', + '/v1/secret/data/foo', + ], + ].forEach(([testName, adapterMethod, args, expectedHttpVerb, expectedURL, exptectedRequestBody]) => { + test(`secret-v2: ${testName}`, function(assert) { + let adapter = this.owner.lookup('adapter:secret-v2-version'); + adapter[adapterMethod](...args); + let { url, method, requestBody } = this.server.handledRequests[0]; + assert.equal(url, expectedURL, `${adapterMethod} calls the correct url: ${expectedURL}`); + assert.equal( + method, + expectedHttpVerb, + `${adapterMethod} uses the correct http verb: ${expectedHttpVerb}` + ); + if (exptectedRequestBody) { + assert.deepEqual(JSON.parse(requestBody), exptectedRequestBody); + } + }); + }); +}); From 33d1f32d510d611e330d135823c1ede3e09d7b9f Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Fri, 5 Oct 2018 22:04:48 -0500 Subject: [PATCH 18/50] move secret creation to always be at the root level, and simplify model creation so we're always directly manipulating a model --- .../cluster/secrets/backend/create-root.js | 46 +++++++++++++- .../vault/cluster/secrets/backend/create.js | 61 ++----------------- .../components/secret-list-header.hbs | 4 +- 3 files changed, 53 insertions(+), 58 deletions(-) diff --git a/ui/app/routes/vault/cluster/secrets/backend/create-root.js b/ui/app/routes/vault/cluster/secrets/backend/create-root.js index 28d5c8ccc5..a8aa145f87 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/create-root.js +++ b/ui/app/routes/vault/cluster/secrets/backend/create-root.js @@ -1 +1,45 @@ -export { default } from './create'; +import { hash } from 'rsvp'; +import { inject as service } from '@ember/service'; +import EditBase from './secret-edit'; + +let secretModel = (store, backend, key) => { + let backendModel = store.peekRecord('secret-engine', backend); + let modelType = backendModel.get('modelTypeForKV'); + if (modelType !== 'secret-v2') { + return store.createRecord(modelType, { + id: key, + }); + } + let secret = store.createRecord(modelType); + secret.set('engine', backendModel); + let version = store.createRecord('secret-v2-version', { + path: key, + }); + secret.set('selectedVersion', version); + return secret; +}; + +export default EditBase.extend({ + wizard: service(), + createModel(transition) { + const { backend } = this.paramsFor('vault.cluster.secrets.backend'); + const modelType = this.modelType(backend); + if (modelType === 'role-ssh') { + return this.store.createRecord(modelType, { keyType: 'ca' }); + } + if (modelType !== 'secret' && modelType !== 'secret-v2') { + if (this.get('wizard.featureState') === 'details' && this.get('wizard.componentState') === 'transit') { + this.get('wizard').transitionFeatureMachine('details', 'CONTINUE', 'transit'); + } + return this.store.createRecord(modelType); + } + return secretModel(this.store, backend, transition.queryParams.initialKey); + }, + + model(params, transition) { + return hash({ + secret: this.createModel(transition), + capabilities: {}, + }); + }, +}); diff --git a/ui/app/routes/vault/cluster/secrets/backend/create.js b/ui/app/routes/vault/cluster/secrets/backend/create.js index 9f9a188cf1..63d6f304bb 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/create.js +++ b/ui/app/routes/vault/cluster/secrets/backend/create.js @@ -1,59 +1,10 @@ -import { hash } from 'rsvp'; -import { inject as service } from '@ember/service'; -import EmberObject from '@ember/object'; -import EditBase from './secret-edit'; -import KeyMixin from 'vault/models/key-mixin'; +import Route from '@ember/routing/route'; -var SecretProxy = EmberObject.extend(KeyMixin, { - store: null, - - toModel() { - return this.getProperties('id', 'secretData', 'backend'); - }, - - createRecord(backend) { - let backendModel = this.store.peekRecord('secret-engine', backend); - return this.store.createRecord(backendModel.get('modelTypeForKV'), this.toModel()); - }, - - willDestroy() { - this.store = null; - }, -}); - -export default EditBase.extend({ - wizard: service(), - createModel(transition, parentKey) { - const { backend } = this.paramsFor('vault.cluster.secrets.backend'); - const modelType = this.modelType(backend); - if (modelType === 'role-ssh') { - return this.store.createRecord(modelType, { keyType: 'ca' }); - } - if (modelType !== 'secret' && modelType !== 'secret-v2') { - if (this.get('wizard.featureState') === 'details' && this.get('wizard.componentState') === 'transit') { - this.get('wizard').transitionFeatureMachine('details', 'CONTINUE', 'transit'); - } - return this.store.createRecord(modelType); - } - const key = transition.queryParams.initialKey || ''; - const model = SecretProxy.create({ - initialParentKey: parentKey, - store: this.store, - }); - - if (key) { - // have to set this after so that it will be - // computed properly in the template (it's dependent on `initialParentKey`) - model.set('keyWithoutParent', key); - } - return model; - }, - - model(params, transition) { - const parentKey = params.secret ? params.secret : ''; - return hash({ - secret: this.createModel(transition, parentKey), - capabilities: {}, +export default Route.extend({ + beforeModel(transition) { + let { secret } = this.paramsFor(this.routeName); + return this.transitionTo('vault.cluster.secrets.backend.create-root', { + queryParams: { initialKey: secret }, }); }, }); diff --git a/ui/app/templates/components/secret-list-header.hbs b/ui/app/templates/components/secret-list-header.hbs index ffb52a961f..4ae8d4d815 100644 --- a/ui/app/templates/components/secret-list-header.hbs +++ b/ui/app/templates/components/secret-list-header.hbs @@ -32,8 +32,8 @@
{{#secret-link mode="create" - secret=(or baseKey.id '') - queryParams=(query-params initialKey='') + secret='' + queryParams=(query-params initialKey=baseKey.id) class="button has-icon-right is-ghost is-compact" data-test-secret-create=true }} From 4dce16776f177357496b3c26202c5b10291c8095 Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Fri, 5 Oct 2018 22:05:53 -0500 Subject: [PATCH 19/50] adjust secret-edit component and associated templates to work for v1 and v2 --- ui/app/components/secret-edit.js | 139 ++++++++---------- .../cluster/secrets/backend/secret-edit.js | 7 +- .../partials/secret-edit-display.hbs | 4 +- .../templates/partials/secret-form-create.hbs | 32 +--- .../templates/partials/secret-form-show.hbs | 4 +- 5 files changed, 79 insertions(+), 107 deletions(-) diff --git a/ui/app/components/secret-edit.js b/ui/app/components/secret-edit.js index cfa3b5d653..cdd2ad04b0 100644 --- a/ui/app/components/secret-edit.js +++ b/ui/app/components/secret-edit.js @@ -1,9 +1,9 @@ import { or } from '@ember/object/computed'; import { isBlank, isNone } from '@ember/utils'; -import $ from 'jquery'; import { inject as service } from '@ember/service'; import Component from '@ember/component'; import { computed, get } from '@ember/object'; +import { task, waitForEvent } from 'ember-concurrency'; import FocusOnInsertMixin from 'vault/mixins/focus-on-insert'; import keys from 'vault/lib/keycodes'; import KVObject from 'vault/lib/kv-object'; @@ -18,6 +18,7 @@ export default Component.extend(FocusOnInsertMixin, { // a key model key: null, + model: null, // a value to pre-fill the key input - this is populated by the corresponding // 'initialKey' queryParam @@ -44,10 +45,15 @@ export default Component.extend(FocusOnInsertMixin, { codemirrorString: null, hasLintError: false, + isV2: false, init() { this._super(...arguments); - const secrets = this.get('key.secretData'); + let secrets = this.model.secretData; + if (!secrets && this.model.selectedVersion) { + this.set('isV2', true); + secrets = this.model.belongsTo('selectedVersion').value().secretData; + } const data = KVObject.create({ content: [] }).fromJSON(secrets); this.set('secretData', data); this.set('codemirrorString', data.toJSONString()); @@ -56,81 +62,75 @@ export default Component.extend(FocusOnInsertMixin, { } this.checkRows(); if (this.get('wizard.featureState') === 'details' && this.get('mode') === 'create') { - let engine = this.get('key').backend.includes('kv') ? 'kv' : this.get('key').backend; + let engine = this.get('model').backend.includes('kv') ? 'kv' : this.get('model').backend; this.get('wizard').transitionFeatureMachine('details', 'CONTINUE', engine); } - if (this.get('mode') === 'edit') { + if (this.mode === 'edit') { this.send('addRow'); } }, - didInsertElement() { - this._super(...arguments); - $(document).on('keyup.keyEdit', this.onEscape.bind(this)); - }, - willDestroyElement() { this._super(...arguments); - const key = this.get('key'); - if (get(key, 'isError') && !key.isDestroyed) { - key.rollbackAttributes(); + if (this.model.isError && !this.model.isDestroyed) { + model.rollbackAttributes(); } - $(document).off('keyup.keyEdit'); }, + waitForKeyUp: task(function*() { + while (true) { + let event = yield waitForEvent(document.body, 'keyup'); + this.onEscape(event); + } + }) + .on('didInsertElement') + .cancelOn('willDestroyElement'), + partialName: computed('mode', function() { return `partials/secret-form-${this.get('mode')}`; }), - showPrefix: or('key.initialParentKey', 'key.parentKey'), - - requestInFlight: or('key.isLoading', 'key.isReloading', 'key.isSaving'), + requestInFlight: or('model.isLoading', 'model.isReloading', 'model.isSaving'), buttonDisabled: or( 'requestInFlight', - 'key.isFolder', - 'key.isError', - 'key.flagsIsInvalid', + 'model.isFolder', + 'model.isError', + 'model.flagsIsInvalid', 'hasLintError', 'error' ), + modelForData: computed('isV2', 'model', function() { + return this.isV2 ? this.model.belongsTo('selectedVersion').value() : this.model; + }), + basicModeDisabled: computed('secretDataIsAdvanced', 'showAdvancedMode', function() { - return this.get('secretDataIsAdvanced') || this.get('showAdvancedMode') === false; + return this.secretDataIsAdvanced || this.showAdvancedMode === false; }), secretDataAsJSON: computed('secretData', 'secretData.[]', function() { - return this.get('secretData').toJSON(); + return this.secretData.toJSON(); }), secretDataIsAdvanced: computed('secretData', 'secretData.[]', function() { - return this.get('secretData').isAdvanced(); + return this.secretData.isAdvanced(); }), - hasDataChanges() { - const keyDataString = this.get('key.dataAsJSONString'); - const sameData = this.get('secretData').toJSONString() === keyDataString; - if (sameData === false) { - this.set('lastChange', Date.now()); - } - - this.get('onDataChange')(!sameData); - }, - showAdvancedMode: computed('preferAdvancedEdit', 'secretDataIsAdvanced', 'lastChange', function() { - return this.get('secretDataIsAdvanced') || this.get('preferAdvancedEdit'); + return this.secretDataIsAdvanced || this.preferAdvancedEdit; }), transitionToRoute() { - this.get('router').transitionTo(...arguments); + this.router.transitionTo(...arguments); }, onEscape(e) { - if (e.keyCode !== keys.ESC || this.get('mode') !== 'show') { + if (e.keyCode !== keys.ESC || this.mode !== 'show') { return; } - const parentKey = this.get('key.parentKey'); + const parentKey = this.model.parentKey; if (parentKey) { this.transitionToRoute(LIST_ROUTE, parentKey); } else { @@ -139,25 +139,19 @@ export default Component.extend(FocusOnInsertMixin, { }, // successCallback is called in the context of the component - persistKey(method, successCallback, isCreate) { - let model = this.get('key'); - let key = model.get('id'); + persistKey(successCallback) { + let model = this.modelForData; + let key = model.get('path') || model.id; if (key.startsWith('/')) { key = key.replace(/^\/+/g, ''); - model.set('id', key); + model.set(model.pathAttr, key); } - if (isCreate && typeof model.createRecord === 'function') { - // create an ember data model from the proxy - model = model.createRecord(model.get('backend')); - this.set('key', model); - } - - return model[method]().then(() => { - if (!get(model, 'isError')) { - if (this.get('wizard.featureState') === 'secret') { - this.get('wizard').transitionFeatureMachine('secret', 'CONTINUE'); + return model.save().then(() => { + if (!model.isError) { + if (this.wizard.featureState === 'secret') { + this.wizard.transitionFeatureMachine('secret', 'CONTINUE'); } successCallback(key); } @@ -165,85 +159,78 @@ export default Component.extend(FocusOnInsertMixin, { }, checkRows() { - if (this.get('secretData').get('length') === 0) { + if (this.secretData.length === 0) { this.send('addRow'); } }, actions: { + //submit on shift + enter handleKeyDown(e) { e.stopPropagation(); if (!(e.keyCode === keys.ENTER && e.metaKey)) { return; } - let $form = this.$('form'); + let $form = this.element.querySelector('form'); + console.log('form is: ', $form); if ($form.length) { $form.submit(); } - $form = null; }, handleChange() { - this.set('codemirrorString', this.get('secretData').toJSONString(true)); - this.hasDataChanges(); + this.set('codemirrorString', this.secretData.toJSONString(true)); }, createOrUpdateKey(type, event) { event.preventDefault(); - const newData = this.get('secretData').toJSON(); - this.get('key').set('secretData', newData); + const newData = this.secretData.toJSON(); + let model = this.modelForData; + model.set('secretData', newData); // prevent from submitting if there's no key // maybe do something fancier later - if (type === 'create' && isBlank(this.get('key.id'))) { + if (type === 'create' && isBlank(model.get('path') || model.id)) { return; } - this.persistKey( - 'save', - key => { - this.hasDataChanges(); - this.transitionToRoute(SHOW_ROUTE, key); - }, - type === 'create' - ); + this.persistKey(key => { + this.transitionToRoute(SHOW_ROUTE, key); + }); }, deleteKey() { - this.persistKey('destroyRecord', () => { + this.model.destroyRecord().then(() => { this.transitionToRoute(LIST_ROOT_ROUTE); }); }, refresh() { - this.get('onRefresh')(); + this.onRefresh(); }, addRow() { - const data = this.get('secretData'); + const data = this.secretData; if (isNone(data.findBy('name', ''))) { data.pushObject({ name: '', value: '' }); this.set('codemirrorString', data.toJSONString(true)); } this.checkRows(); - this.hasDataChanges(); }, deleteRow(name) { - const data = this.get('secretData'); + const data = this.secretData; const item = data.findBy('name', name); if (isBlank(item.name)) { return; } data.removeObject(item); this.checkRows(); - this.hasDataChanges(); this.set('codemirrorString', data.toJSONString(true)); - this.rerender(); }, toggleAdvanced(bool) { - this.get('onToggleAdvancedEdit')(bool); + this.onToggleAdvancedEdit(bool); }, codemirrorUpdated(val, codemirror) { @@ -252,7 +239,7 @@ export default Component.extend(FocusOnInsertMixin, { const noErrors = codemirror.state.lint.marked.length === 0; if (noErrors) { try { - this.get('secretData').fromJSONString(val); + this.secretData.fromJSONString(val); } catch (e) { this.set('error', e.message); } @@ -262,7 +249,7 @@ export default Component.extend(FocusOnInsertMixin, { }, formatJSON() { - this.set('codemirrorString', this.get('secretData').toJSONString(true)); + this.set('codemirrorString', this.secretData.toJSONString(true)); }, }, }); 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 67aec2bf6c..8262211565 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js +++ b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js @@ -76,7 +76,10 @@ export default Route.extend(UnloadModelRoute, { if (modelType === 'secret-v2') { // TODO, find by query param to enable viewing versions let version = resp.versions.findBy('version', resp.currentVersion); - version.reload(); + return version.reload().then(() => { + resp.set('selectedVersion', version); + return resp; + }); } return resp; }), @@ -127,6 +130,8 @@ export default Route.extend(UnloadModelRoute, { }, willTransition(transition) { + console.log(this.hasChanges); + console.log(this.controller.model.hasDirtyAttributes); if (this.get('hasChanges')) { if ( window.confirm( diff --git a/ui/app/templates/partials/secret-edit-display.hbs b/ui/app/templates/partials/secret-edit-display.hbs index 0bb384a0f6..aad4f67161 100644 --- a/ui/app/templates/partials/secret-edit-display.hbs +++ b/ui/app/templates/partials/secret-edit-display.hbs @@ -1,4 +1,3 @@ -{{#unless key.isFolder}} {{#if showAdvancedMode}} {{json-editor value=codemirrorString @@ -44,5 +43,4 @@
{{/each}} - {{/if}} -{{/unless}} + {{/if}} \ No newline at end of file diff --git a/ui/app/templates/partials/secret-form-create.hbs b/ui/app/templates/partials/secret-form-create.hbs index 852e1d6832..1eef75afad 100644 --- a/ui/app/templates/partials/secret-form-create.hbs +++ b/ui/app/templates/partials/secret-form-create.hbs @@ -1,30 +1,12 @@ -
+
- {{message-error model=key errorMessage=error}} + -
- {{#if (not-eq key.initialParentKey '') }} - {{! need this to prevent a shift in the layout before we transition when saving }} - {{#if key.isCreating}} -

- -

- {{else}} -

- -

- {{/if}} - {{/if}} -

- {{input data-test-secret-path=true id="kv-key" class="input" value=key.keyWithoutParent}} -

-
- {{#if key.isFolder}} +

+ {{input data-test-secret-path=true id="kv-key" class="input" value=(get modelForData modelForData.pathAttr)}} +

+ {{#if modelForData.isFolder}}

The secret path may not end in /

@@ -47,7 +29,7 @@
{{#secret-link mode="list" - secret=key.initialParentKey + secret=model.parentKey class="button" }} Cancel diff --git a/ui/app/templates/partials/secret-form-show.hbs b/ui/app/templates/partials/secret-form-show.hbs index de0ac2853f..91c56abaa9 100644 --- a/ui/app/templates/partials/secret-form-show.hbs +++ b/ui/app/templates/partials/secret-form-show.hbs @@ -1,6 +1,6 @@ {{#if showAdvancedMode}} {{json-editor - value=key.dataAsJSONString + value=modelForData.dataAsJSONString options=(hash readOnly=true ) @@ -16,7 +16,7 @@
- {{#each-in key.secretData as |key value|}} + {{#each-in modelForData.secretData as |key value|}} {{#info-table-row label=key value=value alwaysRender=true}} {{masked-input value=value displayOnly=true}} {{/info-table-row}} From 564942dcc1d2650942c217270c79f527a5585149 Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Mon, 8 Oct 2018 10:07:35 -0500 Subject: [PATCH 20/50] fix linting --- ui/app/components/navigate-input.js | 1 - ui/app/components/secret-edit.js | 5 ++--- ui/app/routes/vault/cluster/secrets/backend/create.js | 2 +- ui/app/routes/vault/cluster/secrets/backend/secret-edit.js | 2 -- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/ui/app/components/navigate-input.js b/ui/app/components/navigate-input.js index cf774ecf98..300a9afebd 100644 --- a/ui/app/components/navigate-input.js +++ b/ui/app/components/navigate-input.js @@ -170,7 +170,6 @@ export default Component.extend(FocusOnInsertMixin, { setFilterFocused: function(isFocused) { this.get('filterFocusDidChange')(isFocused); - console.log(isFocused); }, handleKeyPress: function(event) { diff --git a/ui/app/components/secret-edit.js b/ui/app/components/secret-edit.js index cdd2ad04b0..19c2afc37e 100644 --- a/ui/app/components/secret-edit.js +++ b/ui/app/components/secret-edit.js @@ -2,7 +2,7 @@ import { or } from '@ember/object/computed'; import { isBlank, isNone } from '@ember/utils'; import { inject as service } from '@ember/service'; import Component from '@ember/component'; -import { computed, get } from '@ember/object'; +import { computed } from '@ember/object'; import { task, waitForEvent } from 'ember-concurrency'; import FocusOnInsertMixin from 'vault/mixins/focus-on-insert'; import keys from 'vault/lib/keycodes'; @@ -74,7 +74,7 @@ export default Component.extend(FocusOnInsertMixin, { willDestroyElement() { this._super(...arguments); if (this.model.isError && !this.model.isDestroyed) { - model.rollbackAttributes(); + this.model.rollbackAttributes(); } }, @@ -172,7 +172,6 @@ export default Component.extend(FocusOnInsertMixin, { return; } let $form = this.element.querySelector('form'); - console.log('form is: ', $form); if ($form.length) { $form.submit(); } diff --git a/ui/app/routes/vault/cluster/secrets/backend/create.js b/ui/app/routes/vault/cluster/secrets/backend/create.js index 63d6f304bb..e60011aea0 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/create.js +++ b/ui/app/routes/vault/cluster/secrets/backend/create.js @@ -1,7 +1,7 @@ import Route from '@ember/routing/route'; export default Route.extend({ - beforeModel(transition) { + beforeModel() { let { secret } = this.paramsFor(this.routeName); return this.transitionTo('vault.cluster.secrets.backend.create-root', { queryParams: { initialKey: secret }, 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 8262211565..74ce7aaf7f 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js +++ b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js @@ -130,8 +130,6 @@ export default Route.extend(UnloadModelRoute, { }, willTransition(transition) { - console.log(this.hasChanges); - console.log(this.controller.model.hasDirtyAttributes); if (this.get('hasChanges')) { if ( window.confirm( From 988e0174ba393c660e6e940830d7cf37a91795c0 Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Mon, 8 Oct 2018 13:20:55 -0500 Subject: [PATCH 21/50] less get in secret-edit --- ui/app/components/secret-edit.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/app/components/secret-edit.js b/ui/app/components/secret-edit.js index 19c2afc37e..a7d5de551c 100644 --- a/ui/app/components/secret-edit.js +++ b/ui/app/components/secret-edit.js @@ -61,9 +61,9 @@ export default Component.extend(FocusOnInsertMixin, { this.set('preferAdvancedEdit', true); } this.checkRows(); - if (this.get('wizard.featureState') === 'details' && this.get('mode') === 'create') { - let engine = this.get('model').backend.includes('kv') ? 'kv' : this.get('model').backend; - this.get('wizard').transitionFeatureMachine('details', 'CONTINUE', engine); + if (this.wizard.featureState === 'details' && this.mode === 'create') { + let engine = this.model.backend.includes('kv') ? 'kv' : this.model.backend; + this.wizard.transitionFeatureMachine('details', 'CONTINUE', engine); } if (this.mode === 'edit') { @@ -88,7 +88,7 @@ export default Component.extend(FocusOnInsertMixin, { .cancelOn('willDestroyElement'), partialName: computed('mode', function() { - return `partials/secret-form-${this.get('mode')}`; + return `partials/secret-form-${this.mode}`; }), requestInFlight: or('model.isLoading', 'model.isReloading', 'model.isSaving'), From 8df0b9942e645a49f0b354d639161a104ab820ee Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Mon, 8 Oct 2018 23:19:58 -0500 Subject: [PATCH 22/50] blue toggle switch --- ui/app/styles/core/switch.scss | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/ui/app/styles/core/switch.scss b/ui/app/styles/core/switch.scss index 12e30d4a87..08949f9122 100644 --- a/ui/app/styles/core/switch.scss +++ b/ui/app/styles/core/switch.scss @@ -1,27 +1,35 @@ .switch[type="checkbox"] { &.is-small { + label { - font-size: $size-9; + font-size: $size-8; font-weight: bold; - padding-left: $size-9 * 2.5; + padding-left: $size-8 * 2.5; margin: 0 0.25rem; &::before { - top: $size-9 / 5; - height: $size-9; - width: $size-9 * 2; + top: $size-8 / 5; + height: $size-8; + width: $size-8 * 2; } &::after { - width: $size-9 * 0.68; - height: $size-9 * 0.68; - left: .15rem; - top: $size-9 / 2.5; + left: 0; + width: $size-8 * 0.8; + height: $size-8 * 0.8; + transform: translateX(0.15rem); + top: $size-8/ 4; } } &:checked + label::after { - left: ($size-9 * 2) - ($size-9 * 0.9); + left: 0; + transform: translateX(($size-8 * 2) - ($size-8 * 0.9)); } } } +.switch[type="checkbox"].is-small + label::after { + will-change: transform; +} .switch[type="checkbox"]:focus + label { box-shadow: 0 0 1px $blue; } +.switch[type="checkbox"].is-success:checked + label::before { + background: $blue; +} From d057ba1a5d6f5a457ddcbcd8b341d2114c823979 Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Mon, 8 Oct 2018 23:21:02 -0500 Subject: [PATCH 23/50] move capabilities into the secret-edit component --- ui/app/adapters/capabilities.js | 3 +++ ui/app/components/secret-edit.js | 24 +++++++++++++++++++ .../cluster/secrets/backend/secret-edit.js | 10 ++++---- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/ui/app/adapters/capabilities.js b/ui/app/adapters/capabilities.js index f966cb5441..ad04c10368 100644 --- a/ui/app/adapters/capabilities.js +++ b/ui/app/adapters/capabilities.js @@ -18,6 +18,9 @@ export default ApplicationAdapter.extend({ queryRecord(store, type, query) { const { id } = query; + if (!id) { + return; + } return this.findRecord(store, type, id).then(resp => { resp.path = id; return resp; diff --git a/ui/app/components/secret-edit.js b/ui/app/components/secret-edit.js index a7d5de551c..d4b4317639 100644 --- a/ui/app/components/secret-edit.js +++ b/ui/app/components/secret-edit.js @@ -3,10 +3,12 @@ import { isBlank, isNone } from '@ember/utils'; import { inject as service } from '@ember/service'; import Component from '@ember/component'; import { computed } from '@ember/object'; +import { alias } from '@ember/object/computed'; import { task, waitForEvent } from 'ember-concurrency'; import FocusOnInsertMixin from 'vault/mixins/focus-on-insert'; import keys from 'vault/lib/keycodes'; import KVObject from 'vault/lib/kv-object'; +import { queryRecord } from 'ember-computed-query'; const LIST_ROUTE = 'vault.cluster.secrets.backend.list'; const LIST_ROOT_ROUTE = 'vault.cluster.secrets.backend.list-root'; @@ -15,6 +17,7 @@ const SHOW_ROUTE = 'vault.cluster.secrets.backend.show'; export default Component.extend(FocusOnInsertMixin, { wizard: service(), router: service(), + store: service(), // a key model key: null, @@ -91,6 +94,27 @@ export default Component.extend(FocusOnInsertMixin, { return `partials/secret-form-${this.mode}`; }), + updatePath: queryRecord( + 'capabilities', + context => { + if (context.mode === 'create') { + return {}; + } + let backend = context.isV2 ? context.model.belongsTo('engine').id : context.model.backend; + let id = context.model.id; + let path = context.isV2 ? `${backend}/data/${id}` : `${backend}/${id}`; + return { + id: path, + }; + }, + 'isV2', + 'model', + 'model.id', + 'mode' + ), + canDelete: alias('updatePath.canDelete'), + canEdit: alias('updatePath.canUpdate'), + requestInFlight: or('model.isLoading', 'model.isReloading', 'model.isSaving'), buttonDisabled: or( 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 74ce7aaf7f..00909aff1c 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js +++ b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js @@ -1,5 +1,5 @@ import { set } from '@ember/object'; -import { hash } from 'rsvp'; +import { hash, resolve } from 'rsvp'; import Route from '@ember/routing/route'; import utils from 'vault/lib/key-utils'; import UnloadModelRoute from 'vault/mixins/unload-model-route'; @@ -9,16 +9,14 @@ export default Route.extend(UnloadModelRoute, { const { backend } = this.paramsFor('vault.cluster.secrets.backend'); let backendModel = this.modelFor('vault.cluster.secrets.backend'); let backendType = backendModel.get('engineType'); - let version = backendModel.get('options.version'); + if (backendType === 'kv' || backendType === 'cubbyhole') { + return resolve({}); + } let path; if (backendType === 'transit') { path = backend + '/keys/' + secret; } else if (backendType === 'ssh' || backendType === 'aws') { path = backend + '/roles/' + secret; - } else if (version && version === 2) { - path = backend + '/data/' + secret; - } else { - path = backend + '/' + secret; } return this.store.findRecord('capabilities', path); }, From 69c8a122a66f4cce51f4e0aa9bfe8273031cbc0f Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Mon, 8 Oct 2018 23:22:29 -0500 Subject: [PATCH 24/50] use new local capabilities, move secret-edit-display partial to a component --- .../components/secret-edit-display.hbs | 51 ++++++++++++ ui/app/templates/components/secret-edit.hbs | 31 ++++--- .../partials/secret-edit-display.hbs | 46 ----------- .../templates/partials/secret-form-create.hbs | 16 +++- .../templates/partials/secret-form-edit.hbs | 80 ++++++------------- 5 files changed, 111 insertions(+), 113 deletions(-) create mode 100644 ui/app/templates/components/secret-edit-display.hbs delete mode 100644 ui/app/templates/partials/secret-edit-display.hbs diff --git a/ui/app/templates/components/secret-edit-display.hbs b/ui/app/templates/components/secret-edit-display.hbs new file mode 100644 index 0000000000..4065ce10b6 --- /dev/null +++ b/ui/app/templates/components/secret-edit-display.hbs @@ -0,0 +1,51 @@ + {{#if @showAdvancedMode}} + + {{else}} + {{#each @secretData as |secret index|}} +
+
+ {{input + data-test-secret-key=true + value=secret.name + placeholder="key" + change="handleChange" + class="input" + }} +
+
+ +
+
+ {{#if (eq @secretData.length (inc index))}} + + {{else}} + + {{/if}} +
+
+ {{/each}} + {{/if}} \ No newline at end of file diff --git a/ui/app/templates/components/secret-edit.hbs b/ui/app/templates/components/secret-edit.hbs index 218ba3b434..18e2f0f666 100644 --- a/ui/app/templates/components/secret-edit.hbs +++ b/ui/app/templates/components/secret-edit.hbs @@ -19,6 +19,19 @@ {{/if}} + + {{#if canDelete}} + {{#confirm-action + buttonClasses="button is-compact is-ghost has-icon-right" + onConfirmAction=(action "deleteKey") + confirmMessage=(concat "Are you sure you want to delete " model.id "?") + cancelButtonText="Cancel" + data-test-secret-delete="true" + }} + Delete secret + {{/confirm-action}} + {{/if}} +
@@ -39,17 +52,15 @@ />
- {{#if (and (not-eq mode 'create') (or capabilities.canUpdate capabilities.canDelete))}} + {{#if (and (eq mode 'show') canEdit)}}
- {{input - id="edit" - type="checkbox" - name="navToEdit" - class="switch is-rounded is-success is-small" - checked=(eq mode 'edit') - change=(action (nav-to-route (concat 'vault.cluster.secrets.backend.' (if (eq mode 'show') 'edit' 'show')) key.id replace=true) ) - }} - + + Edit Secret +
{{/if}}
diff --git a/ui/app/templates/partials/secret-edit-display.hbs b/ui/app/templates/partials/secret-edit-display.hbs deleted file mode 100644 index aad4f67161..0000000000 --- a/ui/app/templates/partials/secret-edit-display.hbs +++ /dev/null @@ -1,46 +0,0 @@ - {{#if showAdvancedMode}} - {{json-editor - value=codemirrorString - valueUpdated=(action "codemirrorUpdated") - onFocusOut=(action "formatJSON") - }} - {{else}} - {{#each secretData as |secret index|}} -
-
- {{input - data-test-secret-key=true - value=secret.name - placeholder="key" - change="handleChange" - class="input" - }} -
-
- {{masked-input - data-test-secret-value=true - name=secret.name - onKeyDown=(action "handleKeyDown") - onChange=(action "handleChange") - value=secret.value - }} -
-
- {{#if (eq secretData.length (inc index))}} - - {{else}} - - {{/if}} -
-
- {{/each}} - {{/if}} \ No newline at end of file diff --git a/ui/app/templates/partials/secret-form-create.hbs b/ui/app/templates/partials/secret-form-create.hbs index 1eef75afad..0b699251b9 100644 --- a/ui/app/templates/partials/secret-form-create.hbs +++ b/ui/app/templates/partials/secret-form-create.hbs @@ -12,9 +12,19 @@

{{/if}} - - {{partial "partials/secret-edit-display"}} - +
-
- {{/if}} - {{/unless}} +
+ +
{{#secret-link - mode=(if key.isFolder "list" "show") - secret=key.id + mode="show" + secret=model.id class="button" }} Cancel {{/secret-link}}
- {{#if capabilities.canDelete}} - {{#confirm-action - buttonClasses="button" - onConfirmAction=(action "deleteKey") - confirmMessage=(if key.isFolder - (concat "Are you sure you want to delete " key.id " and all its contents?") - (concat "Are you sure you want to delete " key.id "?") - ) - cancelButtonText="Cancel" - data-test-secret-delete="true" - }} - {{#if key.isFolder}} - Delete folder - {{else}} - Delete secret - {{/if}} - {{/confirm-action}} - {{/if}} From 15bf5667ce05007ce198b3cd621e397359304f35 Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Tue, 9 Oct 2018 23:35:33 -0500 Subject: [PATCH 25/50] add new icon --- ui/app/components/i-con.js | 1 + ui/app/templates/svg/icons/cancel-square-outline.hbs | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 ui/app/templates/svg/icons/cancel-square-outline.hbs diff --git a/ui/app/components/i-con.js b/ui/app/components/i-con.js index 4482fe1c84..78d182f10b 100644 --- a/ui/app/components/i-con.js +++ b/ui/app/components/i-con.js @@ -24,6 +24,7 @@ const GLYPHS_WITH_SVG_TAG = [ 'edition-oss', 'check-plain', 'check-circle-fill', + 'cancel-square-outline', ]; export default Component.extend({ diff --git a/ui/app/templates/svg/icons/cancel-square-outline.hbs b/ui/app/templates/svg/icons/cancel-square-outline.hbs new file mode 100644 index 0000000000..bdf294a645 --- /dev/null +++ b/ui/app/templates/svg/icons/cancel-square-outline.hbs @@ -0,0 +1,3 @@ + + + \ No newline at end of file From 76524273d9396b3f03824aeee5b03e6ac5ebe1f0 Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Tue, 9 Oct 2018 23:37:28 -0500 Subject: [PATCH 26/50] add block-error and block-empty components to centralize some bulma markup --- ui/app/components/block-empty.js | 2 ++ ui/app/components/block-error.js | 2 ++ ui/app/templates/components/block-empty.hbs | 11 ++++++ ui/app/templates/components/block-error.hbs | 3 ++ .../vault/cluster/secrets/backend/list.hbs | 34 +++++++------------ ui/app/templates/vault/error.hbs | 4 +-- 6 files changed, 33 insertions(+), 23 deletions(-) create mode 100644 ui/app/components/block-empty.js create mode 100644 ui/app/components/block-error.js create mode 100644 ui/app/templates/components/block-empty.hbs create mode 100644 ui/app/templates/components/block-error.hbs diff --git a/ui/app/components/block-empty.js b/ui/app/components/block-empty.js new file mode 100644 index 0000000000..96167992d7 --- /dev/null +++ b/ui/app/components/block-empty.js @@ -0,0 +1,2 @@ +import OuterHTML from './outer-html'; +export default OuterHTML.extend(); diff --git a/ui/app/components/block-error.js b/ui/app/components/block-error.js new file mode 100644 index 0000000000..96167992d7 --- /dev/null +++ b/ui/app/components/block-error.js @@ -0,0 +1,2 @@ +import OuterHTML from './outer-html'; +export default OuterHTML.extend(); diff --git a/ui/app/templates/components/block-empty.hbs b/ui/app/templates/components/block-empty.hbs new file mode 100644 index 0000000000..d0f7e402f7 --- /dev/null +++ b/ui/app/templates/components/block-empty.hbs @@ -0,0 +1,11 @@ +
+
+
+
+

+ {{yield}} +

+
+
+
+
\ No newline at end of file diff --git a/ui/app/templates/components/block-error.hbs b/ui/app/templates/components/block-error.hbs new file mode 100644 index 0000000000..c09a206dbc --- /dev/null +++ b/ui/app/templates/components/block-error.hbs @@ -0,0 +1,3 @@ +
+{{yield}} +
\ No newline at end of file diff --git a/ui/app/templates/vault/cluster/secrets/backend/list.hbs b/ui/app/templates/vault/cluster/secrets/backend/list.hbs index 1e12a73aeb..ba9076f178 100644 --- a/ui/app/templates/vault/cluster/secrets/backend/list.hbs +++ b/ui/app/templates/vault/cluster/secrets/backend/list.hbs @@ -46,27 +46,19 @@ {{/each}} {{else}} -
-
-
-
-

- {{#if (eq baseKey.id '')}} - There are currently no {{pluralize options.item}} in this backend. - {{else}} - {{#if filterIsFolder}} - {{#if (eq filter baseKey.id)}} - There are no {{pluralize options.item}} under {{or filter}}. - {{else}} - We couldn't find a folder matching {{filter}}. - {{/if}} - {{/if}} - {{/if}} -

-
-
-
-
+ + {{#if (eq baseKey.id '')}} + There are currently no {{pluralize options.item}} in this backend. + {{else}} + {{#if filterIsFolder}} + {{#if (eq filter baseKey.id)}} + There are no {{pluralize options.item}} under {{or filter}}. + {{else}} + We couldn't find a folder matching {{filter}}. + {{/if}} + {{/if}} + {{/if}} + {{/if}} {{/with}} {{#if (gt model.meta.lastPage 1) }} diff --git a/ui/app/templates/vault/error.hbs b/ui/app/templates/vault/error.hbs index ec43dfa417..587690d259 100644 --- a/ui/app/templates/vault/error.hbs +++ b/ui/app/templates/vault/error.hbs @@ -21,14 +21,14 @@ -
+ {{#if model.message}}

{{model.message}}

{{/if}} {{#each model.errors as |error|}}

{{error}}

{{/each}} -
+ {{/if}} From 66a4c261842022b4d264d8c0b6bddb6893a2949d Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Tue, 9 Oct 2018 23:40:13 -0500 Subject: [PATCH 27/50] extract list-header css component to match header from namespace-picker --- ui/app/styles/components/namespace-picker.scss | 7 ------- ui/app/styles/core/lists.scss | 10 +++++++++- ui/app/templates/components/namespace-picker.hbs | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/ui/app/styles/components/namespace-picker.scss b/ui/app/styles/components/namespace-picker.scss index 2dc07e2cfa..8e9ad3b47a 100644 --- a/ui/app/styles/components/namespace-picker.scss +++ b/ui/app/styles/components/namespace-picker.scss @@ -45,13 +45,6 @@ } } -.namespace-header { - margin: $size-9 $size-9 0; - color: $grey; - font-size: $size-8; - font-weight: $font-weight-semibold; - text-transform: uppercase; -} .current-namespace { border-bottom: 1px solid rgba($black, 0.1); } diff --git a/ui/app/styles/core/lists.scss b/ui/app/styles/core/lists.scss index 26e0da6a9f..bc9c6dacc5 100644 --- a/ui/app/styles/core/lists.scss +++ b/ui/app/styles/core/lists.scss @@ -4,7 +4,15 @@ &:before { font-size: $size-5; color: $white-ter; - content: '|'; + content: "|"; position: relative; } } + +.list-header { + margin: $size-9 $size-9 0; + color: $grey; + font-size: $size-8; + font-weight: $font-weight-semibold; + text-transform: uppercase; +} diff --git a/ui/app/templates/components/namespace-picker.hbs b/ui/app/templates/components/namespace-picker.hbs index 3402e98a7f..4d048cf917 100644 --- a/ui/app/templates/components/namespace-picker.hbs +++ b/ui/app/templates/components/namespace-picker.hbs @@ -41,7 +41,7 @@
-
Current namespace
+
Current namespace