Refactor the notary command line to not use global mutable state, and to not exit on error.

This way we can test the command more easily (we want to test the error, as opposed to
just killing the test).

Signed-off-by: Ying Li <ying.li@docker.com>
This commit is contained in:
Ying Li 2016-02-03 04:57:42 -08:00
parent 6acb6a1802
commit d67a7e128c
11 changed files with 537 additions and 361 deletions

View File

@ -2,65 +2,82 @@ package main
import (
"crypto/x509"
"os"
"fmt"
"path/filepath"
"github.com/docker/notary"
notaryclient "github.com/docker/notary/client"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/trustmanager"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func init() {
cmdCert.AddCommand(cmdCertList)
cmdCertRemove.Flags().StringVarP(&certRemoveGUN, "gun", "g", "", "Globally unique name to delete certificates for")
cmdCertRemove.Flags().BoolVarP(&certRemoveYes, "yes", "y", false, "Answer yes to the removal question (no confirmation)")
cmdCert.AddCommand(cmdCertRemove)
}
var cmdCert = &cobra.Command{
var cmdCertTemplate = usageTemplate{
Use: "cert",
Short: "Operates on certificates.",
Long: `Operations on certificates.`,
}
var cmdCertList = &cobra.Command{
var cmdCertListTemplate = usageTemplate{
Use: "list",
Short: "Lists certificates.",
Long: "Lists root certificates known to notary.",
Run: certList,
}
var certRemoveGUN string
var certRemoveYes bool
var cmdCertRemove = &cobra.Command{
var cmdCertRemoveTemplate = usageTemplate{
Use: "remove [ certID ]",
Short: "Removes the certificate with the given cert ID.",
Long: "Remove the certificate with the given cert ID from the local host.",
Run: certRemove,
}
type certCommander struct {
// these need to be set
configGetter func() (*viper.Viper, error)
retriever passphrase.Retriever
// these are for command line parsing - no need to set
certRemoveGUN string
certRemoveYes bool
}
func (c *certCommander) GetCommand() *cobra.Command {
cmd := cmdCertTemplate.ToCommand(nil)
cmd.AddCommand(cmdCertListTemplate.ToCommand(c.certList))
cmdCertRemove := cmdCertRemoveTemplate.ToCommand(c.certRemove)
cmdCertRemove.Flags().StringVarP(
&c.certRemoveGUN, "gun", "g", "", "Globally unique name to delete certificates for")
cmdCertRemove.Flags().BoolVarP(
&c.certRemoveYes, "yes", "y", false, "Answer yes to the removal question (no confirmation)")
cmd.AddCommand(cmdCertRemove)
return cmd
}
// certRemove deletes a certificate given a cert ID or a gun
// If given a gun, certRemove will also remove local TUF data
func certRemove(cmd *cobra.Command, args []string) {
func (c *certCommander) certRemove(cmd *cobra.Command, args []string) error {
// If the user hasn't provided -g with a gun, or a cert ID, show usage
// If the user provided -g and a cert ID, also show usage
if (len(args) < 1 && certRemoveGUN == "") || (len(args) > 0 && certRemoveGUN != "") {
if (len(args) < 1 && c.certRemoveGUN == "") || (len(args) > 0 && c.certRemoveGUN != "") {
cmd.Usage()
fatalf("Must specify the cert ID or the GUN of the certificates to remove")
return fmt.Errorf("Must specify the cert ID or the GUN of the certificates to remove")
}
config, err := c.configGetter()
if err != nil {
return err
}
parseConfig()
trustDir := mainViper.GetString("trust_dir")
trustDir := config.GetString("trust_dir")
certPath := filepath.Join(trustDir, notary.TrustedCertsDir)
certStore, err := trustmanager.NewX509FilteredFileStore(
certPath,
trustmanager.FilterCertsExpiredSha1,
)
if err != nil {
fatalf("Failed to create a new truststore with directory: %s", trustDir)
return fmt.Errorf("Failed to create a new truststore with directory: %s", trustDir)
}
var certsToRemove []*x509.Certificate
@ -68,26 +85,26 @@ func certRemove(cmd *cobra.Command, args []string) {
var removeTrustData bool
// If there is no GUN, we expect a cert ID
if certRemoveGUN == "" {
if c.certRemoveGUN == "" {
certID := args[0]
// Attempt to find this certificate
certFoundByID, err = certStore.GetCertificateByCertID(certID)
if err != nil {
// This is an invalid ID, the user might have forgotten a character
if len(certID) != notary.Sha256HexSize {
fatalf("Unable to retrieve certificate with invalid certificate ID provided: %s", certID)
return fmt.Errorf("Unable to retrieve certificate with invalid certificate ID provided: %s", certID)
}
fatalf("Unable to retrieve certificate with cert ID: %s", certID)
return fmt.Errorf("Unable to retrieve certificate with cert ID: %s", certID)
}
// the GUN is the CN from the certificate
certRemoveGUN = certFoundByID.Subject.CommonName
c.certRemoveGUN = certFoundByID.Subject.CommonName
certsToRemove = []*x509.Certificate{certFoundByID}
}
toRemove, err := certStore.GetCertificatesByCN(certRemoveGUN)
toRemove, err := certStore.GetCertificatesByCN(c.certRemoveGUN)
// We could not find any certificates matching the user's query, so propagate the error
if err != nil {
fatalf("%v", err)
return fmt.Errorf("%v", err)
}
// If we specified a GUN or if the ID we specified is the only certificate with its CN, remove all GUN certs and trust data too
@ -106,48 +123,53 @@ func certRemove(cmd *cobra.Command, args []string) {
}
// If we were given a GUN or the last ID for a GUN, inform the user that we'll also delete all TUF data
if removeTrustData {
cmd.Printf("\nAll local trust data will be removed for %s\n", certRemoveGUN)
cmd.Printf("\nAll local trust data will be removed for %s\n", c.certRemoveGUN)
}
cmd.Println("\nAre you sure you want to remove these certificates? (yes/no)")
// Ask for confirmation before removing certificates, unless -y is provided
if !certRemoveYes {
if !c.certRemoveYes {
confirmed := askConfirm()
if !confirmed {
fatalf("Aborting action.")
return fmt.Errorf("Aborting action.")
}
}
if removeTrustData {
// Remove all TUF data, so call RemoveTrustData on a NotaryRepository with the GUN
// no online operations are performed so the transport argument is nil
nRepo, err := notaryclient.NewNotaryRepository(trustDir, certRemoveGUN, getRemoteTrustServer(mainViper), nil, retriever)
nRepo, err := notaryclient.NewNotaryRepository(
trustDir, c.certRemoveGUN, getRemoteTrustServer(config), nil, c.retriever)
if err != nil {
fatalf("Could not establish trust data for GUN %s", certRemoveGUN)
return fmt.Errorf("Could not establish trust data for GUN %s", c.certRemoveGUN)
}
// DeleteTrustData will pick up all of the same certificates by GUN (CN) and remove them
err = nRepo.DeleteTrustData()
if err != nil {
fatalf("Failed to delete trust data for %s", certRemoveGUN)
return fmt.Errorf("Failed to delete trust data for %s", c.certRemoveGUN)
}
} else {
for _, cert := range certsToRemove {
err = certStore.RemoveCert(cert)
if err != nil {
fatalf("Failed to remove cert %s", cert)
return fmt.Errorf("Failed to remove cert %s", cert)
}
}
}
return nil
}
func certList(cmd *cobra.Command, args []string) {
func (c *certCommander) certList(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
cmd.Usage()
os.Exit(1)
return fmt.Errorf("")
}
config, err := c.configGetter()
if err != nil {
return err
}
parseConfig()
trustDir := mainViper.GetString("trust_dir")
trustDir := config.GetString("trust_dir")
certPath := filepath.Join(trustDir, notary.TrustedCertsDir)
// Load all individual (non-CA) certificates that aren't expired and don't use SHA1
certStore, err := trustmanager.NewX509FilteredFileStore(
@ -155,7 +177,7 @@ func certList(cmd *cobra.Command, args []string) {
trustmanager.FilterCertsExpiredSha1,
)
if err != nil {
fatalf("Failed to create a new truststore with directory: %s", trustDir)
return fmt.Errorf("Failed to create a new truststore with directory: %s", trustDir)
}
trustedCerts := certStore.GetCertificates()
@ -163,4 +185,5 @@ func certList(cmd *cobra.Command, args []string) {
cmd.Println("")
prettyPrintCerts(trustedCerts, cmd.Out())
cmd.Println("")
return nil
}

View File

@ -37,13 +37,13 @@ var cmdDelegationAddTemplate = usageTemplate{
Long: "Add a keys to delegation using the provided public key PEM encoded X509 certificates in a specific Global Unique Name.",
}
var paths []string
var removeAll, removeYes bool
type delegationCommander struct {
// these need to be set
configGetter func() *viper.Viper
configGetter func() (*viper.Viper, error)
retriever passphrase.Retriever
paths []string
removeAll, removeYes bool
}
func (d *delegationCommander) GetCommand() *cobra.Command {
@ -51,12 +51,13 @@ func (d *delegationCommander) GetCommand() *cobra.Command {
cmd.AddCommand(cmdDelegationListTemplate.ToCommand(d.delegationsList))
cmdRemDelg := cmdDelegationRemoveTemplate.ToCommand(d.delegationRemove)
cmdRemDelg.Flags().StringSliceVar(&paths, "paths", nil, "List of paths to remove")
cmdRemDelg.Flags().BoolVarP(&removeYes, "yes", "y", false, "Answer yes to the removal question (no confirmation)")
cmdRemDelg.Flags().StringSliceVar(&d.paths, "paths", nil, "List of paths to remove")
cmdRemDelg.Flags().BoolVarP(
&d.removeYes, "yes", "y", false, "Answer yes to the removal question (no confirmation)")
cmd.AddCommand(cmdRemDelg)
cmdAddDelg := cmdDelegationAddTemplate.ToCommand(d.delegationAdd)
cmdAddDelg.Flags().StringSliceVar(&paths, "paths", nil, "List of paths to add")
cmdAddDelg.Flags().StringSliceVar(&d.paths, "paths", nil, "List of paths to add")
cmd.AddCommand(cmdAddDelg)
return cmd
}
@ -64,16 +65,26 @@ func (d *delegationCommander) GetCommand() *cobra.Command {
// delegationsList lists all the delegations for a particular GUN
func (d *delegationCommander) delegationsList(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
cmd.Usage()
return fmt.Errorf(
"Please provide a Global Unique Name as an argument to list")
}
config := d.configGetter()
config, err := d.configGetter()
if err != nil {
return err
}
gun := args[0]
rt, err := getTransport(config, gun, true)
if err != nil {
return err
}
// initialize repo with transport to get latest state of the world before listing delegations
nRepo, err := notaryclient.NewNotaryRepository(config.GetString("trust_dir"), gun, getRemoteTrustServer(config), getTransport(config, gun, true), retriever)
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, d.retriever)
if err != nil {
return err
}
@ -92,10 +103,14 @@ func (d *delegationCommander) delegationsList(cmd *cobra.Command, args []string)
// delegationRemove removes a public key from a specific role in a GUN
func (d *delegationCommander) delegationRemove(cmd *cobra.Command, args []string) error {
if len(args) < 2 {
cmd.Usage()
return fmt.Errorf("must specify the Global Unique Name and the role of the delegation along with optional keyIDs and/or a list of paths to remove")
}
config := d.configGetter()
config, err := d.configGetter()
if err != nil {
return err
}
gun := args[0]
role := args[1]
@ -106,14 +121,14 @@ func (d *delegationCommander) delegationRemove(cmd *cobra.Command, args []string
}
// If we're only given the gun and the role, attempt to remove all data for this delegation
if len(args) == 2 && paths == nil {
removeAll = true
if len(args) == 2 && d.paths == nil {
d.removeAll = true
}
keyIDs := []string{}
// Change nil paths to empty slice for TUF
if paths == nil {
paths = []string{}
if d.paths == nil {
d.paths = []string{}
}
if len(args) > 2 {
@ -122,15 +137,16 @@ func (d *delegationCommander) delegationRemove(cmd *cobra.Command, args []string
// no online operations are performed by add so the transport argument
// should be nil
nRepo, err := notaryclient.NewNotaryRepository(config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, retriever)
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, d.retriever)
if err != nil {
return err
}
if removeAll {
if d.removeAll {
cmd.Println("\nAre you sure you want to remove all data for this delegation? (yes/no)")
// Ask for confirmation before force removing delegation
if !removeYes {
if !d.removeYes {
confirmed := askConfirm()
if !confirmed {
fatalf("Aborting action.")
@ -145,19 +161,19 @@ func (d *delegationCommander) delegationRemove(cmd *cobra.Command, args []string
}
} else {
// Remove any keys or paths that we passed in
err = nRepo.RemoveDelegationKeysAndPaths(role, keyIDs, paths)
err = nRepo.RemoveDelegationKeysAndPaths(role, keyIDs, d.paths)
if err != nil {
return fmt.Errorf("failed to remove delegation: %v", err)
}
}
cmd.Println("")
if removeAll {
if d.removeAll {
cmd.Printf("Forced removal (including all keys and paths) of delegation role %s to repository \"%s\" staged for next publish.\n", role, gun)
} else {
cmd.Printf(
"Removal of delegation role %s with keys %s and paths %s, to repository \"%s\" staged for next publish.\n",
role, keyIDs, paths, gun)
role, keyIDs, d.paths, gun)
}
cmd.Println("")
@ -166,11 +182,15 @@ func (d *delegationCommander) delegationRemove(cmd *cobra.Command, args []string
// delegationAdd creates a new delegation by adding a public key from a certificate to a specific role in a GUN
func (d *delegationCommander) delegationAdd(cmd *cobra.Command, args []string) error {
if len(args) < 2 || len(args) < 3 && paths == nil {
if len(args) < 2 || len(args) < 3 && d.paths == nil {
cmd.Usage()
return fmt.Errorf("must specify the Global Unique Name and the role of the delegation along with the public key certificate paths and/or a list of paths to add")
}
config := d.configGetter()
config, err := d.configGetter()
if err != nil {
return err
}
gun := args[0]
role := args[1]
@ -196,13 +216,14 @@ func (d *delegationCommander) delegationAdd(cmd *cobra.Command, args []string) e
// no online operations are performed by add so the transport argument
// should be nil
nRepo, err := notaryclient.NewNotaryRepository(config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, retriever)
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, d.retriever)
if err != nil {
return err
}
// Add the delegation to the repository
err = nRepo.AddDelegation(role, pubKeys, paths)
err = nRepo.AddDelegation(role, pubKeys, d.paths)
if err != nil {
return fmt.Errorf("failed to create delegation: %v", err)
}
@ -220,7 +241,7 @@ func (d *delegationCommander) delegationAdd(cmd *cobra.Command, args []string) e
cmd.Println("")
cmd.Printf(
"Addition of delegation role %s with keys %s and paths %s, to repository \"%s\" staged for next publish.\n",
role, pubKeyIDs, paths, gun)
role, pubKeyIDs, d.paths, gun)
cmd.Println("")
return nil
}

View File

@ -3,24 +3,25 @@ package main
import (
"crypto/rand"
"crypto/x509"
"github.com/docker/notary/cryptoservice"
"github.com/docker/notary/trustmanager"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"io/ioutil"
"os"
"testing"
"time"
"github.com/docker/notary/cryptoservice"
"github.com/docker/notary/trustmanager"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
)
var testTrustDir = "trust_dir"
func setup() *delegationCommander {
return &delegationCommander{
configGetter: func() *viper.Viper {
configGetter: func() (*viper.Viper, error) {
mainViper := viper.New()
mainViper.Set("trust_dir", testTrustDir)
return mainViper
return mainViper, nil
},
retriever: nil,
}

View File

@ -6,11 +6,16 @@ import (
"testing"
"github.com/docker/notary/passphrase"
"github.com/spf13/cobra"
)
func init() {
retriever = passphrase.ConstantRetriever("pass")
getRetriever = func() passphrase.Retriever { return retriever }
NewNotaryCommand = func() *cobra.Command {
commander := &notaryCommander{
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
}
return commander.GetCommand()
}
}
func rootOnHardware() bool {

View File

@ -8,29 +8,37 @@ import (
"github.com/docker/notary/passphrase"
"github.com/docker/notary/trustmanager/yubikey"
"github.com/docker/notary/tuf/data"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)
var _retriever passphrase.Retriever
func init() {
yubikey.SetYubikeyKeyMode(yubikey.KeymodeNone)
regRetriver := passphrase.PromptRetriever()
retriever = func(k, a string, c bool, n int) (string, bool, error) {
regRetriver := passphrase.PromptRetriever()
_retriever := func(k, a string, c bool, n int) (string, bool, error) {
if k == "Yubikey" {
return regRetriver(k, a, c, n)
}
return testPassphrase, false, nil
}
getRetriever = func() passphrase.Retriever { return retriever }
// best effort at removing keys here, so nil is fine
s, err := yubikey.NewYubiKeyStore(nil, retriever)
s, err := yubikey.NewYubiKeyStore(nil, _retriever)
if err != nil {
for k := range s.ListKeys() {
s.RemoveKey(k)
}
}
NewNotaryCommand = func() *cobra.Command {
commander := &notaryCommander{
getRetriever: func() passphrase.Retriever { return _retriever },
}
return commander.GetCommand()
}
}
var rootOnHardware = yubikey.YubikeyAccessible
@ -38,7 +46,7 @@ var rootOnHardware = yubikey.YubikeyAccessible
// Per-test set up deletes all keys on the yubikey
func setUp(t *testing.T) {
//we're just removing keys here, so nil is fine
s, err := yubikey.NewYubiKeyStore(nil, retriever)
s, err := yubikey.NewYubiKeyStore(nil, _retriever)
assert.NoError(t, err)
for k := range s.ListKeys() {
err := s.RemoveKey(k)
@ -53,7 +61,7 @@ func verifyRootKeyOnHardware(t *testing.T, rootKeyID string) {
// do not bother verifying if there is no yubikey available
if yubikey.YubikeyAccessible() {
// //we're just getting keys here, so nil is fine
s, err := yubikey.NewYubiKeyStore(nil, retriever)
s, err := yubikey.NewYubiKeyStore(nil, _retriever)
assert.NoError(t, err)
privKey, role, err := s.GetKey(rootKeyID)
assert.NoError(t, err)

View File

@ -20,31 +20,28 @@ import (
"github.com/Sirupsen/logrus"
ctxu "github.com/docker/distribution/context"
"github.com/docker/notary/cryptoservice"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/server"
"github.com/docker/notary/server/storage"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/utils"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
var testPassphrase = "passphrase"
var NewNotaryCommand func() *cobra.Command
// run a command and return the output as a string
func runCommand(t *testing.T, tempDir string, args ...string) (string, error) {
// Using a new viper and Command so we don't have state between command invocations
mainViper = viper.New()
cmd := &cobra.Command{}
setupCommand(cmd)
b := new(bytes.Buffer)
// Create an empty config file so we don't load the default on ~/.notary/config.json
configFile := filepath.Join(tempDir, "config.json")
cmd := NewNotaryCommand()
cmd.SetArgs(append([]string{"-c", configFile, "-d", tempDir}, args...))
cmd.SetOutput(b)
retErr := cmd.Execute()
@ -74,7 +71,7 @@ func setupServer() *httptest.Server {
ctx = ctxu.WithLogger(ctx, logrus.NewEntry(l))
cryptoService := cryptoservice.NewCryptoService(
"", trustmanager.NewKeyMemoryStore(retriever))
"", trustmanager.NewKeyMemoryStore(passphrase.ConstantRetriever("pass")))
return httptest.NewServer(server.RootHandler(nil, ctx, cryptoService))
}
@ -90,7 +87,7 @@ func TestClientTufInteraction(t *testing.T) {
server := setupServer()
defer server.Close()
tempFile, err := ioutil.TempFile("/tmp", "targetfile")
tempFile, err := ioutil.TempFile("", "targetfile")
assert.NoError(t, err)
tempFile.Close()
defer os.Remove(tempFile.Name())
@ -162,7 +159,7 @@ func TestClientDelegationsInteraction(t *testing.T) {
defer server.Close()
// Setup certificate
tempFile, err := ioutil.TempFile("/tmp", "pemfile")
tempFile, err := ioutil.TempFile("", "pemfile")
assert.NoError(t, err)
privKey, err := trustmanager.GenerateECDSAKey(rand.Reader)
@ -229,7 +226,7 @@ func TestClientDelegationsInteraction(t *testing.T) {
assert.Contains(t, output, keyID)
// Setup another certificate
tempFile2, err := ioutil.TempFile("/tmp", "pemfile2")
tempFile2, err := ioutil.TempFile("", "pemfile2")
assert.NoError(t, err)
privKey, err = trustmanager.GenerateECDSAKey(rand.Reader)
@ -391,7 +388,7 @@ func TestClientDelegationsPublishing(t *testing.T) {
defer server.Close()
// Setup certificate for delegation role
tempFile, err := ioutil.TempFile("/tmp", "pemfile")
tempFile, err := ioutil.TempFile("", "pemfile")
assert.NoError(t, err)
privKey, err := trustmanager.GenerateRSAKey(rand.Reader, 2048)
@ -416,7 +413,7 @@ func TestClientDelegationsPublishing(t *testing.T) {
assert.NoError(t, err)
// Set up targets for publishing
tempTargetFile, err := ioutil.TempFile("/tmp", "targetfile")
tempTargetFile, err := ioutil.TempFile("", "targetfile")
assert.NoError(t, err)
tempTargetFile.Close()
defer os.Remove(tempTargetFile.Name())
@ -655,7 +652,7 @@ func TestClientKeyGenerationRotation(t *testing.T) {
tempfiles := make([]string, 2)
for i := 0; i < 2; i++ {
tempFile, err := ioutil.TempFile("/tmp", "targetfile")
tempFile, err := ioutil.TempFile("", "targetfile")
assert.NoError(t, err)
tempFile.Close()
tempfiles[i] = tempFile.Name()
@ -736,7 +733,7 @@ func TestClientKeyBackupAndRestore(t *testing.T) {
tempfiles := make([]string, 2)
for i := 0; i < 2; i++ {
tempFile, err := ioutil.TempFile("/tmp", "tempfile")
tempFile, err := ioutil.TempFile("", "tempfile")
assert.NoError(t, err)
tempFile.Close()
tempfiles[i] = tempFile.Name()
@ -815,10 +812,13 @@ func exportRoot(t *testing.T, exportTo string) string {
oldRoot, _ := assertNumKeys(t, tempDir, 1, 0, true)
// export does not require a password
oldRetriever := retriever
retriever = nil
oldNewCommand := NewNotaryCommand
NewNotaryCommand = func() *cobra.Command {
commander := &notaryCommander{getRetriever: func() passphrase.Retriever { return nil }}
return commander.GetCommand()
}
defer func() { // but import will, later
retriever = oldRetriever
NewNotaryCommand = oldNewCommand
}()
_, err = runCommand(
@ -844,7 +844,7 @@ func TestClientKeyImportExportRootOnly(t *testing.T) {
rootKeyID string
)
tempFile, err := ioutil.TempFile("/tmp", "pemfile")
tempFile, err := ioutil.TempFile("", "pemfile")
assert.NoError(t, err)
// close later, because we might need to write to it
defer os.Remove(tempFile.Name())
@ -1001,22 +1001,23 @@ func TestDefaultRootKeyGeneration(t *testing.T) {
// Tests the interaction with the verbose and log-level flags
func TestLogLevelFlags(t *testing.T) {
// Test default to fatal
setVerbosityLevel()
n := notaryCommander{}
n.setVerbosityLevel()
assert.Equal(t, "fatal", logrus.GetLevel().String())
// Test that verbose (-v) sets to error
verbose = true
setVerbosityLevel()
n.verbose = true
n.setVerbosityLevel()
assert.Equal(t, "error", logrus.GetLevel().String())
// Test that debug (-D) sets to debug
debug = true
setVerbosityLevel()
n.debug = true
n.setVerbosityLevel()
assert.Equal(t, "debug", logrus.GetLevel().String())
// Test that unsetting verboseError still uses verboseDebug
verbose = false
setVerbosityLevel()
n.verbose = false
n.setVerbosityLevel()
assert.Equal(t, "debug", logrus.GetLevel().String())
}
@ -1051,7 +1052,7 @@ func TestClientKeyPassphraseChange(t *testing.T) {
}
func tempDirWithConfig(t *testing.T, config string) string {
tempDir, err := ioutil.TempDir("/tmp", "repo")
tempDir, err := ioutil.TempDir("", "repo")
assert.NoError(t, err)
err = ioutil.WriteFile(filepath.Join(tempDir, "config.json"), []byte(config), 0644)
assert.NoError(t, err)

View File

@ -22,34 +22,6 @@ import (
"github.com/spf13/viper"
)
type usageTemplate struct {
Use string
Short string
Long string
}
type cobraRunE func(cmd *cobra.Command, args []string) error
func (u usageTemplate) ToCommand(run cobraRunE) *cobra.Command {
c := cobra.Command{
Use: u.Use,
Short: u.Short,
Long: u.Long,
}
if run != nil {
// newer versions of cobra support a run function that returns an error,
// but in the meantime, this should help ease the transition
c.Run = func(cmd *cobra.Command, args []string) {
err := run(cmd, args)
if err != nil {
cmd.Usage()
fatalf(err.Error())
}
}
}
return &c
}
var cmdKeyTemplate = usageTemplate{
Use: "key",
Short: "Operates on keys.",
@ -112,8 +84,8 @@ var cmdKeyPasswdTemplate = usageTemplate{
type keyCommander struct {
// these need to be set
configGetter func() *viper.Viper
retriever passphrase.Retriever
configGetter func() (*viper.Viper, error)
getRetriever func() passphrase.Retriever
// these are for command line parsing - no need to set
keysExportRootChangePassphrase bool
@ -158,10 +130,14 @@ func (k *keyCommander) GetCommand() *cobra.Command {
func (k *keyCommander) keysList(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
cmd.Usage()
return fmt.Errorf("")
}
config := k.configGetter()
config, err := k.configGetter()
if err != nil {
return err
}
ks, err := k.getKeyStores(config, true)
if err != nil {
return err
@ -177,6 +153,7 @@ func (k *keyCommander) keysGenerateRootKey(cmd *cobra.Command, args []string) er
// We require one or no arguments (since we have a default value), but if the
// user passes in more than one argument, we error out.
if len(args) > 1 {
cmd.Usage()
return fmt.Errorf(
"Please provide only one Algorithm as an argument to generate (rsa, ecdsa)")
}
@ -198,7 +175,10 @@ func (k *keyCommander) keysGenerateRootKey(cmd *cobra.Command, args []string) er
return fmt.Errorf("Algorithm not allowed, possible values are: RSA, ECDSA")
}
config := k.configGetter()
config, err := k.configGetter()
if err != nil {
return err
}
ks, err := k.getKeyStores(config, true)
if err != nil {
return err
@ -217,10 +197,14 @@ func (k *keyCommander) keysGenerateRootKey(cmd *cobra.Command, args []string) er
// keysBackup exports a collection of keys to a ZIP file
func (k *keyCommander) keysBackup(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
cmd.Usage()
return fmt.Errorf("Must specify output filename for export")
}
config := k.configGetter()
config, err := k.configGetter()
if err != nil {
return err
}
ks, err := k.getKeyStores(config, false)
if err != nil {
return err
@ -236,7 +220,7 @@ func (k *keyCommander) keysBackup(cmd *cobra.Command, args []string) error {
// Must use a different passphrase retriever to avoid caching the
// unlocking passphrase and reusing that.
exportRetriever := getRetriever()
exportRetriever := k.getRetriever()
if k.keysExportGUN != "" {
err = cs.ExportKeysByGUN(exportFile, k.keysExportGUN, exportRetriever)
} else {
@ -255,6 +239,7 @@ func (k *keyCommander) keysBackup(cmd *cobra.Command, args []string) error {
// keysExportRoot exports a root key by ID to a PEM file
func (k *keyCommander) keysExportRoot(cmd *cobra.Command, args []string) error {
if len(args) < 2 {
cmd.Usage()
return fmt.Errorf("Must specify key ID and output filename for export")
}
@ -265,7 +250,10 @@ func (k *keyCommander) keysExportRoot(cmd *cobra.Command, args []string) error {
return fmt.Errorf("Please specify a valid root key ID")
}
config := k.configGetter()
config, err := k.configGetter()
if err != nil {
return err
}
ks, err := k.getKeyStores(config, true)
if err != nil {
return err
@ -279,7 +267,7 @@ func (k *keyCommander) keysExportRoot(cmd *cobra.Command, args []string) error {
if k.keysExportRootChangePassphrase {
// Must use a different passphrase retriever to avoid caching the
// unlocking passphrase and reusing that.
exportRetriever := getRetriever()
exportRetriever := k.getRetriever()
err = cs.ExportRootKeyReencrypt(exportFile, keyID, exportRetriever)
} else {
err = cs.ExportRootKey(exportFile, keyID)
@ -295,12 +283,16 @@ func (k *keyCommander) keysExportRoot(cmd *cobra.Command, args []string) error {
// keysRestore imports keys from a ZIP file
func (k *keyCommander) keysRestore(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
cmd.Usage()
return fmt.Errorf("Must specify input filename for import")
}
importFilename := args[0]
config := k.configGetter()
config, err := k.configGetter()
if err != nil {
return err
}
ks, err := k.getKeyStores(config, true)
if err != nil {
return err
@ -324,10 +316,14 @@ func (k *keyCommander) keysRestore(cmd *cobra.Command, args []string) error {
// keysImportRoot imports a root key from a PEM file
func (k *keyCommander) keysImportRoot(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
cmd.Usage()
return fmt.Errorf("Must specify input filename for import")
}
config := k.configGetter()
config, err := k.configGetter()
if err != nil {
return err
}
ks, err := k.getKeyStores(config, true)
if err != nil {
return err
@ -352,6 +348,7 @@ func (k *keyCommander) keysImportRoot(cmd *cobra.Command, args []string) error {
func (k *keyCommander) keysRotate(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
cmd.Usage()
return fmt.Errorf("Must specify a GUN")
}
rotateKeyRole := strings.ToLower(k.rotateKeyRole)
@ -372,18 +369,24 @@ func (k *keyCommander) keysRotate(cmd *cobra.Command, args []string) error {
"remote signing/key management is only supported for the snapshot key")
}
config := k.configGetter()
config, err := k.configGetter()
if err != nil {
return err
}
gun := args[0]
var rt http.RoundTripper
if k.rotateKeyServerManaged {
// this does not actually push the changes, just creates the keys, but
// it creates a key remotely so it needs a transport
rt = getTransport(config, gun, false)
rt, err = getTransport(config, gun, false)
if err != nil {
return err
}
}
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config),
rt, k.retriever)
rt, k.getRetriever())
if err != nil {
return err
}
@ -470,10 +473,14 @@ func removeKeyInteractively(keyStores []trustmanager.KeyStore, keyID string,
// keyRemove deletes a private key based on ID
func (k *keyCommander) keyRemove(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
cmd.Usage()
return fmt.Errorf("must specify the key ID of the key to remove")
}
config := k.configGetter()
config, err := k.configGetter()
if err != nil {
return err
}
ks, err := k.getKeyStores(config, true)
if err != nil {
return err
@ -494,10 +501,14 @@ func (k *keyCommander) keyRemove(cmd *cobra.Command, args []string) error {
// keyPassphraseChange changes the passphrase for a root key's private key based on ID
func (k *keyCommander) keyPassphraseChange(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
cmd.Usage()
return fmt.Errorf("must specify the key ID of the root key to change the passphrase of")
}
config := k.configGetter()
config, err := k.configGetter()
if err != nil {
return err
}
ks, err := k.getKeyStores(config, true)
if err != nil {
return err
@ -519,7 +530,7 @@ func (k *keyCommander) keyPassphraseChange(cmd *cobra.Command, args []string) er
// Must use a different passphrase retriever to avoid caching the
// unlocking passphrase and reusing that.
passChangeRetriever := getRetriever()
passChangeRetriever := k.getRetriever()
keyStore, err := trustmanager.NewKeyFileStore(config.GetString("trust_dir"), passChangeRetriever)
err = keyStore.AddKey(keyID, role, privKey)
if err != nil {
@ -534,9 +545,10 @@ func (k *keyCommander) keyPassphraseChange(cmd *cobra.Command, args []string) er
func (k *keyCommander) getKeyStores(
config *viper.Viper, withHardware bool) ([]trustmanager.KeyStore, error) {
retriever := k.getRetriever()
directory := config.GetString("trust_dir")
fileKeyStore, err := trustmanager.NewKeyFileStore(directory, k.retriever)
fileKeyStore, err := trustmanager.NewKeyFileStore(directory, retriever)
if err != nil {
return nil, fmt.Errorf(
"Failed to create private key store in directory: %s", directory)
@ -545,7 +557,7 @@ func (k *keyCommander) getKeyStores(
ks := []trustmanager.KeyStore{fileKeyStore}
if withHardware {
yubiStore, err := getYubiKeyStore(fileKeyStore, k.retriever)
yubiStore, err := getYubiKeyStore(fileKeyStore, retriever)
if err == nil && yubiStore != nil {
// Note that the order is important, since we want to prioritize
// the yubikey store

View File

@ -237,8 +237,8 @@ func TestRotateKeyInvalidRoles(t *testing.T) {
for _, role := range invalids {
for _, serverManaged := range []bool{true, false} {
k := &keyCommander{
configGetter: viper.New,
retriever: passphrase.ConstantRetriever("pass"),
configGetter: func() (*viper.Viper, error) { return viper.New(), nil },
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
rotateKeyRole: role,
rotateKeyServerManaged: serverManaged,
}
@ -253,8 +253,8 @@ func TestRotateKeyInvalidRoles(t *testing.T) {
// Cannot rotate a targets key and require that the server manage it
func TestRotateKeyTargetCannotBeServerManaged(t *testing.T) {
k := &keyCommander{
configGetter: viper.New,
retriever: passphrase.ConstantRetriever("pass"),
configGetter: func() (*viper.Viper, error) { return viper.New(), nil },
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
rotateKeyRole: data.CanonicalTargetsRole,
rotateKeyServerManaged: true,
}
@ -267,8 +267,8 @@ func TestRotateKeyTargetCannotBeServerManaged(t *testing.T) {
// rotate key must be provided with a gun
func TestRotateKeyNoGUN(t *testing.T) {
k := &keyCommander{
configGetter: viper.New,
retriever: passphrase.ConstantRetriever("pass"),
configGetter: func() (*viper.Viper, error) { return viper.New(), nil },
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
rotateKeyRole: data.CanonicalTargetsRole,
}
err := k.keysRotate(&cobra.Command{}, []string{})
@ -321,13 +321,13 @@ func TestRotateKeyRemoteServerManagesKey(t *testing.T) {
defer ts.Close()
k := &keyCommander{
configGetter: func() *viper.Viper {
configGetter: func() (*viper.Viper, error) {
v := viper.New()
v.SetDefault("trust_dir", tempBaseDir)
v.SetDefault("remote_server.url", ts.URL)
return v
return v, nil
},
retriever: ret,
getRetriever: func() passphrase.Retriever { return ret },
rotateKeyRole: data.CanonicalSnapshotRole,
rotateKeyServerManaged: true,
}
@ -361,13 +361,13 @@ func TestRotateKeyBothKeys(t *testing.T) {
ts.Close()
k := &keyCommander{
configGetter: func() *viper.Viper {
configGetter: func() (*viper.Viper, error) {
v := viper.New()
v.SetDefault("trust_dir", tempBaseDir)
// won't need a remote server URL, since we are creating local keys
return v
return v, nil
},
retriever: ret,
getRetriever: func() passphrase.Retriever { return ret },
}
err = k.keysRotate(&cobra.Command{}, []string{gun})
assert.NoError(t, err)
@ -405,8 +405,8 @@ func TestRotateKeyBothKeys(t *testing.T) {
func TestChangeKeyPassphraseInvalidID(t *testing.T) {
k := &keyCommander{
configGetter: viper.New,
retriever: passphrase.ConstantRetriever("pass"),
configGetter: func() (*viper.Viper, error) { return viper.New(), nil },
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
}
err := k.keyPassphraseChange(&cobra.Command{}, []string{"too_short"})
assert.Error(t, err)
@ -415,8 +415,8 @@ func TestChangeKeyPassphraseInvalidID(t *testing.T) {
func TestChangeKeyPassphraseInvalidNumArgs(t *testing.T) {
k := &keyCommander{
configGetter: viper.New,
retriever: passphrase.ConstantRetriever("pass"),
configGetter: func() (*viper.Viper, error) { return viper.New(), nil },
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
}
err := k.keyPassphraseChange(&cobra.Command{}, []string{})
assert.Error(t, err)
@ -425,8 +425,8 @@ func TestChangeKeyPassphraseInvalidNumArgs(t *testing.T) {
func TestChangeKeyPassphraseNonexistentID(t *testing.T) {
k := &keyCommander{
configGetter: viper.New,
retriever: passphrase.ConstantRetriever("pass"),
configGetter: func() (*viper.Viper, error) { return viper.New(), nil },
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
}
// Valid ID size, but does not exist as a key ID
err := k.keyPassphraseChange(&cobra.Command{}, []string{strings.Repeat("x", notary.Sha256HexSize)})

View File

@ -19,136 +19,166 @@ const (
defaultServerURL = "https://notary-server:4443"
)
var (
type usageTemplate struct {
Use string
Short string
Long string
}
type cobraRunE func(cmd *cobra.Command, args []string) error
func (u usageTemplate) ToCommand(run cobraRunE) *cobra.Command {
c := cobra.Command{
Use: u.Use,
Short: u.Short,
Long: u.Long,
}
if run != nil {
// newer versions of cobra support a run function that returns an error,
// but in the meantime, this should help ease the transition
c.RunE = run
}
return &c
}
type notaryCommander struct {
// this needs to be set
getRetriever func() passphrase.Retriever
// these are for command line parsing - no need to set
debug bool
verbose bool
roles []string
trustDir string
configFile string
remoteTrustServer string
configPath string
configFileName = "config"
configFileExt = "json"
retriever passphrase.Retriever
getRetriever = getPassphraseRetriever
mainViper = viper.New()
)
func init() {
retriever = getPassphraseRetriever()
}
func parseConfig() *viper.Viper {
setVerbosityLevel()
func (n *notaryCommander) parseConfig() (*viper.Viper, error) {
n.setVerbosityLevel()
// Get home directory for current user
homeDir, err := homedir.Dir()
if err != nil {
fatalf("Cannot get current user home directory: %v", err)
return nil, fmt.Errorf("cannot get current user home directory: %v", err)
}
if homeDir == "" {
fatalf("Cannot get current user home directory")
return nil, fmt.Errorf("cannot get current user home directory")
}
config := viper.New()
// By default our trust directory (where keys are stored) is in ~/.notary/
mainViper.SetDefault("trust_dir", filepath.Join(homeDir, filepath.Dir(configDir)))
defaultTrustDir := filepath.Join(homeDir, filepath.Dir(configDir))
// If there was a commandline configFile set, we parse that.
// If there wasn't we attempt to find it on the default location ~/.notary/config
if configFile != "" {
configFileExt = strings.TrimPrefix(filepath.Ext(configFile), ".")
configFileName = strings.TrimSuffix(filepath.Base(configFile), filepath.Ext(configFile))
configPath = filepath.Dir(configFile)
} else {
configPath = filepath.Join(homeDir, filepath.Dir(configDir))
// If there wasn't we attempt to find it on the default location ~/.notary/config.json
configFileName, configFileExt, configPath := "config", "json", defaultTrustDir
if n.configFile != "" {
configFileExt = strings.TrimPrefix(filepath.Ext(n.configFile), ".")
configFileName = strings.TrimSuffix(filepath.Base(n.configFile), filepath.Ext(n.configFile))
configPath = filepath.Dir(n.configFile)
}
// Setup the configuration details into viper
mainViper.SetConfigName(configFileName)
mainViper.SetConfigType(configFileExt)
mainViper.AddConfigPath(configPath)
config.SetConfigName(configFileName)
config.SetConfigType(configFileExt)
config.AddConfigPath(configPath)
config.SetDefault("trust_dir", defaultTrustDir)
config.SetDefault("remote_server", map[string]string{"url": defaultServerURL})
// Find and read the config file
err = mainViper.ReadInConfig()
if err != nil {
if err := config.ReadInConfig(); err != nil {
logrus.Debugf("Configuration file not found, using defaults")
// If we were passed in a configFile via -c, bail if it doesn't exist,
// If we were passed in a configFile via command linen flags, bail if it doesn't exist,
// otherwise ignore it: we can use the defaults
if configFile != "" || !os.IsNotExist(err) {
fatalf("error opening config file %v", err)
if n.configFile != "" || !os.IsNotExist(err) {
return nil, fmt.Errorf("error opening config file: %v", err)
}
}
// At this point we either have the default value or the one set by the config.
// Either way, the command-line flag has precedence and overwrites the value
if trustDir != "" {
mainViper.Set("trust_dir", trustDir)
// Either way, some command-line flags have precedence and overwrites the value
if n.trustDir != "" {
config.Set("trust_dir", n.trustDir)
}
if n.remoteTrustServer != "" {
config.Set("remote_server.url", n.remoteTrustServer)
}
// Expands all the possible ~/ that have been given, either through -d or config
// If there is no error, use it, if not, attempt to use whatever the user gave us
expandedTrustDir, err := homedir.Expand(mainViper.GetString("trust_dir"))
// If there is no error, use it, if not, just attempt to use whatever the user gave us
expandedTrustDir, err := homedir.Expand(config.GetString("trust_dir"))
if err == nil {
mainViper.Set("trust_dir", expandedTrustDir)
config.Set("trust_dir", expandedTrustDir)
}
logrus.Debugf("Using the following trust directory: %s", mainViper.GetString("trust_dir"))
logrus.Debugf("Using the following trust directory: %s", config.GetString("trust_dir"))
return mainViper
return config, nil
}
func setupCommand(notaryCmd *cobra.Command) {
var versionCmd = &cobra.Command{
func (n *notaryCommander) GetCommand() *cobra.Command {
notaryCmd := cobra.Command{
Use: "notary",
Short: "Notary allows the creation of trusted collections.",
Long: "Notary allows the creation and management of collections of signed targets, allowing the signing and validation of arbitrary content.",
SilenceUsage: true,
SilenceErrors: true,
}
notaryCmd.SetOutput(os.Stdout)
notaryCmd.AddCommand(&cobra.Command{
Use: "version",
Short: "Print the version number of notary",
Long: `print the version number of notary`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("notary\n Version: %s\n Git commit: %s\n", version.NotaryVersion, version.GitCommit)
},
}
})
notaryCmd.AddCommand(versionCmd)
notaryCmd.PersistentFlags().StringVarP(&trustDir, "trustDir", "d", "", "Directory where the trust data is persisted to")
notaryCmd.PersistentFlags().StringVarP(&configFile, "configFile", "c", "", "Path to the configuration file to use")
notaryCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output")
notaryCmd.PersistentFlags().BoolVarP(&debug, "debug", "D", false, "Debug output")
notaryCmd.PersistentFlags().StringVarP(&remoteTrustServer, "server", "s", "", "Remote trust server location")
notaryCmd.PersistentFlags().StringVarP(
&n.trustDir, "trustDir", "d", "", "Directory where the trust data is persisted to")
notaryCmd.PersistentFlags().StringVarP(
&n.configFile, "configFile", "c", "", "Path to the configuration file to use")
notaryCmd.PersistentFlags().BoolVarP(&n.verbose, "verbose", "v", false, "Verbose output")
notaryCmd.PersistentFlags().BoolVarP(&n.debug, "debug", "D", false, "Debug output")
notaryCmd.PersistentFlags().StringVarP(&n.remoteTrustServer, "server", "s", "", "Remote trust server location")
cmdKeyGenerator := &keyCommander{
configGetter: parseConfig,
retriever: retriever,
configGetter: n.parseConfig,
getRetriever: n.getRetriever,
}
cmdDelegationGenerator := &delegationCommander{
configGetter: parseConfig,
retriever: retriever,
configGetter: n.parseConfig,
retriever: n.getRetriever(),
}
cmdCertGenerator := &certCommander{
configGetter: n.parseConfig,
retriever: n.getRetriever(),
}
cmdTufGenerator := &tufCommander{
configGetter: n.parseConfig,
retriever: n.getRetriever(),
}
notaryCmd.AddCommand(cmdKeyGenerator.GetCommand())
notaryCmd.AddCommand(cmdDelegationGenerator.GetCommand())
notaryCmd.AddCommand(cmdCert)
notaryCmd.AddCommand(cmdTufInit)
cmdTufList.Flags().StringSliceVarP(&roles, "roles", "r", nil, "Delegation roles to list targets for (will shadow targets role)")
notaryCmd.AddCommand(cmdTufList)
cmdTufAdd.Flags().StringSliceVarP(&roles, "roles", "r", nil, "Delegation roles to add this target to")
notaryCmd.AddCommand(cmdTufAdd)
cmdTufRemove.Flags().StringSliceVarP(&roles, "roles", "r", nil, "Delegation roles to remove this target from")
notaryCmd.AddCommand(cmdTufRemove)
notaryCmd.AddCommand(cmdTufStatus)
notaryCmd.AddCommand(cmdTufPublish)
notaryCmd.AddCommand(cmdTufLookup)
notaryCmd.AddCommand(cmdTufVerify)
notaryCmd.AddCommand(cmdCertGenerator.GetCommand())
cmdTufGenerator.AddToCommand(&notaryCmd)
return &notaryCmd
}
func main() {
var notaryCmd = &cobra.Command{
Use: "notary",
Short: "Notary allows the creation of trusted collections.",
Long: "Notary allows the creation and management of collections of signed targets, allowing the signing and validation of arbitrary content.",
notaryCommander := &notaryCommander{getRetriever: getPassphraseRetriever}
notaryCmd := notaryCommander.GetCommand()
if err := notaryCmd.Execute(); err != nil {
notaryCmd.Println("")
fatalf(err.Error())
}
notaryCmd.SetOutput(os.Stdout)
setupCommand(notaryCmd)
notaryCmd.Execute()
}
func fatalf(format string, args ...interface{}) {
@ -185,10 +215,10 @@ func getPassphraseRetriever() passphrase.Retriever {
}
// Set the logging level to fatal on default, or the most specific level the user specified (debug or error)
func setVerbosityLevel() {
if debug {
func (n *notaryCommander) setVerbosityLevel() {
if n.debug {
logrus.SetLevel(logrus.DebugLevel)
} else if verbose {
} else if n.verbose {
logrus.SetLevel(logrus.ErrorLevel)
} else {
logrus.SetLevel(logrus.FatalLevel)

View File

@ -9,7 +9,6 @@ import (
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
@ -20,74 +19,100 @@ import (
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/docker/pkg/term"
notaryclient "github.com/docker/notary/client"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/utils"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cmdTufList = &cobra.Command{
var cmdTufListTemplate = usageTemplate{
Use: "list [ GUN ]",
Short: "Lists targets for a remote trusted collection.",
Long: "Lists all targets for a remote trusted collection identified by the Globally Unique Name. This is an online operation.",
Run: tufList,
}
var cmdTufAdd = &cobra.Command{
var cmdTufAddTemplate = usageTemplate{
Use: "add [ GUN ] <target> <file>",
Short: "Adds the file as a target to the trusted collection.",
Long: "Adds the file as a target to the local trusted collection identified by the Globally Unique Name. This is an offline operation. Please then use `publish` to push the changes to the remote trusted collection.",
Run: tufAdd,
}
var cmdTufRemove = &cobra.Command{
var cmdTufRemoveTemplate = usageTemplate{
Use: "remove [ GUN ] <target>",
Short: "Removes a target from a trusted collection.",
Long: "Removes a target from the local trusted collection identified by the Globally Unique Name. This is an offline operation. Please then use `publish` to push the changes to the remote trusted collection.",
Run: tufRemove,
}
var cmdTufInit = &cobra.Command{
var cmdTufInitTemplate = usageTemplate{
Use: "init [ GUN ]",
Short: "Initializes a local trusted collection.",
Long: "Initializes a local trusted collection identified by the Globally Unique Name. This is an online operation.",
Run: tufInit,
}
var cmdTufLookup = &cobra.Command{
var cmdTufLookupTemplate = usageTemplate{
Use: "lookup [ GUN ] <target>",
Short: "Looks up a specific target in a remote trusted collection.",
Long: "Looks up a specific target in a remote trusted collection identified by the Globally Unique Name.",
Run: tufLookup,
}
var cmdTufPublish = &cobra.Command{
var cmdTufPublishTemplate = usageTemplate{
Use: "publish [ GUN ]",
Short: "Publishes the local trusted collection.",
Long: "Publishes the local trusted collection identified by the Globally Unique Name, sending the local changes to a remote trusted server.",
Run: tufPublish,
}
var cmdTufStatus = &cobra.Command{
var cmdTufStatusTemplate = usageTemplate{
Use: "status [ GUN ]",
Short: "Displays status of unpublished changes to the local trusted collection.",
Long: "Displays status of unpublished changes to the local trusted collection identified by the Globally Unique Name.",
Run: tufStatus,
}
var cmdTufVerify = &cobra.Command{
var cmdTufVerifyTemplate = usageTemplate{
Use: "verify [ GUN ] <target>",
Short: "Verifies if the content is included in the remote trusted collection",
Long: "Verifies if the data passed in STDIN is included in the remote trusted collection identified by the Global Unique Name.",
Run: tufVerify,
}
func tufAdd(cmd *cobra.Command, args []string) {
type tufCommander struct {
// these need to be set
configGetter func() (*viper.Viper, error)
retriever passphrase.Retriever
// these are for command line parsing - no need to set
roles []string
}
func (t *tufCommander) AddToCommand(cmd *cobra.Command) {
cmd.AddCommand(cmdTufInitTemplate.ToCommand(t.tufInit))
cmd.AddCommand(cmdTufStatusTemplate.ToCommand(t.tufStatus))
cmd.AddCommand(cmdTufPublishTemplate.ToCommand(t.tufPublish))
cmd.AddCommand(cmdTufLookupTemplate.ToCommand(t.tufLookup))
cmd.AddCommand(cmdTufVerifyTemplate.ToCommand(t.tufVerify))
cmdTufList := cmdTufListTemplate.ToCommand(t.tufList)
cmdTufList.Flags().StringSliceVarP(
&t.roles, "roles", "r", nil, "Delegation roles to list targets for (will shadow targets role)")
cmd.AddCommand(cmdTufList)
cmdTufAdd := cmdTufAddTemplate.ToCommand(t.tufAdd)
cmdTufAdd.Flags().StringSliceVarP(&t.roles, "roles", "r", nil, "Delegation roles to add this target to")
cmd.AddCommand(cmdTufAdd)
cmdTufRemove := cmdTufRemoveTemplate.ToCommand(t.tufRemove)
cmdTufRemove.Flags().StringSliceVarP(&t.roles, "roles", "r", nil, "Delegation roles to remove this target from")
cmd.AddCommand(cmdTufRemove)
}
func (t *tufCommander) tufAdd(cmd *cobra.Command, args []string) error {
if len(args) < 3 {
cmd.Usage()
fatalf("Must specify a GUN, target, and path to target data")
return fmt.Errorf("Must specify a GUN, target, and path to target data")
}
config, err := t.configGetter()
if err != nil {
return err
}
parseConfig()
gun := args[0]
targetName := args[1]
@ -95,37 +120,47 @@ func tufAdd(cmd *cobra.Command, args []string) {
// no online operations are performed by add so the transport argument
// should be nil
nRepo, err := notaryclient.NewNotaryRepository(mainViper.GetString("trust_dir"), gun, getRemoteTrustServer(mainViper), nil, retriever)
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever)
if err != nil {
fatalf(err.Error())
return err
}
target, err := notaryclient.NewTarget(targetName, targetPath)
if err != nil {
fatalf(err.Error())
return err
}
// If roles is empty, we default to adding to targets
err = nRepo.AddTarget(target, roles...)
if err != nil {
fatalf(err.Error())
if err = nRepo.AddTarget(target, t.roles...); err != nil {
return err
}
cmd.Printf(
"Addition of target \"%s\" to repository \"%s\" staged for next publish.\n",
targetName, gun)
return nil
}
func tufInit(cmd *cobra.Command, args []string) {
func (t *tufCommander) tufInit(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
cmd.Usage()
fatalf("Must specify a GUN")
return fmt.Errorf("Must specify a GUN")
}
parseConfig()
config, err := t.configGetter()
if err != nil {
return err
}
gun := args[0]
nRepo, err := notaryclient.NewNotaryRepository(mainViper.GetString("trust_dir"), gun, getRemoteTrustServer(mainViper), getTransport(mainViper, gun, false), retriever)
rt, err := getTransport(config, gun, false)
if err != nil {
fatalf(err.Error())
return err
}
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever)
if err != nil {
return err
}
rootKeyList := nRepo.CryptoService.ListKeys(data.CanonicalRootRole)
@ -136,7 +171,7 @@ func tufInit(cmd *cobra.Command, args []string) {
rootPublicKey, err := nRepo.CryptoService.Create(data.CanonicalRootRole, data.ECDSAKey)
rootKeyID = rootPublicKey.ID()
if err != nil {
fatalf(err.Error())
return err
}
} else {
// Choses the first root key available, which is initialization specific
@ -145,80 +180,104 @@ func tufInit(cmd *cobra.Command, args []string) {
cmd.Printf("Root key found, using: %s\n", rootKeyID)
}
err = nRepo.Initialize(rootKeyID)
if err != nil {
fatalf(err.Error())
if err = nRepo.Initialize(rootKeyID); err != nil {
return err
}
return nil
}
func tufList(cmd *cobra.Command, args []string) {
func (t *tufCommander) tufList(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
cmd.Usage()
fatalf("Must specify a GUN")
return fmt.Errorf("Must specify a GUN")
}
config, err := t.configGetter()
if err != nil {
return err
}
parseConfig()
gun := args[0]
nRepo, err := notaryclient.NewNotaryRepository(mainViper.GetString("trust_dir"), gun, getRemoteTrustServer(mainViper), getTransport(mainViper, gun, true), retriever)
rt, err := getTransport(config, gun, true)
if err != nil {
fatalf(err.Error())
return err
}
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever)
if err != nil {
return err
}
// Retrieve the remote list of signed targets, prioritizing the passed-in list over targets
roles = append(roles, data.CanonicalTargetsRole)
roles := append(t.roles, data.CanonicalTargetsRole)
targetList, err := nRepo.ListTargets(roles...)
if err != nil {
fatalf(err.Error())
return err
}
prettyPrintTargets(targetList, cmd.Out())
return nil
}
func tufLookup(cmd *cobra.Command, args []string) {
func (t *tufCommander) tufLookup(cmd *cobra.Command, args []string) error {
if len(args) < 2 {
cmd.Usage()
fatalf("Must specify a GUN and target")
return fmt.Errorf("Must specify a GUN and target")
}
config, err := t.configGetter()
if err != nil {
return err
}
parseConfig()
gun := args[0]
targetName := args[1]
nRepo, err := notaryclient.NewNotaryRepository(mainViper.GetString("trust_dir"), gun, getRemoteTrustServer(mainViper), getTransport(mainViper, gun, true), retriever)
rt, err := getTransport(config, gun, true)
if err != nil {
fatalf(err.Error())
return err
}
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever)
if err != nil {
return err
}
target, err := nRepo.GetTargetByName(targetName)
if err != nil {
fatalf(err.Error())
return err
}
cmd.Println(target.Name, fmt.Sprintf("sha256:%x", target.Hashes["sha256"]), target.Length)
return nil
}
func tufStatus(cmd *cobra.Command, args []string) {
func (t *tufCommander) tufStatus(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
cmd.Usage()
fatalf("Must specify a GUN")
return fmt.Errorf("Must specify a GUN")
}
parseConfig()
config, err := t.configGetter()
if err != nil {
return err
}
gun := args[0]
nRepo, err := notaryclient.NewNotaryRepository(mainViper.GetString("trust_dir"), gun, getRemoteTrustServer(mainViper), nil, retriever)
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever)
if err != nil {
fatalf(err.Error())
return err
}
cl, err := nRepo.GetChangelist()
if err != nil {
fatalf(err.Error())
return err
}
if len(cl.List()) == 0 {
cmd.Printf("No unpublished changes for %s\n", gun)
return
return nil
}
cmd.Printf("Unpublished changes for %s:\n\n", gun)
@ -227,80 +286,102 @@ func tufStatus(cmd *cobra.Command, args []string) {
for _, ch := range cl.List() {
cmd.Printf("%-10s%-10s%-12s%s\n", ch.Action(), ch.Scope(), ch.Type(), ch.Path())
}
return nil
}
func tufPublish(cmd *cobra.Command, args []string) {
func (t *tufCommander) tufPublish(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
cmd.Usage()
fatalf("Must specify a GUN")
return fmt.Errorf("Must specify a GUN")
}
parseConfig()
config, err := t.configGetter()
if err != nil {
return err
}
gun := args[0]
cmd.Println("Pushing changes to", gun)
nRepo, err := notaryclient.NewNotaryRepository(mainViper.GetString("trust_dir"), gun, getRemoteTrustServer(mainViper), getTransport(mainViper, gun, false), retriever)
rt, err := getTransport(config, gun, false)
if err != nil {
fatalf(err.Error())
return err
}
err = nRepo.Publish()
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever)
if err != nil {
fatalf(err.Error())
return err
}
if err = nRepo.Publish(); err != nil {
return err
}
return nil
}
func tufRemove(cmd *cobra.Command, args []string) {
func (t *tufCommander) tufRemove(cmd *cobra.Command, args []string) error {
if len(args) < 2 {
cmd.Usage()
fatalf("Must specify a GUN and target")
return fmt.Errorf("Must specify a GUN and target")
}
config, err := t.configGetter()
if err != nil {
return err
}
parseConfig()
gun := args[0]
targetName := args[1]
// no online operation are performed by remove so the transport argument
// should be nil.
repo, err := notaryclient.NewNotaryRepository(mainViper.GetString("trust_dir"), gun, getRemoteTrustServer(mainViper), nil, retriever)
repo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever)
if err != nil {
fatalf(err.Error())
return err
}
// If roles is empty, we default to removing from targets
err = repo.RemoveTarget(targetName, roles...)
if err != nil {
fatalf(err.Error())
if err = repo.RemoveTarget(targetName, t.roles...); err != nil {
return err
}
cmd.Printf("Removal of %s from %s staged for next publish.\n", targetName, gun)
return nil
}
func tufVerify(cmd *cobra.Command, args []string) {
func (t *tufCommander) tufVerify(cmd *cobra.Command, args []string) error {
if len(args) < 2 {
cmd.Usage()
fatalf("Must specify a GUN and target")
return fmt.Errorf("Must specify a GUN and target")
}
parseConfig()
config, err := t.configGetter()
if err != nil {
return err
}
// Reads all of the data on STDIN
payload, err := ioutil.ReadAll(os.Stdin)
if err != nil {
fatalf("Error reading content from STDIN: %v", err)
return fmt.Errorf("Error reading content from STDIN: %v", err)
}
gun := args[0]
targetName := args[1]
nRepo, err := notaryclient.NewNotaryRepository(mainViper.GetString("trust_dir"), gun, getRemoteTrustServer(mainViper), getTransport(mainViper, gun, true), retriever)
rt, err := getTransport(config, gun, true)
if err != nil {
fatalf(err.Error())
return err
}
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever)
if err != nil {
return err
}
target, err := nRepo.GetTargetByName(targetName)
if err != nil {
logrus.Errorf("error retrieving target by name:%s, error:%v", targetName, err)
os.Exit(1)
return fmt.Errorf("error retrieving target by name:%s, error:%v", targetName, err)
}
// Create hasher and hash data
@ -308,12 +389,10 @@ func tufVerify(cmd *cobra.Command, args []string) {
serverHash := target.Hashes["sha256"]
if subtle.ConstantTimeCompare(stdinHash[:], serverHash) == 0 {
logrus.Error("notary: data not present in the trusted collection.")
os.Exit(1)
} else {
_, _ = os.Stdout.Write(payload)
return fmt.Errorf("notary: data not present in the trusted collection")
}
return
_, _ = os.Stdout.Write(payload)
return nil
}
type passwordStore struct {
@ -364,16 +443,9 @@ func (ps passwordStore) Basic(u *url.URL) (string, string) {
// The readOnly flag indicates if the operation should be performed as an
// anonymous read only operation. If the command entered requires write
// permissions on the server, readOnly must be false
func getTransport(config *viper.Viper, gun string, readOnly bool) http.RoundTripper {
func getTransport(config *viper.Viper, gun string, readOnly bool) (http.RoundTripper, error) {
// Attempt to get a root CA from the config file. Nil is the host defaults.
rootCAFile := config.GetString("remote_server.root_ca")
if rootCAFile != "" {
// If we haven't been given an Absolute path, we assume it's relative
// from the configuration directory (~/.notary by default)
if !filepath.IsAbs(rootCAFile) {
rootCAFile = filepath.Join(configPath, rootCAFile)
}
}
rootCAFile := utils.GetPathRelativeToConfig(config, "remote_server.root_ca")
insecureSkipVerify := false
if config.IsSet("remote_server.skipTLSVerify") {
@ -403,7 +475,7 @@ func getTransport(config *viper.Viper, gun string, readOnly bool) http.RoundTrip
}
func tokenAuth(trustServerURL string, baseTransport *http.Transport, gun string,
readOnly bool) http.RoundTripper {
readOnly bool) (http.RoundTripper, error) {
// TODO(dmcgowan): add notary specific headers
authTransport := transport.NewTransport(baseTransport)
@ -413,25 +485,25 @@ func tokenAuth(trustServerURL string, baseTransport *http.Transport, gun string,
}
endpoint, err := url.Parse(trustServerURL)
if err != nil {
fatalf("Could not parse remote trust server url (%s): %s", trustServerURL, err.Error())
return nil, fmt.Errorf("Could not parse remote trust server url (%s): %s", trustServerURL, err.Error())
}
if endpoint.Scheme == "" {
fatalf("Trust server url has to be in the form of http(s)://URL:PORT. Got: %s", trustServerURL)
return nil, fmt.Errorf("Trust server url has to be in the form of http(s)://URL:PORT. Got: %s", trustServerURL)
}
subPath, err := url.Parse("v2/")
if err != nil {
fatalf("Failed to parse v2 subpath. This error should not have been reached. Please report it as an issue at https://github.com/docker/notary/issues: %s", err.Error())
return nil, fmt.Errorf("Failed to parse v2 subpath. This error should not have been reached. Please report it as an issue at https://github.com/docker/notary/issues: %s", err.Error())
}
endpoint = endpoint.ResolveReference(subPath)
req, err := http.NewRequest("GET", endpoint.String(), nil)
if err != nil {
fatalf(err.Error())
return nil, err
}
resp, err := pingClient.Do(req)
if err != nil {
logrus.Errorf("could not reach %s: %s", trustServerURL, err.Error())
logrus.Info("continuing in offline mode")
return nil
return nil, nil
}
// non-nil err means we must close body
defer resp.Body.Close()
@ -442,29 +514,24 @@ func tokenAuth(trustServerURL string, baseTransport *http.Transport, gun string,
// not a valid status code.
logrus.Errorf("could not reach %s: %d", trustServerURL, resp.StatusCode)
logrus.Info("continuing in offline mode")
return nil
return nil, nil
}
challengeManager := auth.NewSimpleChallengeManager()
if err := challengeManager.AddResponse(resp); err != nil {
fatalf(err.Error())
return nil, err
}
ps := passwordStore{anonymous: readOnly}
tokenHandler := auth.NewTokenHandler(authTransport, ps, gun, "push", "pull")
basicHandler := auth.NewBasicHandler(ps)
modifier := transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
return transport.NewTransport(baseTransport, modifier)
return transport.NewTransport(baseTransport, modifier), nil
}
func getRemoteTrustServer(config *viper.Viper) string {
if remoteTrustServer == "" {
configRemote := config.GetString("remote_server.url")
if configRemote != "" {
remoteTrustServer = configRemote
} else {
remoteTrustServer = defaultServerURL
}
if configRemote := config.GetString("remote_server.url"); configRemote != "" {
return configRemote
}
return remoteTrustServer
return defaultServerURL
}

View File

@ -14,7 +14,9 @@ func TestTokenAuth(t *testing.T) {
baseTransport = &http.Transport{}
gun = "test"
)
require.Nil(t, tokenAuth("https://localhost:9999", baseTransport, gun, readOnly))
auth, err := tokenAuth("https://localhost:9999", baseTransport, gun, readOnly)
require.NoError(t, err)
require.Nil(t, auth)
}
func StatusOKTestHandler(w http.ResponseWriter, r *http.Request) {
@ -31,7 +33,9 @@ func TestTokenAuth200Status(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(NotAuthorizedTestHandler))
defer s.Close()
require.NotNil(t, tokenAuth(s.URL, baseTransport, gun, readOnly))
auth, err := tokenAuth(s.URL, baseTransport, gun, readOnly)
require.NoError(t, err)
require.NotNil(t, auth)
}
func NotAuthorizedTestHandler(w http.ResponseWriter, r *http.Request) {
@ -47,7 +51,9 @@ func TestTokenAuth401Status(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(NotAuthorizedTestHandler))
defer s.Close()
require.NotNil(t, tokenAuth(s.URL, baseTransport, gun, readOnly))
auth, err := tokenAuth(s.URL, baseTransport, gun, readOnly)
require.NoError(t, err)
require.NotNil(t, auth)
}
func NotFoundTestHandler(w http.ResponseWriter, r *http.Request) {
@ -63,5 +69,7 @@ func TestTokenAuthNon200Non401Status(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(NotFoundTestHandler))
defer s.Close()
require.Nil(t, tokenAuth(s.URL, baseTransport, gun, readOnly))
auth, err := tokenAuth(s.URL, baseTransport, gun, readOnly)
require.NoError(t, err)
require.Nil(t, auth)
}