Skip to content

Commit

Permalink
Add a command to export credentials
Browse files Browse the repository at this point in the history
Exporting credentials is useful both for backup and management purposes
(e.g. mass-moving a number of credentials to a different path). This
command enables exporting by path matching, utilising the bulk export
models to create an output that is reimportable.

By default, the command exports all credentials to `stdout` in import
compatible YAML. The `-p` flag can be used to restrict the paths
exported; the `-f` flag to write the output to a file. Note that the
credentials are held unencrypted in-memory during this process; as this
seems true of all the code, this should not be seen as a problem; just a
caveat.
  • Loading branch information
archgrove committed Mar 11, 2018
1 parent 32d6358 commit 92cc047
Show file tree
Hide file tree
Showing 3 changed files with 244 additions and 0 deletions.
1 change: 1 addition & 0 deletions commands/credhub.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package commands
type CredhubCommand struct {
Api ApiCommand `command:"api" alias:"a" description:"Get or set the CredHub API target where commands are sent" long-description:"Get or set the CredHub API target where commands are sent. The api command without any flags will return the current target. If --ca-cert or --skip-tls-validation are provided, these preferences will be cached for future requests."`
Delete DeleteCommand `command:"delete" alias:"d" description:"Delete a credential" long-description:"Delete a credential. This will delete all versions of the credential.\n\n More information: https://credhub-api.cfapps.io/#delete-credentials"`
Export ExportCommand `command:"export" alias:"e" description:"Export all credentials" long-description:"Export all credentials.\n\n More information: https://credhub-api.cfapps.io/#export-credentials"`
Find FindCommand `command:"find" alias:"f" description:"Find stored credential names or paths based on query parameters" long-description:"Find stored credential names or paths based on query parameters.\n\n More information: https://credhub-api.cfapps.io/#find-credentials"`
Generate GenerateCommand `command:"generate" alias:"n" description:"Generate and set a credential value" long-description:"Set a credential with generated value(s). A type must be specified when generating a credential. The provided flags are used to set parameters for the credential that is generated, e.g. a certificate credential may use --common-name, --duration and --self-sign to generate an appropriate value. Supported credential types are prefixed in the flag description.\n\n More information: https://credhub-api.cfapps.io/#generate-credentials"`
Get GetCommand `command:"get" alias:"g" description:"Get a credential value" long-description:"Get a credential value by name or ID.\n\n More information: https://credhub-api.cfapps.io/#get-credentials"`
Expand Down
65 changes: 65 additions & 0 deletions commands/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package commands

import (
"fmt"
"io/ioutil"

"github.com/cloudfoundry-incubator/credhub-cli/config"
"github.com/cloudfoundry-incubator/credhub-cli/credhub/credentials"
"github.com/cloudfoundry-incubator/credhub-cli/models"
)

type ExportCommand struct {
Path string `short:"p" long:"path" description:"Path of credentials to export" required:"false"`
File string `short:"f" long:"file" description:"File in which to write credentials" required:"false"`
}

func (cmd ExportCommand) Execute([]string) error {
allCredentials, err := getAllCredentialsForPath(cmd.Path)

if err != nil {
return err
}

exportCreds, err := models.ExportCredentials(allCredentials)

if err != nil {
return err
}

if cmd.File == "" {
fmt.Printf("%s", exportCreds)

return err
} else {
return ioutil.WriteFile(cmd.File, exportCreds.Bytes, 0644)
}
}

func getAllCredentialsForPath(path string) ([]credentials.Credential, error) {
cfg := config.ReadConfig()
credhubClient, err := initializeCredhubClient(cfg)

if err != nil {
return nil, err
}

allPaths, err := credhubClient.FindByPath(path)

if err != nil {
return nil, err
}

credentials := make([]credentials.Credential, len(allPaths.Credentials))
for i, baseCred := range allPaths.Credentials {
credential, err := credhubClient.GetLatestVersion(baseCred.Name)

if err != nil {
return nil, err
}

credentials[i] = credential
}

return credentials, nil
}
178 changes: 178 additions & 0 deletions commands/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package commands_test

import (
"io/ioutil"
"net/http"
"os"

"github.com/cloudfoundry-incubator/credhub-cli/config"
"runtime"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gbytes"
. "github.com/onsi/gomega/gexec"
. "github.com/onsi/gomega/ghttp"
)

func withTemporaryFile(wantingFile func(string)) error {
f, err := ioutil.TempFile("", "credhub_tests_")

if err != nil {
return err
}

name := f.Name()

f.Close()
wantingFile(name)

return os.Remove(name)
}

var _ = Describe("Export", func() {
BeforeEach(func() {
login()
})

ItRequiresAuthentication("export")
ItRequiresAnAPIToBeSet("export")
ItAutomaticallyLogsIn("GET", "get_response.json", "/api/v1/data", "export")

ItBehavesLikeHelp("export", "e", func(session *Session) {
Expect(session.Err).To(Say("Usage"))
if runtime.GOOS == "windows" {
Expect(session.Err).To(Say("credhub-cli.exe \\[OPTIONS\\] export \\[export-OPTIONS\\]"))
} else {
Expect(session.Err).To(Say("credhub-cli \\[OPTIONS\\] export \\[export-OPTIONS\\]"))
}
})

Describe("Exporting", func() {
It("queries for the most recent version of all credentials", func() {
findJson := `{
"credentials": [
{
"version_created_at": "idc",
"name": "/path/to/cred"
},
{
"version_created_at": "idc",
"name": "/path/to/another/cred"
}
]
}`

getJson := `{
"data": [{
"type":"value",
"id":"some_uuid",
"name":"/path/to/cred",
"version_created_at":"idc",
"value": "foo"
}]
}`

responseTable := `credentials:
- name: /path/to/cred
type: value
value: foo
- name: /path/to/cred
type: value
value: foo`

server.AppendHandlers(
CombineHandlers(
VerifyRequest("GET", "/api/v1/data", "path="),
RespondWith(http.StatusOK, findJson),
),
CombineHandlers(
VerifyRequest("GET", "/api/v1/data", "name=/path/to/cred&current=true"),
RespondWith(http.StatusOK, getJson),
),
CombineHandlers(
VerifyRequest("GET", "/api/v1/data", "name=/path/to/another/cred&current=true"),
RespondWith(http.StatusOK, getJson),
),
)

session := runCommand("export")

Eventually(session).Should(Exit(0))
Eventually(session.Out).Should(Say(responseTable))
})

Context("when given a path", func() {
It("queries for credentials matching that path", func() {
noCredsJson := `{ "credentials" : [] }`

server.AppendHandlers(
CombineHandlers(
VerifyRequest("GET", "/api/v1/data", "path=/some/path"),
RespondWith(http.StatusOK, noCredsJson),
),
)

session := runCommand("export", "-p", "/some/path")

Eventually(session).Should(Exit(0))
})
})

Context("when given a file", func() {
It("writes the YAML to that file", func() {
withTemporaryFile(func(filename string) {
noCredsJson := `{ "credentials" : [] }`
noCredsYaml := `credentials: []
`

server.AppendHandlers(
CombineHandlers(
VerifyRequest("GET", "/api/v1/data", "path="),
RespondWith(http.StatusOK, noCredsJson),
),
)

session := runCommand("export", "-f", filename)

Eventually(session).Should(Exit(0))

Expect(filename).To(BeAnExistingFile())

fileContents, _ := ioutil.ReadFile(filename)

Expect(string(fileContents)).To(Equal(noCredsYaml))
})
})
})
})

Describe("Errors", func() {
It("prints an error when the network request fails", func() {
cfg := config.ReadConfig()
cfg.ApiURL = "mashed://potatoes"
config.WriteConfig(cfg)

session := runCommand("export")

Eventually(session).Should(Exit(1))
Eventually(string(session.Err.Contents())).Should(ContainSubstring("Get mashed://potatoes/api/v1/data?path=: unsupported protocol scheme \"mashed\""))
})

It("prints an error if the specified output file cannot be opened", func() {
noCredsJson := `{ "credentials" : [] }`

server.AppendHandlers(
CombineHandlers(
VerifyRequest("GET", "/api/v1/data", "path="),
RespondWith(http.StatusOK, noCredsJson),
),
)

session := runCommand("export", "-f", "/this/should/not/exist/anywhere")

Eventually(session).Should(Exit(1))
Eventually(string(session.Err.Contents())).Should(ContainSubstring("open /this/should/not/exist/anywhere: no such file or directory"))
})
})
})

0 comments on commit 92cc047

Please sign in to comment.