From df1c425caea6dceed32c01b1cbd12a66e282e04c Mon Sep 17 00:00:00 2001
From: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
Date: Tue, 2 Aug 2016 16:06:23 -0700
Subject: [PATCH 1/2] notary delete CLI command and integration test

Signed-off-by: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
---
 cmd/notary/integration_test.go | 167 ++++++++++++++++++++++++++++++---
 cmd/notary/main_test.go        |   1 +
 cmd/notary/tuf.go              |  47 ++++++++++
 3 files changed, 200 insertions(+), 15 deletions(-)

diff --git a/cmd/notary/integration_test.go b/cmd/notary/integration_test.go
index 06a6780ae..1745ee868 100644
--- a/cmd/notary/integration_test.go
+++ b/cmd/notary/integration_test.go
@@ -24,10 +24,12 @@ import (
 	"github.com/Sirupsen/logrus"
 	ctxu "github.com/docker/distribution/context"
 	"github.com/docker/notary"
+	"github.com/docker/notary/client"
 	"github.com/docker/notary/cryptoservice"
 	"github.com/docker/notary/passphrase"
 	"github.com/docker/notary/server"
 	"github.com/docker/notary/server/storage"
+	notaryStorage "github.com/docker/notary/storage"
 	"github.com/docker/notary/trustmanager"
 	"github.com/docker/notary/tuf/data"
 	"github.com/docker/notary/tuf/utils"
@@ -245,6 +247,156 @@ func TestClientTUFInteraction(t *testing.T) {
 	require.False(t, strings.Contains(string(output), target))
 }
 
+func TestClientDeleteTUFInteraction(t *testing.T) {
+	// -- setup --
+	setUp(t)
+
+	tempDir := tempDirWithConfig(t, "{}")
+	defer os.RemoveAll(tempDir)
+
+	server := setupServer()
+	defer server.Close()
+
+	tempFile, err := ioutil.TempFile("", "targetfile")
+	require.NoError(t, err)
+	tempFile.Close()
+	defer os.Remove(tempFile.Name())
+
+	// Setup certificate
+	certFile, err := ioutil.TempFile("", "pemfile")
+	require.NoError(t, err)
+
+	privKey, err := utils.GenerateECDSAKey(rand.Reader)
+	startTime := time.Now()
+	endTime := startTime.AddDate(10, 0, 0)
+	cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime)
+	require.NoError(t, err)
+
+	_, err = certFile.Write(utils.CertToPEM(cert))
+	require.NoError(t, err)
+	tempFile.Close()
+	defer os.Remove(certFile.Name())
+
+	var (
+		output string
+		target = "helloIamanotarytarget"
+	)
+	// -- tests --
+
+	// init repo
+	_, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun")
+	require.NoError(t, err)
+
+	// add a target
+	_, err = runCommand(t, tempDir, "add", "gun", target, tempFile.Name())
+	require.NoError(t, err)
+
+	// check status - see target
+	output, err = runCommand(t, tempDir, "status", "gun")
+	require.NoError(t, err)
+	require.True(t, strings.Contains(output, target))
+
+	// publish repo
+	_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun")
+	require.NoError(t, err)
+
+	// list repo - see target
+	output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun")
+	require.NoError(t, err)
+	require.True(t, strings.Contains(string(output), target))
+
+	// add a delegation and publish
+	output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", certFile.Name())
+	require.NoError(t, err)
+	_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun")
+	require.NoError(t, err)
+
+	// list delegations - see role
+	output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun")
+	require.NoError(t, err)
+	require.True(t, strings.Contains(string(output), "targets/delegation"))
+
+	// Delete the repo metadata locally, so no need for server URL
+	output, err = runCommand(t, tempDir, "delete", "gun")
+	require.NoError(t, err)
+	assertLocalMetadataForGun(t, tempDir, "gun", false)
+
+	// list repo - see target still because remote data exists
+	output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun")
+	require.NoError(t, err)
+	require.True(t, strings.Contains(string(output), target))
+
+	// list delegations - see role because remote data still exists
+	output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun")
+	require.NoError(t, err)
+	require.True(t, strings.Contains(string(output), "targets/delegation"))
+
+	// Trying to delete the repo with the remote flag fails if it's given a badly formed URL
+	output, err = runCommand(t, tempDir, "-s", "//invalidURLType", "delete", "gun", "--remote")
+	require.Error(t, err)
+	// since the connection fails to parse the URL before we can delete anything, local data should exist
+	assertLocalMetadataForGun(t, tempDir, "gun", true)
+
+	// Trying to delete the repo with the remote flag fails if it's given a well-formed URL that doesn't point to a server
+	output, err = runCommand(t, tempDir, "-s", "https://invalid-server", "delete", "gun", "--remote")
+	require.Error(t, err)
+	require.IsType(t, notaryStorage.ErrOffline{}, err)
+	// In this case, local notary metadata does not exist since local deletion operates first if we have a valid transport
+	assertLocalMetadataForGun(t, tempDir, "gun", false)
+
+	// Delete the repo remotely and locally, pointing to the correct server
+	output, err = runCommand(t, tempDir, "-s", server.URL, "delete", "gun", "--remote")
+	require.NoError(t, err)
+	assertLocalMetadataForGun(t, tempDir, "gun", false)
+	_, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun")
+	require.Error(t, err)
+	require.IsType(t, client.ErrRepositoryNotExist{}, err)
+
+	// Silent success on extraneous deletes
+	output, err = runCommand(t, tempDir, "-s", server.URL, "delete", "gun", "--remote")
+	require.NoError(t, err)
+	assertLocalMetadataForGun(t, tempDir, "gun", false)
+	_, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun")
+	require.Error(t, err)
+	require.IsType(t, client.ErrRepositoryNotExist{}, err)
+
+	// Now check that we can re-publish the same repo
+	// init repo
+	_, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun")
+	require.NoError(t, err)
+
+	// add a target
+	_, err = runCommand(t, tempDir, "add", "gun", target, tempFile.Name())
+	require.NoError(t, err)
+
+	// check status - see target
+	output, err = runCommand(t, tempDir, "status", "gun")
+	require.NoError(t, err)
+	require.True(t, strings.Contains(output, target))
+
+	// publish repo
+	_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun")
+	require.NoError(t, err)
+
+	// list repo - see target
+	output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun")
+	require.NoError(t, err)
+	require.True(t, strings.Contains(string(output), target))
+}
+
+func assertLocalMetadataForGun(t *testing.T, configDir, gun string, shouldExist bool) {
+	for _, role := range data.BaseRoles {
+		fileInfo, err := os.Stat(filepath.Join(configDir, "tuf", gun, "metadata", role+".json"))
+		if shouldExist {
+			require.NoError(t, err)
+			require.NotNil(t, fileInfo)
+		} else {
+			require.Error(t, err)
+			require.Nil(t, fileInfo)
+		}
+	}
+}
+
 // Initializes a repo, adds a target, publishes the target by hash, lists the target,
 // verifies the target, and then removes the target.
 func TestClientTUFAddByHashInteraction(t *testing.T) {
@@ -1171,21 +1323,6 @@ func TestClientKeyGenerationRotation(t *testing.T) {
 	require.True(t, strings.Contains(string(output), target))
 }
 
-// Helper method to get the subdirectory for TUF keys
-func getKeySubdir(role, gun string) string {
-	subdir := notary.PrivDir
-	switch role {
-	case data.CanonicalRootRole:
-		return filepath.Join(subdir, notary.RootKeysSubdir)
-	case data.CanonicalTargetsRole:
-		return filepath.Join(subdir, notary.NonRootKeysSubdir, gun)
-	case data.CanonicalSnapshotRole:
-		return filepath.Join(subdir, notary.NonRootKeysSubdir, gun)
-	default:
-		return filepath.Join(subdir, notary.NonRootKeysSubdir)
-	}
-}
-
 // Tests default root key generation
 func TestDefaultRootKeyGeneration(t *testing.T) {
 	// -- setup --
diff --git a/cmd/notary/main_test.go b/cmd/notary/main_test.go
index 42e768e46..1d44a6be9 100644
--- a/cmd/notary/main_test.go
+++ b/cmd/notary/main_test.go
@@ -166,6 +166,7 @@ var exampleValidCommands = []string{
 	"delegation add repo targets/releases path/to/pem/file.pem",
 	"delegation remove repo targets/releases",
 	"witness gun targets/releases",
+	"delete repo",
 }
 
 // config parsing bugs are propagated in all commands
diff --git a/cmd/notary/tuf.go b/cmd/notary/tuf.go
index 671f73227..72da3f20a 100644
--- a/cmd/notary/tuf.go
+++ b/cmd/notary/tuf.go
@@ -89,6 +89,12 @@ var cmdWitnessTemplate = usageTemplate{
 	Long:  "Marks roles to be re-signed the next time they're published. Currently will always bump version and expiry for role. N.B. behaviour may change when thresholding is introduced.",
 }
 
+var cmdTUFDeleteTemplate = usageTemplate{
+	Use:   "delete [ GUN ]",
+	Short: "Deletes all content for a trusted collection",
+	Long:  "Deletes all local content for a trusted collection identified by the Globally Unique Name. Remote data can also be deleted with an additional flag.",
+}
+
 type tufCommander struct {
 	// these need to be set
 	configGetter func() (*viper.Viper, error)
@@ -107,6 +113,8 @@ type tufCommander struct {
 	deleteIdx         []int
 	reset             bool
 	archiveChangelist string
+
+	deleteRemote bool
 }
 
 func (t *tufCommander) AddToCommand(cmd *cobra.Command) {
@@ -149,6 +157,10 @@ func (t *tufCommander) AddToCommand(cmd *cobra.Command) {
 
 	cmdWitness := cmdWitnessTemplate.ToCommand(t.tufWitness)
 	cmd.AddCommand(cmdWitness)
+
+	cmdTUFDeleteGUN := cmdTUFDeleteTemplate.ToCommand(t.tufDeleteGUN)
+	cmdTUFDeleteGUN.Flags().BoolVar(&t.deleteRemote, "remote", false, "Delete remote data for GUN in addition to local cache")
+	cmd.AddCommand(cmdTUFDeleteGUN)
 }
 
 func (t *tufCommander) tufWitness(cmd *cobra.Command, args []string) error {
@@ -301,6 +313,41 @@ func (t *tufCommander) tufAdd(cmd *cobra.Command, args []string) error {
 	return nil
 }
 
+func (t *tufCommander) tufDeleteGUN(cmd *cobra.Command, args []string) error {
+	if len(args) < 1 {
+		cmd.Usage()
+		return fmt.Errorf("Must specify a GUN")
+	}
+	config, err := t.configGetter()
+	if err != nil {
+		return err
+	}
+
+	gun := args[0]
+
+	trustPin, err := getTrustPinning(config)
+
+	// Only initialize a roundtripper if we get the remote flag
+	var rt http.RoundTripper
+	if t.deleteRemote {
+		rt, err = getTransport(config, gun, admin)
+		if err != nil {
+			return err
+		}
+	}
+
+	nRepo, err := notaryclient.NewNotaryRepository(
+		config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, trustPin)
+
+	if err != nil {
+		return err
+	}
+
+	cmd.Printf("Deleting trust data for repository %s.\n", gun)
+
+	return nRepo.DeleteTrustData(t.deleteRemote)
+}
+
 func (t *tufCommander) tufInit(cmd *cobra.Command, args []string) error {
 	if len(args) < 1 {
 		cmd.Usage()

From 2beb1fdb698d09cc8ed04fd3f4b31cfc06570389 Mon Sep 17 00:00:00 2001
From: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
Date: Wed, 3 Aug 2016 11:04:14 -0700
Subject: [PATCH 2/2] Factor out cert generation to helper

Signed-off-by: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
---
 cmd/notary/integration_test.go | 118 ++++++++++++---------------------
 1 file changed, 41 insertions(+), 77 deletions(-)

diff --git a/cmd/notary/integration_test.go b/cmd/notary/integration_test.go
index 1745ee868..1e04dd345 100644
--- a/cmd/notary/integration_test.go
+++ b/cmd/notary/integration_test.go
@@ -11,7 +11,9 @@ import (
 	"crypto/rand"
 	"crypto/sha256"
 	"crypto/sha512"
+	"crypto/x509"
 	"encoding/hex"
+	"fmt"
 	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
@@ -265,16 +267,8 @@ func TestClientDeleteTUFInteraction(t *testing.T) {
 	// Setup certificate
 	certFile, err := ioutil.TempFile("", "pemfile")
 	require.NoError(t, err)
-
-	privKey, err := utils.GenerateECDSAKey(rand.Reader)
-	startTime := time.Now()
-	endTime := startTime.AddDate(10, 0, 0)
-	cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime)
-	require.NoError(t, err)
-
+	cert, _, _ := generateCertPrivKeyPair(t, "gun", data.ECDSAKey)
 	_, err = certFile.Write(utils.CertToPEM(cert))
-	require.NoError(t, err)
-	tempFile.Close()
 	defer os.Remove(certFile.Name())
 
 	var (
@@ -557,23 +551,12 @@ func TestClientDelegationsInteraction(t *testing.T) {
 	// Setup certificate
 	tempFile, err := ioutil.TempFile("", "pemfile")
 	require.NoError(t, err)
-
-	privKey, err := utils.GenerateECDSAKey(rand.Reader)
-	startTime := time.Now()
-	endTime := startTime.AddDate(10, 0, 0)
-	cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime)
-	require.NoError(t, err)
-
+	cert, _, keyID := generateCertPrivKeyPair(t, "gun", data.ECDSAKey)
 	_, err = tempFile.Write(utils.CertToPEM(cert))
 	require.NoError(t, err)
 	tempFile.Close()
 	defer os.Remove(tempFile.Name())
 
-	rawPubBytes, _ := ioutil.ReadFile(tempFile.Name())
-	parsedPubKey, _ := utils.ParsePEMPublicKey(rawPubBytes)
-	keyID, err := utils.CanonicalKeyID(parsedPubKey)
-	require.NoError(t, err)
-
 	var output string
 
 	// -- tests --
@@ -646,23 +629,12 @@ func TestClientDelegationsInteraction(t *testing.T) {
 	tempFile2, err := ioutil.TempFile("", "pemfile2")
 	require.NoError(t, err)
 
-	privKey, err = utils.GenerateECDSAKey(rand.Reader)
-	startTime = time.Now()
-	endTime = startTime.AddDate(10, 0, 0)
-	cert, err = cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime)
-	require.NoError(t, err)
-
-	_, err = tempFile2.Write(utils.CertToPEM(cert))
-	require.NoError(t, err)
+	cert2, _, keyID2 := generateCertPrivKeyPair(t, "gun", data.ECDSAKey)
+	_, err = tempFile2.Write(utils.CertToPEM(cert2))
 	require.NoError(t, err)
 	tempFile2.Close()
 	defer os.Remove(tempFile2.Name())
 
-	rawPubBytes2, _ := ioutil.ReadFile(tempFile2.Name())
-	parsedPubKey2, _ := utils.ParsePEMPublicKey(rawPubBytes2)
-	keyID2, err := utils.CanonicalKeyID(parsedPubKey2)
-	require.NoError(t, err)
-
 	// add to the delegation by specifying the same role, this time add a scoped path
 	output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", tempFile2.Name(), "--paths", "path")
 	require.NoError(t, err)
@@ -929,26 +901,15 @@ func TestClientDelegationsPublishing(t *testing.T) {
 	// Setup certificate for delegation role
 	tempFile, err := ioutil.TempFile("", "pemfile")
 	require.NoError(t, err)
-
-	privKey, err := utils.GenerateRSAKey(rand.Reader, 2048)
-	require.NoError(t, err)
-	privKeyBytesNoRole, err := utils.KeyToPEM(privKey, "")
-	require.NoError(t, err)
-	privKeyBytesWithRole, err := utils.KeyToPEM(privKey, "user")
-	require.NoError(t, err)
-	startTime := time.Now()
-	endTime := startTime.AddDate(10, 0, 0)
-	cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime)
-	require.NoError(t, err)
-
+	cert, privKey, canonicalKeyID := generateCertPrivKeyPair(t, "gun", data.RSAKey)
 	_, err = tempFile.Write(utils.CertToPEM(cert))
 	require.NoError(t, err)
 	tempFile.Close()
 	defer os.Remove(tempFile.Name())
 
-	rawPubBytes, _ := ioutil.ReadFile(tempFile.Name())
-	parsedPubKey, _ := utils.ParsePEMPublicKey(rawPubBytes)
-	canonicalKeyID, err := utils.CanonicalKeyID(parsedPubKey)
+	privKeyBytesNoRole, err := utils.KeyToPEM(privKey, "")
+	require.NoError(t, err)
+	privKeyBytesWithRole, err := utils.KeyToPEM(privKey, "user")
 	require.NoError(t, err)
 
 	// Set up targets for publishing
@@ -1103,24 +1064,12 @@ func TestClientDelegationsPublishing(t *testing.T) {
 	// Setup another certificate
 	tempFile2, err := ioutil.TempFile("", "pemfile2")
 	require.NoError(t, err)
-
-	privKey, err = utils.GenerateECDSAKey(rand.Reader)
-	startTime = time.Now()
-	endTime = startTime.AddDate(10, 0, 0)
-	cert, err = cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime)
-	require.NoError(t, err)
-
-	_, err = tempFile2.Write(utils.CertToPEM(cert))
-	require.NoError(t, err)
+	cert2, _, keyID2 := generateCertPrivKeyPair(t, "gun", data.RSAKey)
+	_, err = tempFile2.Write(utils.CertToPEM(cert2))
 	require.NoError(t, err)
 	tempFile2.Close()
 	defer os.Remove(tempFile2.Name())
 
-	rawPubBytes2, _ := ioutil.ReadFile(tempFile2.Name())
-	parsedPubKey2, _ := utils.ParsePEMPublicKey(rawPubBytes2)
-	keyID2, err := utils.CanonicalKeyID(parsedPubKey2)
-	require.NoError(t, err)
-
 	// add a nested delegation under this releases role
 	output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/releases/nested", tempFile2.Name(), "--paths", "nested/path")
 	require.NoError(t, err)
@@ -1525,29 +1474,21 @@ func TestWitness(t *testing.T) {
 	server := setupServer()
 	defer server.Close()
 
-	startTime := time.Now()
-	endTime := startTime.AddDate(10, 0, 0)
-
 	// Setup certificates
 	tempFile, err := ioutil.TempFile("", "pemfile")
 	require.NoError(t, err)
-	privKey, err := utils.GenerateECDSAKey(rand.Reader)
-	cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime)
-	require.NoError(t, err)
+
+	cert, privKey, keyID := generateCertPrivKeyPair(t, "gun", data.ECDSAKey)
 	_, err = tempFile.Write(utils.CertToPEM(cert))
 	require.NoError(t, err)
 	tempFile.Close()
 	defer os.Remove(tempFile.Name())
-	rawPubBytes, _ := ioutil.ReadFile(tempFile.Name())
-	parsedPubKey, _ := utils.ParsePEMPublicKey(rawPubBytes)
-	keyID, err := utils.CanonicalKeyID(parsedPubKey)
-	require.NoError(t, err)
 
-	tempFile2, err := ioutil.TempFile("", "pemfile")
-	require.NoError(t, err)
-	privKey2, err := utils.GenerateECDSAKey(rand.Reader)
-	cert2, err := cryptoservice.GenerateCertificate(privKey2, "gun", startTime, endTime)
+	// Setup another certificate
+	tempFile2, err := ioutil.TempFile("", "pemfile2")
 	require.NoError(t, err)
+
+	cert2, privKey2, _ := generateCertPrivKeyPair(t, "gun", data.ECDSAKey)
 	_, err = tempFile2.Write(utils.CertToPEM(cert2))
 	require.NoError(t, err)
 	tempFile2.Close()
@@ -1663,3 +1604,26 @@ func TestWitness(t *testing.T) {
 		require.Error(t, err)
 	}
 }
+
+func generateCertPrivKeyPair(t *testing.T, gun, keyAlgorithm string) (*x509.Certificate, data.PrivateKey, string) {
+	// Setup certificate
+	var privKey data.PrivateKey
+	var err error
+	switch keyAlgorithm {
+	case data.ECDSAKey:
+		privKey, err = utils.GenerateECDSAKey(rand.Reader)
+	case data.RSAKey:
+		privKey, err = utils.GenerateRSAKey(rand.Reader, 4096)
+	default:
+		err = fmt.Errorf("invalid key algorithm provided: %s", keyAlgorithm)
+	}
+	require.NoError(t, err)
+	startTime := time.Now()
+	endTime := startTime.AddDate(10, 0, 0)
+	cert, err := cryptoservice.GenerateCertificate(privKey, gun, startTime, endTime)
+	require.NoError(t, err)
+	parsedPubKey, _ := utils.ParsePEMPublicKey(utils.CertToPEM(cert))
+	keyID, err := utils.CanonicalKeyID(parsedPubKey)
+	require.NoError(t, err)
+	return cert, privKey, keyID
+}