2018-04-23 22:00:02 +00:00
package command
import (
2021-10-13 19:24:31 +00:00
"context"
2018-04-23 22:00:02 +00:00
"fmt"
"io"
"os"
"strings"
2021-10-13 19:24:31 +00:00
"github.com/hashicorp/vault/api"
2018-04-23 22:00:02 +00:00
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
2021-04-08 16:43:39 +00:00
var (
_ cli . Command = ( * KVPatchCommand ) ( nil )
_ cli . CommandAutocomplete = ( * KVPatchCommand ) ( nil )
)
2018-04-23 22:00:02 +00:00
type KVPatchCommand struct {
* BaseCommand
2021-10-13 19:24:31 +00:00
flagCAS int
flagMethod string
testStdin io . Reader // for tests
2018-04-23 22:00:02 +00:00
}
func ( c * KVPatchCommand ) Synopsis ( ) string {
2018-06-15 19:34:17 +00:00
return "Sets or updates data in the KV store without overwriting"
2018-04-23 22:00:02 +00:00
}
func ( c * KVPatchCommand ) Help ( ) string {
helpText := `
2018-04-25 07:21:13 +00:00
Usage : vault kv patch [ options ] KEY [ DATA ]
2018-04-23 22:00:02 +00:00
* NOTE * : This is only supported for KV v2 engine mounts .
Writes the data to the given path in the key - value store . The data can be of
any type .
$ vault kv patch secret / foo bar = baz
The data can also be consumed from a file on disk by prefixing with the "@"
symbol . For example :
$ vault kv patch secret / foo @ data . json
Or it can be read from stdin using the "-" symbol :
$ echo "abcd1234" | vault kv patch secret / foo bar = -
2021-10-13 19:24:31 +00:00
To perform a Check - And - Set operation , specify the - cas flag with the
appropriate version number corresponding to the key you want to perform
the CAS operation on :
$ vault kv patch - cas = 1 secret / foo bar = baz
By default , this operation will attempt an HTTP PATCH operation . If your
policy does not allow that , it will fall back to a read / local update / write approach .
If you wish to specify which method this command should use , you may do so
with the - method flag . When - method = patch is specified , only an HTTP PATCH
operation will be tried . If it fails , the entire command will fail .
$ vault kv patch - method = patch secret / foo bar = baz
When - method = rw is specified , only a read / local update / write approach will be tried .
This was the default behavior previous to Vault 1.9 .
$ vault kv patch - method = rw secret / foo bar = baz
2018-04-23 22:00:02 +00:00
Additional flags and more advanced use cases are detailed below .
` + c . Flags ( ) . Help ( )
return strings . TrimSpace ( helpText )
}
func ( c * KVPatchCommand ) Flags ( ) * FlagSets {
set := c . flagSet ( FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat )
2021-10-13 19:24:31 +00:00
// Patch specific options
f := set . NewFlagSet ( "Common Options" )
f . IntVar ( & IntVar {
Name : "cas" ,
Target : & c . flagCAS ,
Default : 0 ,
Usage : ` Specifies to use a Check - And - Set operation . If set to 0 or not
set , the patch will be allowed . If the index is non - zero the patch will
only be allowed if the key ’ s current version matches the version
specified in the cas parameter . ` ,
} )
f . StringVar ( & StringVar {
Name : "method" ,
Target : & c . flagMethod ,
Usage : ` Specifies which method of patching to use . If set to "patch" , then
an HTTP PATCH request will be issued . If set to "rw" , then a read will be
performed , then a local update , followed by a remote update . ` ,
} )
2018-04-23 22:00:02 +00:00
return set
}
func ( c * KVPatchCommand ) AutocompleteArgs ( ) complete . Predictor {
return nil
}
func ( c * KVPatchCommand ) AutocompleteFlags ( ) complete . Flags {
return c . Flags ( ) . Completions ( )
}
func ( c * KVPatchCommand ) Run ( args [ ] string ) int {
f := c . Flags ( )
if err := f . Parse ( args ) ; err != nil {
c . UI . Error ( err . Error ( ) )
return 1
}
args = f . Args ( )
// Pull our fake stdin if needed
stdin := ( io . Reader ) ( os . Stdin )
if c . testStdin != nil {
stdin = c . testStdin
}
switch {
case len ( args ) < 1 :
c . UI . Error ( fmt . Sprintf ( "Not enough arguments (expected >1, got %d)" , len ( args ) ) )
return 1
case len ( args ) == 1 :
c . UI . Error ( "Must supply data" )
return 1
}
var err error
path := sanitizePath ( args [ 0 ] )
client , err := c . Client ( )
if err != nil {
c . UI . Error ( err . Error ( ) )
return 2
}
newData , err := parseArgsData ( stdin , args [ 1 : ] )
if err != nil {
c . UI . Error ( fmt . Sprintf ( "Failed to parse K=V data: %s" , err ) )
return 1
}
mountPath , v2 , err := isKVv2 ( path , client )
if err != nil {
c . UI . Error ( err . Error ( ) )
return 2
}
if ! v2 {
2021-09-30 11:33:14 +00:00
c . UI . Error ( "K/V engine mount must be version 2 for patch support" )
2018-04-23 22:00:02 +00:00
return 2
}
path = addPrefixToVKVPath ( path , mountPath , "data" )
if err != nil {
c . UI . Error ( err . Error ( ) )
return 2
}
2021-10-13 19:24:31 +00:00
// Check the method and behave accordingly
var secret * api . Secret
var code int
switch c . flagMethod {
case "rw" :
secret , code = c . readThenWrite ( client , path , newData )
case "patch" :
secret , code = c . mergePatch ( client , path , newData , false )
case "" :
secret , code = c . mergePatch ( client , path , newData , true )
default :
c . UI . Error ( fmt . Sprintf ( "Unsupported method provided to -method flag: %s" , c . flagMethod ) )
return 2
}
if code != 0 {
return code
}
return OutputSecret ( c . UI , secret )
}
func ( c * KVPatchCommand ) readThenWrite ( client * api . Client , path string , newData map [ string ] interface { } ) ( * api . Secret , int ) {
2019-07-03 13:03:35 +00:00
// First, do a read.
// Note that we don't want to see curl output for the read request.
curOutputCurl := client . OutputCurlString ( )
client . SetOutputCurlString ( false )
2018-04-23 22:00:02 +00:00
secret , err := kvReadRequest ( client , path , nil )
2019-07-03 13:03:35 +00:00
client . SetOutputCurlString ( curOutputCurl )
2018-04-23 22:00:02 +00:00
if err != nil {
c . UI . Error ( fmt . Sprintf ( "Error doing pre-read at %s: %s" , path , err ) )
2021-10-13 19:24:31 +00:00
return nil , 2
2018-04-23 22:00:02 +00:00
}
// Make sure a value already exists
if secret == nil || secret . Data == nil {
c . UI . Error ( fmt . Sprintf ( "No value found at %s" , path ) )
2021-10-13 19:24:31 +00:00
return nil , 2
2018-04-23 22:00:02 +00:00
}
// Verify metadata found
rawMeta , ok := secret . Data [ "metadata" ]
if ! ok || rawMeta == nil {
c . UI . Error ( fmt . Sprintf ( "No metadata found at %s; patch only works on existing data" , path ) )
2021-10-13 19:24:31 +00:00
return nil , 2
2018-04-23 22:00:02 +00:00
}
meta , ok := rawMeta . ( map [ string ] interface { } )
if ! ok {
c . UI . Error ( fmt . Sprintf ( "Metadata found at %s is not the expected type (JSON object)" , path ) )
2021-10-13 19:24:31 +00:00
return nil , 2
2018-04-23 22:00:02 +00:00
}
if meta == nil {
c . UI . Error ( fmt . Sprintf ( "No metadata found at %s; patch only works on existing data" , path ) )
2021-10-13 19:24:31 +00:00
return nil , 2
2018-04-23 22:00:02 +00:00
}
// Verify old data found
rawData , ok := secret . Data [ "data" ]
if ! ok || rawData == nil {
c . UI . Error ( fmt . Sprintf ( "No data found at %s; patch only works on existing data" , path ) )
2021-10-13 19:24:31 +00:00
return nil , 2
2018-04-23 22:00:02 +00:00
}
data , ok := rawData . ( map [ string ] interface { } )
if ! ok {
c . UI . Error ( fmt . Sprintf ( "Data found at %s is not the expected type (JSON object)" , path ) )
2021-10-13 19:24:31 +00:00
return nil , 2
2018-04-23 22:00:02 +00:00
}
if data == nil {
c . UI . Error ( fmt . Sprintf ( "No data found at %s; patch only works on existing data" , path ) )
2021-10-13 19:24:31 +00:00
return nil , 2
2018-04-23 22:00:02 +00:00
}
// Copy new data over
for k , v := range newData {
data [ k ] = v
}
secret , err = client . Logical ( ) . Write ( path , map [ string ] interface { } {
"data" : data ,
"options" : map [ string ] interface { } {
"cas" : meta [ "version" ] ,
} ,
} )
if err != nil {
c . UI . Error ( fmt . Sprintf ( "Error writing data to %s: %s" , path , err ) )
2021-10-13 19:24:31 +00:00
return nil , 2
2018-04-23 22:00:02 +00:00
}
2021-10-13 19:24:31 +00:00
2018-04-23 22:00:02 +00:00
if secret == nil {
// Don't output anything unless using the "table" format
if Format ( c . UI ) == "table" {
c . UI . Info ( fmt . Sprintf ( "Success! Data written to: %s" , path ) )
}
2021-10-13 19:24:31 +00:00
return nil , 0
2018-04-23 22:00:02 +00:00
}
if c . flagField != "" {
2021-10-13 19:24:31 +00:00
return nil , PrintRawField ( c . UI , secret , c . flagField )
2018-04-23 22:00:02 +00:00
}
2021-10-13 19:24:31 +00:00
return secret , 0
}
func ( c * KVPatchCommand ) mergePatch ( client * api . Client , path string , newData map [ string ] interface { } , rwFallback bool ) ( * api . Secret , int ) {
data := map [ string ] interface { } {
"data" : newData ,
"options" : map [ string ] interface { } { } ,
}
if c . flagCAS > 0 {
data [ "options" ] . ( map [ string ] interface { } ) [ "cas" ] = c . flagCAS
}
secret , err := client . Logical ( ) . JSONMergePatch ( context . Background ( ) , path , data )
if err != nil {
2022-01-11 16:52:24 +00:00
// If it's a 405, that probably means the server is running a pre-1.9
// Vault version that doesn't support the HTTP PATCH method.
// Fall back to the old way of doing it if the user didn't specify a -method.
// If they did, and it was "patch", then just error.
if re , ok := err . ( * api . ResponseError ) ; ok && re . StatusCode == 405 && rwFallback {
return c . readThenWrite ( client , path , newData )
}
2021-10-13 19:24:31 +00:00
// If it's a 403, that probably means they don't have the patch capability in their policy. Fall back to
// the old way of doing it if the user didn't specify a -method. If they did, and it was "patch", then just error.
if re , ok := err . ( * api . ResponseError ) ; ok && re . StatusCode == 403 && rwFallback {
c . UI . Warn ( fmt . Sprintf ( "Data was written to %s but we recommend that you add the \"patch\" capability to your ACL policy in order to use HTTP PATCH in the future." , path ) )
return c . readThenWrite ( client , path , newData )
}
c . UI . Error ( fmt . Sprintf ( "Error writing data to %s: %s" , path , err ) )
return nil , 2
}
if secret == nil {
// Don't output anything unless using the "table" format
if Format ( c . UI ) == "table" {
c . UI . Info ( fmt . Sprintf ( "Success! Data written to: %s" , path ) )
}
return nil , 0
}
if c . flagField != "" {
return nil , PrintRawField ( c . UI , secret , c . flagField )
}
return secret , 0
2018-04-23 22:00:02 +00:00
}