Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add command unset #1475

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1470,6 +1470,25 @@ The value must be formatted as json.

$ sops set ~/git/svc/sops/example.yaml '["an_array"][1]' '{"uid1":null,"uid2":1000,"uid3":["bob"]}'

Unset a sub-part in a document tree
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Symmetrically, SOPS can unset a specific part of a YAML or JSON document, by providing
the path in the ``unset`` command. This is useful to unset specific values, like keys, without
needing an editor.

.. code:: sh

$ sops unset ~/git/svc/sops/example.yaml '["app2"]["key"]'

The tree path syntax uses regular python dictionary syntax, without the
variable name. Set to keys by naming them, and array elements by
numbering them.

.. code:: sh

$ sops unset ~/git/svc/sops/example.yaml '["an_array"][1]'

Showing diffs in cleartext in git
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
88 changes: 88 additions & 0 deletions cmd/sops/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1283,6 +1283,94 @@ func main() {
return toExitError(err)
}

// We open the file *after* the operations on the tree have been
// executed to avoid truncating it when there's errors
file, err := os.Create(fileName)
if err != nil {
return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
}
defer file.Close()
_, err = file.Write(output)
if err != nil {
return toExitError(err)
}
log.Info("File written successfully")
return nil
},
},
{
Name: "unset",
Usage: `unset a specific key or branch in the input document.`,
ArgsUsage: `file index`,
Flags: append([]cli.Flag{
cli.StringFlag{
Name: "input-type",
Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
},
cli.StringFlag{
Name: "output-type",
Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format",
},
cli.IntFlag{
Name: "shamir-secret-sharing-threshold",
Usage: "the number of master keys required to retrieve the data key with shamir",
},
cli.BoolFlag{
Name: "ignore-mac",
Usage: "ignore Message Authentication Code during decryption",
},
cli.StringFlag{
Name: "decryption-order",
Usage: "comma separated list of decryption key types",
EnvVar: "SOPS_DECRYPTION_ORDER",
},
cli.BoolFlag{
Name: "idempotent",
Usage: "do nothing if the given index does not exist",
},
}, keyserviceFlags...),
Action: func(c *cli.Context) error {
if c.Bool("verbose") {
logging.SetLevel(logrus.DebugLevel)
}
if c.NArg() != 2 {
return common.NewExitError("Error: no file specified, or index is missing", codes.NoFileSpecified)
}
fileName, err := filepath.Abs(c.Args()[0])
if err != nil {
return toExitError(err)
}

inputStore := inputStore(c, fileName)
outputStore := outputStore(c, fileName)
svcs := keyservices(c)

path, err := parseTreePath(c.Args()[1])
if err != nil {
return common.NewExitError("Invalid unset index format", codes.ErrorInvalidSetFormat)
}

order, err := decryptionOrder(c.String("decryption-order"))
if err != nil {
return toExitError(err)
}
output, err := unset(unsetOpts{
OutputStore: outputStore,
InputStore: inputStore,
InputPath: fileName,
Cipher: aes.NewCipher(),
KeyServices: svcs,
DecryptionOrder: order,
IgnoreMAC: c.Bool("ignore-mac"),
TreePath: path,
})
if err != nil {
if _, ok := err.(*sops.SopsKeyNotFound); ok && c.Bool("idempotent") {
return nil
}
return toExitError(err)
}

// We open the file *after* the operations on the tree have been
// executed to avoid truncating it when there's errors
file, err := os.Create(fileName)
Expand Down
67 changes: 67 additions & 0 deletions cmd/sops/unset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package main

import (
"fmt"

"github.com/getsops/sops/v3"
"github.com/getsops/sops/v3/cmd/sops/codes"
"github.com/getsops/sops/v3/cmd/sops/common"
"github.com/getsops/sops/v3/keyservice"
)

type unsetOpts struct {
Cipher sops.Cipher
InputStore sops.Store
OutputStore sops.Store
InputPath string
IgnoreMAC bool
TreePath []interface{}
KeyServices []keyservice.KeyServiceClient
DecryptionOrder []string
}

func unset(opts unsetOpts) ([]byte, error) {
// Load the file
tree, err := common.LoadEncryptedFileWithBugFixes(common.GenericDecryptOpts{
Cipher: opts.Cipher,
InputStore: opts.InputStore,
InputPath: opts.InputPath,
IgnoreMAC: opts.IgnoreMAC,
KeyServices: opts.KeyServices,
})
if err != nil {
return nil, err
}

// Decrypt the file
dataKey, err := common.DecryptTree(common.DecryptTreeOpts{
Cipher: opts.Cipher,
IgnoreMac: opts.IgnoreMAC,
Tree: tree,
KeyServices: opts.KeyServices,
DecryptionOrder: opts.DecryptionOrder,
})
if err != nil {
return nil, err
}

// Unset the value
newBranch, err := tree.Branches[0].Unset(opts.TreePath)
if err != nil {
return nil, err
}
tree.Branches[0] = newBranch

err = common.EncryptTree(common.EncryptTreeOpts{
DataKey: dataKey, Tree: tree, Cipher: opts.Cipher,
})
if err != nil {
return nil, err
}

encryptedFile, err := opts.OutputStore.EmitEncryptedFile(*tree)
if err != nil {
return nil, common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree)
}
return encryptedFile, err
}