2023-03-15 16:00:52 +00:00
/ * *
* Copyright ( c ) HashiCorp , Inc .
* SPDX - License - Identifier : MPL - 2.0
* /
2021-08-31 15:41:41 +00:00
/ * *
* @ module SecretCreateOrUpdate
* SecretCreateOrUpdate component displays either the form for creating a new secret or creating a new version of the secret
*
* @ example
* ` ` ` js
* < SecretCreateOrUpdate
* @ mode = "create"
* @ model = { { model } }
* @ showAdvancedMode = true
* @ modelForData = { { @ modelForData } }
* @ isV2 = true
* @ secretData = { { @ secretData } }
2021-09-29 20:35:00 +00:00
* @ canCreateSecretMetadata = false
2021-08-31 15:41:41 +00:00
* / >
* ` ` `
* @ param { string } mode - create , edit , show determines what view to display
* @ param { object } model - the route model , comes from secret - v2 ember record
* @ param { boolean } showAdvancedMode - whether or not to show the JSON editor
* @ param { object } modelForData - a class that helps track secret data , defined in secret - edit
* @ param { boolean } isV2 - whether or not KV1 or KV2
* @ param { object } secretData - class that is created in secret - edit
2021-09-29 20:35:00 +00:00
* @ param { boolean } canUpdateSecretMetadata - based on permissions to the / metadata / endpoint . If user has secret update . create is not enough for metadata .
2021-08-31 15:41:41 +00:00
* /
import Component from '@glimmer/component' ;
import ControlGroupError from 'vault/lib/control-group-error' ;
import Ember from 'ember' ;
import keys from 'vault/lib/keycodes' ;
2021-12-17 03:44:29 +00:00
import { action , set } from '@ember/object' ;
2021-08-31 15:41:41 +00:00
import { inject as service } from '@ember/service' ;
import { tracked } from '@glimmer/tracking' ;
import { isBlank , isNone } from '@ember/utils' ;
import { task , waitForEvent } from 'ember-concurrency' ;
const LIST _ROUTE = 'vault.cluster.secrets.backend.list' ;
const LIST _ROOT _ROUTE = 'vault.cluster.secrets.backend.list-root' ;
const SHOW _ROUTE = 'vault.cluster.secrets.backend.show' ;
export default class SecretCreateOrUpdate extends Component {
@ tracked codemirrorString = null ;
@ tracked error = null ;
@ tracked secretPaths = null ;
2021-10-28 16:50:33 +00:00
@ tracked pathWhiteSpaceWarning = false ;
2021-08-31 15:41:41 +00:00
@ tracked validationErrorCount = 0 ;
@ tracked validationMessages = null ;
@ service controlGroup ;
@ service router ;
@ service store ;
2022-10-18 15:46:02 +00:00
@ action
setup ( elem , [ secretData , model , mode ] ) {
this . codemirrorString = secretData . toJSONString ( ) ;
2021-08-31 15:41:41 +00:00
this . validationMessages = {
path : '' ,
} ;
// for validation, return array of path names already assigned
if ( Ember . testing ) {
this . secretPaths = [ 'beep' , 'bop' , 'boop' ] ;
} else {
2022-10-18 15:46:02 +00:00
const adapter = this . store . adapterFor ( 'secret-v2' ) ;
const type = { modelName : 'secret-v2' } ;
const query = { backend : model . backend } ;
2021-12-17 03:44:29 +00:00
adapter . query ( this . store , type , query ) . then ( ( result ) => {
2021-08-31 15:41:41 +00:00
this . secretPaths = result . data . keys ;
} ) ;
}
this . checkRows ( ) ;
2022-10-18 15:46:02 +00:00
if ( mode === 'edit' ) {
2021-08-31 15:41:41 +00:00
this . addRow ( ) ;
}
}
checkRows ( ) {
if ( this . args . secretData . length === 0 ) {
this . addRow ( ) ;
}
}
checkValidation ( name , value ) {
if ( name === 'path' ) {
2021-10-28 16:50:33 +00:00
// check for whitespace
this . pathHasWhiteSpace ( value ) ;
2021-08-31 15:41:41 +00:00
! value
? set ( this . validationMessages , name , ` ${ name } can't be blank. ` )
: set ( this . validationMessages , name , '' ) ;
}
// check duplicate on path
if ( name === 'path' && value ) {
this . secretPaths ? . includes ( value )
? set ( this . validationMessages , name , ` A secret with this ${ name } already exists. ` )
: set ( this . validationMessages , name , '' ) ;
}
2022-11-09 23:15:31 +00:00
const values = Object . values ( this . validationMessages ) ;
2021-08-31 15:41:41 +00:00
this . validationErrorCount = values . filter ( Boolean ) . length ;
}
onEscape ( e ) {
if ( e . keyCode !== keys . ESC || this . args . mode !== 'show' ) {
return ;
}
const parentKey = this . args . model . parentKey ;
if ( parentKey ) {
this . transitionToRoute ( LIST _ROUTE , parentKey ) ;
} else {
this . transitionToRoute ( LIST _ROOT _ROUTE ) ;
}
}
2021-10-28 16:50:33 +00:00
pathHasWhiteSpace ( value ) {
2022-11-09 23:15:31 +00:00
const validation = new RegExp ( '\\s' , 'g' ) ; // search for whitespace
2021-10-28 16:50:33 +00:00
this . pathWhiteSpaceWarning = validation . test ( value ) ;
}
2021-08-31 15:41:41 +00:00
// successCallback is called in the context of the component
persistKey ( successCallback ) {
2022-11-09 23:15:31 +00:00
const secret = this . args . model ;
const secretData = this . args . modelForData ;
const isV2 = this . args . isV2 ;
2021-08-31 15:41:41 +00:00
let key = secretData . get ( 'path' ) || secret . id ;
if ( key . startsWith ( '/' ) ) {
key = key . replace ( /^\/+/g , '' ) ;
secretData . set ( secretData . pathAttr , key ) ;
}
2022-11-09 23:15:31 +00:00
const changed = secret . changedAttributes ( ) ;
const changedKeys = Object . keys ( changed ) ;
2021-08-31 15:41:41 +00:00
return secretData
. save ( )
. then ( ( ) => {
2022-04-01 21:05:42 +00:00
if ( ! this . args . canReadSecretData && secret . selectedVersion ) {
delete secret . selectedVersion . secretData ;
}
2021-08-31 15:41:41 +00:00
if ( ! secretData . isError ) {
if ( isV2 ) {
secret . set ( 'id' , key ) ;
}
2021-10-12 19:42:04 +00:00
// this secret.save() saves to the metadata endpoint. Only saved if metadata has been added
// and if the currentVersion attr changed that's because we added it (only happens if they don't have read access to metadata on mode = update which does not allow you to change metadata)
if ( isV2 && changedKeys . length > 0 && changedKeys [ 0 ] !== 'currentVersion' ) {
2021-08-31 15:41:41 +00:00
// save secret metadata
secret
. save ( )
. then ( ( ) => {
this . saveComplete ( successCallback , key ) ;
} )
2021-12-17 03:44:29 +00:00
. catch ( ( e ) => {
2021-08-31 15:41:41 +00:00
// when mode is not create the metadata error is handled in secret-edit-metadata
2021-10-12 19:42:04 +00:00
if ( this . args . mode === 'create' ) {
2021-08-31 15:41:41 +00:00
this . error = e . errors . join ( ' ' ) ;
}
return ;
} ) ;
} else {
this . saveComplete ( successCallback , key ) ;
}
}
} )
2021-12-17 03:44:29 +00:00
. catch ( ( error ) => {
2021-08-31 15:41:41 +00:00
if ( error instanceof ControlGroupError ) {
2022-11-09 23:15:31 +00:00
const errorMessage = this . controlGroup . logFromError ( error ) ;
2021-08-31 15:41:41 +00:00
this . error = errorMessage . content ;
}
throw error ;
} ) ;
}
saveComplete ( callback , key ) {
callback ( key ) ;
}
transitionToRoute ( ) {
return this . router . transitionTo ( ... arguments ) ;
}
get isCreateNewVersionFromOldVersion ( ) {
2022-11-09 23:15:31 +00:00
const model = this . args . model ;
2021-08-31 15:41:41 +00:00
if ( ! model ) {
return false ;
}
if (
! model . failedServerRead &&
! model . selectedVersion ? . failedServerRead &&
model . selectedVersion ? . version !== model . currentVersion
) {
return true ;
}
return false ;
}
2021-12-17 03:44:29 +00:00
@ ( task ( function * ( name , value ) {
2021-08-31 15:41:41 +00:00
this . checkValidation ( name , value ) ;
while ( true ) {
2022-11-09 23:15:31 +00:00
const event = yield waitForEvent ( document . body , 'keyup' ) ;
2021-08-31 15:41:41 +00:00
this . onEscape ( event ) ;
}
} )
. on ( 'didInsertElement' )
. cancelOn ( 'willDestroyElement' ) )
waitForKeyUp ;
@ action
addRow ( ) {
const data = this . args . secretData ;
// fired off on init
if ( isNone ( data . findBy ( 'name' , '' ) ) ) {
data . pushObject ( { name : '' , value : '' } ) ;
this . handleChange ( ) ;
}
this . checkRows ( ) ;
}
@ action
codemirrorUpdated ( val , codemirror ) {
this . error = null ;
codemirror . performLint ( ) ;
const noErrors = codemirror . state . lint . marked . length === 0 ;
if ( noErrors ) {
try {
this . args . secretData . fromJSONString ( val ) ;
set ( this . args . modelForData , 'secretData' , this . args . secretData . toJSON ( ) ) ;
} catch ( e ) {
this . error = e . message ;
}
}
this . codemirrorString = val ;
}
@ action
createOrUpdateKey ( type , event ) {
event . preventDefault ( ) ;
if ( type === 'create' && isBlank ( this . args . modelForData . path || this . args . modelForData . id ) ) {
this . checkValidation ( 'path' , '' ) ;
return ;
}
this . persistKey ( ( ) => {
this . transitionToRoute ( SHOW _ROUTE , this . args . model . path || this . args . model . id ) ;
} ) ;
}
@ action
deleteRow ( name ) {
const data = this . args . secretData ;
const item = data . findBy ( 'name' , name ) ;
if ( isBlank ( item . name ) ) {
return ;
}
data . removeObject ( item ) ;
this . checkRows ( ) ;
this . handleChange ( ) ;
}
@ action
formatJSON ( ) {
this . codemirrorString = this . args . secretData . toJSONString ( true ) ;
}
@ action
handleChange ( ) {
this . codemirrorString = this . args . secretData . toJSONString ( true ) ;
set ( this . args . modelForData , 'secretData' , this . args . secretData . toJSON ( ) ) ;
}
//submit on shift + enter
@ action
handleKeyDown ( e ) {
e . stopPropagation ( ) ;
if ( ! ( e . keyCode === keys . ENTER && e . metaKey ) ) {
return ;
}
2022-11-09 23:15:31 +00:00
const $form = this . element . querySelector ( 'form' ) ;
2021-08-31 15:41:41 +00:00
if ( $form . length ) {
$form . submit ( ) ;
}
}
@ action
updateValidationErrorCount ( errorCount ) {
this . validationErrorCount = errorCount ;
}
}