mirror of https://github.com/docker/docs.git
				
				
				
			Merge pull request #440 from docker/diogo-cli-adding-delegations
delegation command for notary-cli
This commit is contained in:
		
						commit
						cf0bb5a9be
					
				|  | @ -352,7 +352,7 @@ func (r *NotaryRepository) AddDelegation(name string, threshold int, | |||
| // the repository when the changelist gets applied at publish time.
 | ||||
| // This does not validate that the delegation exists, since one might exist
 | ||||
| // after applying all changes.
 | ||||
| func (r *NotaryRepository) RemoveDelegation(name string) error { | ||||
| func (r *NotaryRepository) RemoveDelegation(name string, keyIDs, paths []string, removeAll bool) error { | ||||
| 
 | ||||
| 	if !data.IsDelegation(name) { | ||||
| 		return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"} | ||||
|  | @ -365,14 +365,35 @@ func (r *NotaryRepository) RemoveDelegation(name string) error { | |||
| 	defer cl.Close() | ||||
| 
 | ||||
| 	logrus.Debugf(`Removing delegation "%s"\n`, name) | ||||
| 	var template *changelist.TufChange | ||||
| 
 | ||||
| 	template := changelist.NewTufChange( | ||||
| 		changelist.ActionDelete, | ||||
| 		name, | ||||
| 		changelist.TypeTargetsDelegation, | ||||
| 		"", // no path
 | ||||
| 		nil, | ||||
| 	) | ||||
| 	// We use the Delete action only for force removal, Update is used for removing individual keys and paths
 | ||||
| 	if removeAll { | ||||
| 		template = changelist.NewTufChange( | ||||
| 			changelist.ActionDelete, | ||||
| 			name, | ||||
| 			changelist.TypeTargetsDelegation, | ||||
| 			"",  // no path
 | ||||
| 			nil, // deleting role, no data needed
 | ||||
| 		) | ||||
| 
 | ||||
| 	} else { | ||||
| 		tdJSON, err := json.Marshal(&changelist.TufDelegation{ | ||||
| 			RemoveKeys:  keyIDs, | ||||
| 			RemovePaths: paths, | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		template = changelist.NewTufChange( | ||||
| 			changelist.ActionUpdate, | ||||
| 			name, | ||||
| 			changelist.TypeTargetsDelegation, | ||||
| 			"", // no path
 | ||||
| 			tdJSON, | ||||
| 		) | ||||
| 	} | ||||
| 
 | ||||
| 	return addChange(cl, template, name) | ||||
| } | ||||
|  | @ -514,6 +535,45 @@ func (r *NotaryRepository) GetChangelist() (changelist.Changelist, error) { | |||
| 	return cl, nil | ||||
| } | ||||
| 
 | ||||
| // GetDelegationRoles returns the keys and roles of the repository's delegations
 | ||||
| func (r *NotaryRepository) GetDelegationRoles() ([]*data.Role, error) { | ||||
| 	// Update state of the repo to latest
 | ||||
| 	if _, err := r.Update(false); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// All top level delegations (ex: targets/level1) are stored exclusively in targets.json
 | ||||
| 	targets, ok := r.tufRepo.Targets[data.CanonicalTargetsRole] | ||||
| 	if !ok { | ||||
| 		return nil, store.ErrMetaNotFound{data.CanonicalTargetsRole} | ||||
| 	} | ||||
| 
 | ||||
| 	allDelegations := targets.Signed.Delegations.Roles | ||||
| 
 | ||||
| 	// make a copy for traversing nested delegations
 | ||||
| 	delegationsList := make([]*data.Role, len(allDelegations)) | ||||
| 	copy(delegationsList, allDelegations) | ||||
| 
 | ||||
| 	// Now traverse to lower level delegations (ex: targets/level1/level2)
 | ||||
| 	for len(delegationsList) > 0 { | ||||
| 		// Pop off first delegation to traverse
 | ||||
| 		delegation := delegationsList[0] | ||||
| 		delegationsList = delegationsList[1:] | ||||
| 
 | ||||
| 		// Get metadata
 | ||||
| 		delegationMeta, ok := r.tufRepo.Targets[delegation.Name] | ||||
| 		// If we get an error, don't try to traverse further into this subtree because it doesn't exist or is malformed
 | ||||
| 		if !ok { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		// Add nested delegations to return list and exploration list
 | ||||
| 		allDelegations = append(allDelegations, delegationMeta.Signed.Delegations.Roles...) | ||||
| 		delegationsList = append(delegationsList, delegationMeta.Signed.Delegations.Roles...) | ||||
| 	} | ||||
| 	return allDelegations, nil | ||||
| } | ||||
| 
 | ||||
| // Publish pushes the local changes in signed material to the remote notary-server
 | ||||
| // Conceptually it performs an operation similar to a `git rebase`
 | ||||
| func (r *NotaryRepository) Publish() error { | ||||
|  |  | |||
|  | @ -2312,7 +2312,7 @@ func TestPublishRemoveDelgation(t *testing.T) { | |||
| 	assert.NoError(t, delgRepo.Publish()) | ||||
| 
 | ||||
| 	// owner removes delegation
 | ||||
| 	assert.NoError(t, ownerRepo.RemoveDelegation("targets/a")) | ||||
| 	assert.NoError(t, ownerRepo.RemoveDelegation("targets/a", []string{aKey.ID()}, []string{}, false)) | ||||
| 	assert.NoError(t, ownerRepo.Publish()) | ||||
| 
 | ||||
| 	// delegated repo can now no longer publish to delegated role
 | ||||
|  | @ -2654,23 +2654,22 @@ func TestRemoveDelegationChangefileValid(t *testing.T) { | |||
| 	rootPubKey := repo.CryptoService.GetKey(rootKeyID) | ||||
| 	assert.NotNil(t, rootPubKey) | ||||
| 
 | ||||
| 	err := repo.RemoveDelegation("root") | ||||
| 	err := repo.RemoveDelegation("root", []string{rootKeyID}, []string{}, false) | ||||
| 	assert.Error(t, err) | ||||
| 	assert.IsType(t, data.ErrInvalidRole{}, err) | ||||
| 	assert.Empty(t, getChanges(t, repo)) | ||||
| 
 | ||||
| 	// to demonstrate that so long as the delegation name is valid, the
 | ||||
| 	// existence of the delegation doesn't matter
 | ||||
| 	assert.NoError(t, repo.RemoveDelegation("targets/a/b/c")) | ||||
| 	assert.NoError(t, repo.RemoveDelegation("targets/a/b/c", []string{rootKeyID}, []string{}, false)) | ||||
| 
 | ||||
| 	// ensure that the changefile is correct
 | ||||
| 	changes := getChanges(t, repo) | ||||
| 	assert.Len(t, changes, 1) | ||||
| 	assert.Equal(t, changelist.ActionDelete, changes[0].Action()) | ||||
| 	assert.Equal(t, changelist.ActionUpdate, changes[0].Action()) | ||||
| 	assert.Equal(t, "targets/a/b/c", changes[0].Scope()) | ||||
| 	assert.Equal(t, changelist.TypeTargetsDelegation, changes[0].Type()) | ||||
| 	assert.Equal(t, "", changes[0].Path()) | ||||
| 	assert.Empty(t, changes[0].Content()) | ||||
| } | ||||
| 
 | ||||
| // The changefile produced by RemoveDelegation, when applied, actually removes
 | ||||
|  | @ -2697,7 +2696,7 @@ func TestRemoveDelegationChangefileApplicable(t *testing.T) { | |||
| 	assert.Len(t, targetRole.Signed.Delegations.Keys, 1) | ||||
| 
 | ||||
| 	// now remove it
 | ||||
| 	assert.NoError(t, repo.RemoveDelegation("targets/a")) | ||||
| 	assert.NoError(t, repo.RemoveDelegation("targets/a", []string{rootKeyID}, []string{}, false)) | ||||
| 	changes = getChanges(t, repo) | ||||
| 	assert.Len(t, changes, 2) | ||||
| 	assert.NoError(t, applyTargetsChange(repo.tufRepo, changes[1])) | ||||
|  | @ -2711,7 +2710,7 @@ func TestRemoveDelegationChangefileApplicable(t *testing.T) { | |||
| // file to be propagated.
 | ||||
| func TestRemoveDelegationErrorWritingChanges(t *testing.T) { | ||||
| 	testErrorWritingChangefiles(t, func(repo *NotaryRepository) error { | ||||
| 		return repo.RemoveDelegation("targets/a") | ||||
| 		return repo.RemoveDelegation("targets/a", []string{""}, []string{}, false) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import ( | |||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"path" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/Sirupsen/logrus" | ||||
|  | @ -85,13 +86,13 @@ func changeTargetsDelegation(repo *tuf.Repo, c changelist.Change) error { | |||
| 			return err | ||||
| 		} | ||||
| 		if err == nil { | ||||
| 			// role existed
 | ||||
| 			return data.ErrInvalidRole{ | ||||
| 				Role:   c.Scope(), | ||||
| 				Reason: "cannot create a role that already exists", | ||||
| 			// role existed, attempt to merge paths and keys
 | ||||
| 			if err := r.AddPaths(td.AddPaths); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			return repo.UpdateDelegations(r, td.AddKeys) | ||||
| 		} | ||||
| 		// role doesn't exist, create brand new
 | ||||
| 		// create brand new role
 | ||||
| 		r, err = td.ToNewRole(c.Scope()) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
|  | @ -107,7 +108,12 @@ func changeTargetsDelegation(repo *tuf.Repo, c changelist.Change) error { | |||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		// role exists, merge
 | ||||
| 		// If we specify the only keys left delete the role, else just delete specified keys
 | ||||
| 		if strings.Join(r.KeyIDs, ";") == strings.Join(td.RemoveKeys, ";") && len(td.AddKeys) == 0 { | ||||
| 			r := data.Role{Name: c.Scope()} | ||||
| 			return repo.DeleteDelegation(r) | ||||
| 		} | ||||
| 		// if we aren't deleting and the role exists, merge
 | ||||
| 		if err := r.AddPaths(td.AddPaths); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  |  | |||
|  | @ -225,12 +225,18 @@ func TestApplyTargetsDelegationCreateDelete(t *testing.T) { | |||
| 	assert.Equal(t, "level1", role.Paths[0]) | ||||
| 
 | ||||
| 	// delete delegation
 | ||||
| 	td = &changelist.TufDelegation{ | ||||
| 		RemoveKeys: []string{newKey.ID()}, | ||||
| 	} | ||||
| 
 | ||||
| 	tdJSON, err = json.Marshal(td) | ||||
| 	assert.NoError(t, err) | ||||
| 	ch = changelist.NewTufChange( | ||||
| 		changelist.ActionDelete, | ||||
| 		"targets/level1", | ||||
| 		changelist.TypeTargetsDelegation, | ||||
| 		"", | ||||
| 		nil, | ||||
| 		tdJSON, | ||||
| 	) | ||||
| 
 | ||||
| 	err = applyTargetsChange(repo, ch) | ||||
|  | @ -307,13 +313,18 @@ func TestApplyTargetsDelegationCreate2SharedKey(t *testing.T) { | |||
| 	assert.Equal(t, "targets/level2", role2.Name) | ||||
| 	assert.Equal(t, "level2", role2.Paths[0]) | ||||
| 
 | ||||
| 	// delete one delegation, ensure key remains
 | ||||
| 	// delete one delegation, ensure shared key remains
 | ||||
| 	td = &changelist.TufDelegation{ | ||||
| 		RemoveKeys: []string{newKey.ID()}, | ||||
| 	} | ||||
| 	tdJSON, err = json.Marshal(td) | ||||
| 	assert.NoError(t, err) | ||||
| 	ch = changelist.NewTufChange( | ||||
| 		changelist.ActionDelete, | ||||
| 		"targets/level1", | ||||
| 		changelist.TypeTargetsDelegation, | ||||
| 		"", | ||||
| 		nil, | ||||
| 		tdJSON, | ||||
| 	) | ||||
| 
 | ||||
| 	err = applyTargetsChange(repo, ch) | ||||
|  | @ -328,7 +339,7 @@ func TestApplyTargetsDelegationCreate2SharedKey(t *testing.T) { | |||
| 		"targets/level2", | ||||
| 		changelist.TypeTargetsDelegation, | ||||
| 		"", | ||||
| 		nil, | ||||
| 		tdJSON, | ||||
| 	) | ||||
| 
 | ||||
| 	err = applyTargetsChange(repo, ch) | ||||
|  | @ -468,11 +479,91 @@ func TestApplyTargetsDelegationCreateAlreadyExisting(t *testing.T) { | |||
| 	// we have sufficient checks elsewhere we don't need to confirm that
 | ||||
| 	// creating fresh works here via more asserts.
 | ||||
| 
 | ||||
| 	// when attempting to create the same role again, assert we receive
 | ||||
| 	// an ErrInvalidRole because an existing role can't be "created"
 | ||||
| 	extraKey, err := cs.Create("targets/level1", data.ED25519Key) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// create delegation
 | ||||
| 	kl = data.KeyList{extraKey} | ||||
| 	td = &changelist.TufDelegation{ | ||||
| 		NewThreshold: 1, | ||||
| 		AddKeys:      kl, | ||||
| 		AddPaths:     []string{"level1"}, | ||||
| 	} | ||||
| 
 | ||||
| 	tdJSON, err = json.Marshal(td) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	ch = changelist.NewTufChange( | ||||
| 		changelist.ActionCreate, | ||||
| 		"targets/level1", | ||||
| 		changelist.TypeTargetsDelegation, | ||||
| 		"", | ||||
| 		tdJSON, | ||||
| 	) | ||||
| 
 | ||||
| 	// when attempting to create the same role again, check that we added a key
 | ||||
| 	err = applyTargetsChange(repo, ch) | ||||
| 	assert.Error(t, err) | ||||
| 	assert.IsType(t, data.ErrInvalidRole{}, err) | ||||
| 	assert.NoError(t, err) | ||||
| 	delegation, err := repo.GetDelegation("targets/level1") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, delegation.Paths, "level1") | ||||
| 	assert.Equal(t, len(delegation.KeyIDs), 2) | ||||
| } | ||||
| 
 | ||||
| func TestApplyTargetsDelegationAlreadyExistingMergePaths(t *testing.T) { | ||||
| 	_, repo, cs, err := testutils.EmptyRepo("docker.com/notary") | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	newKey, err := cs.Create("targets/level1", data.ED25519Key) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// create delegation
 | ||||
| 	kl := data.KeyList{newKey} | ||||
| 	td := &changelist.TufDelegation{ | ||||
| 		NewThreshold: 1, | ||||
| 		AddKeys:      kl, | ||||
| 		AddPaths:     []string{"level1"}, | ||||
| 	} | ||||
| 
 | ||||
| 	tdJSON, err := json.Marshal(td) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	ch := changelist.NewTufChange( | ||||
| 		changelist.ActionCreate, | ||||
| 		"targets/level1", | ||||
| 		changelist.TypeTargetsDelegation, | ||||
| 		"", | ||||
| 		tdJSON, | ||||
| 	) | ||||
| 
 | ||||
| 	err = applyTargetsChange(repo, ch) | ||||
| 	assert.NoError(t, err) | ||||
| 	// we have sufficient checks elsewhere we don't need to confirm that
 | ||||
| 	// creating fresh works here via more asserts.
 | ||||
| 
 | ||||
| 	// Use different path for this changelist
 | ||||
| 	td.AddPaths = []string{"level2"} | ||||
| 
 | ||||
| 	tdJSON, err = json.Marshal(td) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	ch = changelist.NewTufChange( | ||||
| 		changelist.ActionCreate, | ||||
| 		"targets/level1", | ||||
| 		changelist.TypeTargetsDelegation, | ||||
| 		"", | ||||
| 		tdJSON, | ||||
| 	) | ||||
| 
 | ||||
| 	// when attempting to create the same role again, check that we
 | ||||
| 	// merged with previous details
 | ||||
| 	err = applyTargetsChange(repo, ch) | ||||
| 	assert.NoError(t, err) | ||||
| 	delegation, err := repo.GetDelegation("targets/level1") | ||||
| 	assert.NoError(t, err) | ||||
| 	// Assert we have both paths
 | ||||
| 	assert.Contains(t, delegation.Paths, "level2") | ||||
| 	assert.Contains(t, delegation.Paths, "level1") | ||||
| } | ||||
| 
 | ||||
| func TestApplyTargetsDelegationInvalidRole(t *testing.T) { | ||||
|  |  | |||
|  | @ -0,0 +1,212 @@ | |||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 
 | ||||
| 	"github.com/docker/notary" | ||||
| 	notaryclient "github.com/docker/notary/client" | ||||
| 	"github.com/docker/notary/passphrase" | ||||
| 	"github.com/docker/notary/trustmanager" | ||||
| 	"github.com/docker/notary/tuf/data" | ||||
| 	"github.com/spf13/cobra" | ||||
| 	"github.com/spf13/viper" | ||||
| ) | ||||
| 
 | ||||
| var cmdDelegationTemplate = usageTemplate{ | ||||
| 	Use:   "delegation", | ||||
| 	Short: "Operates on delegations.", | ||||
| 	Long:  `Operations on TUF delegations.`, | ||||
| } | ||||
| 
 | ||||
| var cmdDelegationListTemplate = usageTemplate{ | ||||
| 	Use:   "list [ GUN ]", | ||||
| 	Short: "Lists delegations for the Global Unique Name.", | ||||
| 	Long:  "Lists all delegations known to notary for a specific Global Unique Name.", | ||||
| } | ||||
| 
 | ||||
| var cmdDelegationRemoveTemplate = usageTemplate{ | ||||
| 	Use:   "remove [ GUN ] [ Role ] <KeyID 1> ...", | ||||
| 	Short: "Remove KeyID(s) from the specified Role delegation.", | ||||
| 	Long:  "Remove KeyID(s) from the specified Role delegation in a specific Global Unique Name.", | ||||
| } | ||||
| 
 | ||||
| var cmdDelegationAddTemplate = usageTemplate{ | ||||
| 	Use:   "add [ GUN ] [ Role ] <PEM file path 1> ...", | ||||
| 	Short: "Add a keys to delegation using the provided public key certificate PEMs.", | ||||
| 	Long:  "Add a keys to delegation using the provided public key certificate PEMs 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 | ||||
| 	retriever    passphrase.Retriever | ||||
| } | ||||
| 
 | ||||
| func (d *delegationCommander) GetCommand() *cobra.Command { | ||||
| 	cmd := cmdDelegationTemplate.ToCommand(nil) | ||||
| 	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)") | ||||
| 	cmd.AddCommand(cmdRemDelg) | ||||
| 
 | ||||
| 	cmdAddDelg := cmdDelegationAddTemplate.ToCommand(d.delegationAdd) | ||||
| 	cmdAddDelg.Flags().StringSliceVar(&paths, "paths", nil, "List of paths to add") | ||||
| 	cmd.AddCommand(cmdAddDelg) | ||||
| 	return cmd | ||||
| } | ||||
| 
 | ||||
| // delegationsList lists all the delegations for a particular GUN
 | ||||
| func (d *delegationCommander) delegationsList(cmd *cobra.Command, args []string) error { | ||||
| 	if len(args) != 1 { | ||||
| 		return fmt.Errorf( | ||||
| 			"Please provide a Global Unique Name as an argument to list") | ||||
| 	} | ||||
| 
 | ||||
| 	config := d.configGetter() | ||||
| 
 | ||||
| 	gun := args[0] | ||||
| 
 | ||||
| 	// 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) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	delegationRoles, err := nRepo.GetDelegationRoles() | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("Error retrieving delegation roles for repository %s: %v", gun, err) | ||||
| 	} | ||||
| 
 | ||||
| 	cmd.Println("") | ||||
| 	prettyPrintRoles(delegationRoles, cmd.Out()) | ||||
| 	cmd.Println("") | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // 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 { | ||||
| 		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() | ||||
| 
 | ||||
| 	gun := args[0] | ||||
| 	role := args[1] | ||||
| 
 | ||||
| 	// 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 | ||||
| 	} | ||||
| 
 | ||||
| 	keyIDs := []string{} | ||||
| 	// Change nil paths to empty slice for TUF
 | ||||
| 	if paths == nil { | ||||
| 		paths = []string{} | ||||
| 	} | ||||
| 
 | ||||
| 	if len(args) > 2 { | ||||
| 		keyIDs = args[2:] | ||||
| 	} | ||||
| 
 | ||||
| 	// 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) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if 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 { | ||||
| 			confirmed := askConfirm() | ||||
| 			if !confirmed { | ||||
| 				fatalf("Aborting action.") | ||||
| 			} | ||||
| 		} else { | ||||
| 			cmd.Println("Confirmed `yes` from flag") | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Remove the delegation from the repository
 | ||||
| 	err = nRepo.RemoveDelegation(role, keyIDs, paths, removeAll) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to remove delegation: %v", err) | ||||
| 	} | ||||
| 	cmd.Println("") | ||||
| 	if removeAll { | ||||
| 		cmd.Printf("Forced removal (including all keys and paths) of delegation role %s to repository \"%s\" staged for next publish.\n", role, gun) | ||||
| 	} | ||||
| 	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) | ||||
| 	cmd.Println("") | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // 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 { | ||||
| 		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() | ||||
| 
 | ||||
| 	gun := args[0] | ||||
| 	role := args[1] | ||||
| 
 | ||||
| 	pubKeys := []data.PublicKey{} | ||||
| 	if len(args) > 2 { | ||||
| 		pubKeyPaths := args[2:] | ||||
| 		for _, pubKeyPath := range pubKeyPaths { | ||||
| 			// Read public key bytes from PEM file
 | ||||
| 			pubKeyBytes, err := ioutil.ReadFile(pubKeyPath) | ||||
| 			if err != nil { | ||||
| 				return fmt.Errorf("unable to read public key from file: %s", pubKeyPath) | ||||
| 			} | ||||
| 
 | ||||
| 			// Parse PEM bytes into type PublicKey
 | ||||
| 			pubKey, err := trustmanager.ParsePEMPublicKey(pubKeyBytes) | ||||
| 			if err != nil { | ||||
| 				return fmt.Errorf("unable to parse valid public key certificate from PEM file %s: %v", pubKeyPath, err) | ||||
| 			} | ||||
| 			pubKeys = append(pubKeys, pubKey) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// 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) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Add the delegation to the repository
 | ||||
| 	// Sets threshold to 1 since we only added one key - thresholds are not currently fully supported, though
 | ||||
| 	// one can use additional client-side validation to check for signatures from a quorum of varying delegation roles
 | ||||
| 	err = nRepo.AddDelegation(role, notary.MinThreshold, pubKeys, paths) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to create delegation: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Make keyID slice for better CLI print
 | ||||
| 	pubKeyIDs := []string{} | ||||
| 	for _, pubKey := range pubKeys { | ||||
| 		pubKeyIDs = append(pubKeyIDs, pubKey.ID()) | ||||
| 	} | ||||
| 
 | ||||
| 	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) | ||||
| 	cmd.Println("") | ||||
| 	return nil | ||||
| } | ||||
|  | @ -0,0 +1,139 @@ | |||
| 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" | ||||
| ) | ||||
| 
 | ||||
| var testTrustDir = "trust_dir" | ||||
| 
 | ||||
| func setup() *delegationCommander { | ||||
| 	return &delegationCommander{ | ||||
| 		configGetter: func() *viper.Viper { | ||||
| 			mainViper := viper.New() | ||||
| 			mainViper.Set("trust_dir", testTrustDir) | ||||
| 			return mainViper | ||||
| 		}, | ||||
| 		retriever: nil, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestAddInvalidDelegationName(t *testing.T) { | ||||
| 	// Cleanup after test
 | ||||
| 	defer os.RemoveAll(testTrustDir) | ||||
| 
 | ||||
| 	// Setup certificate
 | ||||
| 	tempFile, err := ioutil.TempFile("/tmp", "pemfile") | ||||
| 	assert.NoError(t, err) | ||||
| 	cert, _, err := generateValidTestCert() | ||||
| 	_, err = tempFile.Write(trustmanager.CertToPEM(cert)) | ||||
| 	assert.NoError(t, err) | ||||
| 	tempFile.Close() | ||||
| 	defer os.Remove(tempFile.Name()) | ||||
| 
 | ||||
| 	// Setup commander
 | ||||
| 	commander := setup() | ||||
| 
 | ||||
| 	// Should error due to invalid delegation name (should be prefixed by "targets/")
 | ||||
| 	err = commander.delegationAdd(commander.GetCommand(), []string{"gun", "INVALID_NAME", tempFile.Name()}) | ||||
| 	assert.Error(t, err) | ||||
| } | ||||
| 
 | ||||
| func TestAddInvalidDelegationCert(t *testing.T) { | ||||
| 	// Cleanup after test
 | ||||
| 	defer os.RemoveAll(testTrustDir) | ||||
| 
 | ||||
| 	// Setup certificate
 | ||||
| 	tempFile, err := ioutil.TempFile("/tmp", "pemfile") | ||||
| 	assert.NoError(t, err) | ||||
| 	cert, _, err := generateExpiredTestCert() | ||||
| 	_, err = tempFile.Write(trustmanager.CertToPEM(cert)) | ||||
| 	assert.NoError(t, err) | ||||
| 	tempFile.Close() | ||||
| 	defer os.Remove(tempFile.Name()) | ||||
| 
 | ||||
| 	// Setup commander
 | ||||
| 	commander := setup() | ||||
| 
 | ||||
| 	// Should error due to expired cert
 | ||||
| 	err = commander.delegationAdd(commander.GetCommand(), []string{"gun", "targets/delegation", tempFile.Name(), "--paths", "path"}) | ||||
| 	assert.Error(t, err) | ||||
| } | ||||
| 
 | ||||
| func TestRemoveInvalidDelegationName(t *testing.T) { | ||||
| 	// Cleanup after test
 | ||||
| 	defer os.RemoveAll(testTrustDir) | ||||
| 
 | ||||
| 	// Setup commander
 | ||||
| 	commander := setup() | ||||
| 
 | ||||
| 	// Should error due to invalid delegation name (should be prefixed by "targets/")
 | ||||
| 	err := commander.delegationRemove(commander.GetCommand(), []string{"gun", "INVALID_NAME", "fake_key_id1", "fake_key_id2"}) | ||||
| 	assert.Error(t, err) | ||||
| } | ||||
| 
 | ||||
| func TestAddInvalidNumArgs(t *testing.T) { | ||||
| 	// Setup commander
 | ||||
| 	commander := setup() | ||||
| 
 | ||||
| 	// Should error due to invalid number of args (2 instead of 3)
 | ||||
| 	err := commander.delegationAdd(commander.GetCommand(), []string{"not", "enough"}) | ||||
| 	assert.Error(t, err) | ||||
| } | ||||
| 
 | ||||
| func TestListInvalidNumArgs(t *testing.T) { | ||||
| 	// Setup commander
 | ||||
| 	commander := setup() | ||||
| 
 | ||||
| 	// Should error due to invalid number of args (0 instead of 1)
 | ||||
| 	err := commander.delegationsList(commander.GetCommand(), []string{}) | ||||
| 	assert.Error(t, err) | ||||
| } | ||||
| 
 | ||||
| func TestRemoveInvalidNumArgs(t *testing.T) { | ||||
| 	// Setup commander
 | ||||
| 	commander := setup() | ||||
| 
 | ||||
| 	// Should error due to invalid number of args (1 instead of 2)
 | ||||
| 	err := commander.delegationRemove(commander.GetCommand(), []string{"notenough"}) | ||||
| 	assert.Error(t, err) | ||||
| } | ||||
| 
 | ||||
| func generateValidTestCert() (*x509.Certificate, string, error) { | ||||
| 	privKey, err := trustmanager.GenerateECDSAKey(rand.Reader) | ||||
| 	if err != nil { | ||||
| 		return nil, "", err | ||||
| 	} | ||||
| 	keyID := privKey.ID() | ||||
| 	startTime := time.Now() | ||||
| 	endTime := startTime.AddDate(10, 0, 0) | ||||
| 	cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime) | ||||
| 	if err != nil { | ||||
| 		return nil, "", err | ||||
| 	} | ||||
| 	return cert, keyID, nil | ||||
| } | ||||
| 
 | ||||
| func generateExpiredTestCert() (*x509.Certificate, string, error) { | ||||
| 	privKey, err := trustmanager.GenerateECDSAKey(rand.Reader) | ||||
| 	if err != nil { | ||||
| 		return nil, "", err | ||||
| 	} | ||||
| 	keyID := privKey.ID() | ||||
| 	// Set to Unix time 0 start time, valid for one more day
 | ||||
| 	startTime := time.Unix(0, 0) | ||||
| 	endTime := startTime.AddDate(0, 0, 1) | ||||
| 	cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime) | ||||
| 	if err != nil { | ||||
| 		return nil, "", err | ||||
| 	} | ||||
| 	return cert, keyID, nil | ||||
| } | ||||
|  | @ -145,6 +145,223 @@ func TestClientTufInteraction(t *testing.T) { | |||
| 	assert.False(t, strings.Contains(string(output), target)) | ||||
| } | ||||
| 
 | ||||
| // Initialize repo and test delegations commands by adding, listing, and removing delegations
 | ||||
| func TestClientDelegationsInteraction(t *testing.T) { | ||||
| 	setUp(t) | ||||
| 
 | ||||
| 	tempDir := tempDirWithConfig(t, "{}") | ||||
| 	defer os.RemoveAll(tempDir) | ||||
| 
 | ||||
| 	server := setupServer() | ||||
| 	defer server.Close() | ||||
| 
 | ||||
| 	// Setup certificate
 | ||||
| 	tempFile, err := ioutil.TempFile("/tmp", "pemfile") | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	privKey, err := trustmanager.GenerateECDSAKey(rand.Reader) | ||||
| 	startTime := time.Now() | ||||
| 	endTime := startTime.AddDate(10, 0, 0) | ||||
| 	cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	_, err = tempFile.Write(trustmanager.CertToPEM(cert)) | ||||
| 	assert.NoError(t, err) | ||||
| 	tempFile.Close() | ||||
| 	defer os.Remove(tempFile.Name()) | ||||
| 
 | ||||
| 	rawPubBytes, _ := ioutil.ReadFile(tempFile.Name()) | ||||
| 	parsedPubKey, _ := trustmanager.ParsePEMPublicKey(rawPubBytes) | ||||
| 	keyID := parsedPubKey.ID() | ||||
| 
 | ||||
| 	var output string | ||||
| 
 | ||||
| 	// -- tests --
 | ||||
| 
 | ||||
| 	// init repo
 | ||||
| 	_, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// publish repo
 | ||||
| 	_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// list delegations - none yet
 | ||||
| 	output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, "No such roles published in this repository.") | ||||
| 
 | ||||
| 	// add new valid delegation with single new cert
 | ||||
| 	output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", tempFile.Name()) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, "Addition of delegation role") | ||||
| 
 | ||||
| 	// check status - see delegation
 | ||||
| 	output, err = runCommand(t, tempDir, "status", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, "Unpublished changes for gun") | ||||
| 
 | ||||
| 	// list delegations - none yet because still unpublished
 | ||||
| 	output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, "No such roles published in this repository.") | ||||
| 
 | ||||
| 	// publish repo
 | ||||
| 	_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// check status - no changelist
 | ||||
| 	output, err = runCommand(t, tempDir, "status", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, "No unpublished changes for gun") | ||||
| 
 | ||||
| 	// list delegations - we should see our added delegation
 | ||||
| 	output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, "targets/delegation") | ||||
| 
 | ||||
| 	// Setup another certificate
 | ||||
| 	tempFile2, err := ioutil.TempFile("/tmp", "pemfile2") | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	privKey, err = trustmanager.GenerateECDSAKey(rand.Reader) | ||||
| 	startTime = time.Now() | ||||
| 	endTime = startTime.AddDate(10, 0, 0) | ||||
| 	cert, err = cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	_, err = tempFile2.Write(trustmanager.CertToPEM(cert)) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.NoError(t, err) | ||||
| 	tempFile2.Close() | ||||
| 	defer os.Remove(tempFile2.Name()) | ||||
| 
 | ||||
| 	rawPubBytes2, _ := ioutil.ReadFile(tempFile2.Name()) | ||||
| 	parsedPubKey2, _ := trustmanager.ParsePEMPublicKey(rawPubBytes2) | ||||
| 	keyID2 := parsedPubKey2.ID() | ||||
| 
 | ||||
| 	// add to the delegation by specifying the same role, this time add a path
 | ||||
| 	output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", tempFile2.Name(), "--paths", "path") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, "Addition of delegation role") | ||||
| 
 | ||||
| 	// publish repo
 | ||||
| 	_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// list delegations - we should see two keys
 | ||||
| 	output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, ",") | ||||
| 	assert.Contains(t, output, "path") | ||||
| 
 | ||||
| 	// remove the delegation's first key
 | ||||
| 	output, err = runCommand(t, tempDir, "delegation", "remove", "gun", "targets/delegation", keyID) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, "Removal of delegation role") | ||||
| 
 | ||||
| 	// publish repo
 | ||||
| 	_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// list delegations - we should see the delegation but with only the second key
 | ||||
| 	output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.NotContains(t, output, ",") | ||||
| 
 | ||||
| 	// remove the delegation's second key
 | ||||
| 	output, err = runCommand(t, tempDir, "delegation", "remove", "gun", "targets/delegation", keyID2) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, "Removal of delegation role") | ||||
| 
 | ||||
| 	// publish repo
 | ||||
| 	_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// list delegations - we should see no delegations
 | ||||
| 	output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, "No such roles published in this repository.") | ||||
| 
 | ||||
| 	// add delegation with multiple certs and multiple paths
 | ||||
| 	output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", tempFile.Name(), tempFile2.Name(), "--paths", "path1,path2") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, "Addition of delegation role") | ||||
| 
 | ||||
| 	// publish repo
 | ||||
| 	_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// list delegations - we should see two keys
 | ||||
| 	output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, ",") | ||||
| 	assert.Contains(t, output, "path1,path2") | ||||
| 
 | ||||
| 	// add delegation with multiple certs and multiple paths
 | ||||
| 	output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", "--paths", "path3") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, "Addition of delegation role") | ||||
| 
 | ||||
| 	// publish repo
 | ||||
| 	_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// list delegations - we should see two keys
 | ||||
| 	output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, ",") | ||||
| 	assert.Contains(t, output, "path1,path2,path3") | ||||
| 
 | ||||
| 	// just remove two paths from this delegation
 | ||||
| 	output, err = runCommand(t, tempDir, "delegation", "remove", "gun", "targets/delegation", "--paths", "path2,path3") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, "Removal of delegation role") | ||||
| 
 | ||||
| 	// publish repo
 | ||||
| 	_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// list delegations - we should see the same two keys, and only path1
 | ||||
| 	output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, ",") | ||||
| 	assert.Contains(t, output, "path1") | ||||
| 	assert.NotContains(t, output, "path2") | ||||
| 	assert.NotContains(t, output, "path3") | ||||
| 
 | ||||
| 	// remove the remaining path, should not remove the delegation entirely
 | ||||
| 	output, err = runCommand(t, tempDir, "delegation", "remove", "gun", "targets/delegation", "--paths", "path1") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, "Removal of delegation role") | ||||
| 
 | ||||
| 	// publish repo
 | ||||
| 	_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// list delegations - we should see the same two keys, and no paths
 | ||||
| 	output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, ",") | ||||
| 	assert.NotContains(t, output, "path1") | ||||
| 	assert.NotContains(t, output, "path2") | ||||
| 	assert.NotContains(t, output, "path3") | ||||
| 
 | ||||
| 	// remove by force to delete the delegation entirely
 | ||||
| 	output, err = runCommand(t, tempDir, "delegation", "remove", "gun", "targets/delegation", "-y") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, "Removal of delegation role") | ||||
| 
 | ||||
| 	// publish repo
 | ||||
| 	_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// list delegations - we should see no delegations
 | ||||
| 	output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Contains(t, output, "No such roles published in this repository.") | ||||
| } | ||||
| 
 | ||||
| // Splits a string into lines, and returns any lines that are not empty (
 | ||||
| // striped of whitespace)
 | ||||
| func splitLines(chunk string) []string { | ||||
|  |  | |||
|  | @ -119,7 +119,13 @@ func setupCommand(notaryCmd *cobra.Command) { | |||
| 		retriever:    retriever, | ||||
| 	} | ||||
| 
 | ||||
| 	cmdDelegationGenerator := &delegationCommander{ | ||||
| 		configGetter: parseConfig, | ||||
| 		retriever:    retriever, | ||||
| 	} | ||||
| 
 | ||||
| 	notaryCmd.AddCommand(cmdKeyGenerator.GetCommand()) | ||||
| 	notaryCmd.AddCommand(cmdDelegationGenerator.GetCommand()) | ||||
| 	notaryCmd.AddCommand(cmdCert) | ||||
| 	notaryCmd.AddCommand(cmdTufInit) | ||||
| 	notaryCmd.AddCommand(cmdTufList) | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ import ( | |||
| 	"math" | ||||
| 	"path/filepath" | ||||
| 	"sort" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/docker/notary/client" | ||||
|  | @ -142,8 +143,17 @@ func (t targetsSorter) Less(i, j int) bool { | |||
| 	return t[i].Name < t[j].Name | ||||
| } | ||||
| 
 | ||||
| // Given a list of KeyStores in order of listing preference, pretty-prints the
 | ||||
| // root keys and then the signing keys.
 | ||||
| // --- pretty printing roles ---
 | ||||
| 
 | ||||
| type roleSorter []*data.Role | ||||
| 
 | ||||
| func (r roleSorter) Len() int      { return len(r) } | ||||
| func (r roleSorter) Swap(i, j int) { r[i], r[j] = r[j], r[i] } | ||||
| func (r roleSorter) Less(i, j int) bool { | ||||
| 	return r[i].Name < r[j].Name | ||||
| } | ||||
| 
 | ||||
| // Pretty-prints the sorted list of TargetWithRoles.
 | ||||
| func prettyPrintTargets(ts []*client.TargetWithRole, writer io.Writer) { | ||||
| 	if len(ts) == 0 { | ||||
| 		writer.Write([]byte("\nNo targets present in this repository.\n\n")) | ||||
|  | @ -165,6 +175,29 @@ func prettyPrintTargets(ts []*client.TargetWithRole, writer io.Writer) { | |||
| 	table.Render() | ||||
| } | ||||
| 
 | ||||
| // Pretty-prints the list of provided Roles
 | ||||
| func prettyPrintRoles(rs []*data.Role, writer io.Writer) { | ||||
| 	if len(rs) == 0 { | ||||
| 		writer.Write([]byte("\nNo such roles published in this repository.\n\n")) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// this sorter works for Role types
 | ||||
| 	sort.Stable(roleSorter(rs)) | ||||
| 
 | ||||
| 	table := getTable([]string{"Role", "Paths", "Key IDs", "Threshold"}, writer) | ||||
| 
 | ||||
| 	for _, r := range rs { | ||||
| 		table.Append([]string{ | ||||
| 			r.Name, | ||||
| 			strings.Join(r.Paths, ","), | ||||
| 			strings.Join(r.KeyIDs, ","), | ||||
| 			fmt.Sprintf("%v", r.Threshold), | ||||
| 		}) | ||||
| 	} | ||||
| 	table.Render() | ||||
| } | ||||
| 
 | ||||
| // --- pretty printing certs ---
 | ||||
| 
 | ||||
| // cert by repo name then expiry time.  Don't bother sorting by fingerprint.
 | ||||
|  |  | |||
|  | @ -214,6 +214,58 @@ func generateCertificate(t *testing.T, gun string, expireInHours int64) *x509.Ce | |||
| 	return cert | ||||
| } | ||||
| 
 | ||||
| // --- tests for pretty printing roles ---
 | ||||
| 
 | ||||
| // If there are no roles, no table is printed, only a line saying that there
 | ||||
| // are no roles.
 | ||||
| func TestPrettyPrintZeroRoles(t *testing.T) { | ||||
| 	var b bytes.Buffer | ||||
| 	prettyPrintRoles([]*data.Role{}, &b) | ||||
| 	text, err := ioutil.ReadAll(&b) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	lines := strings.Split(strings.TrimSpace(string(text)), "\n") | ||||
| 	assert.Len(t, lines, 1) | ||||
| 	assert.Equal(t, "No such roles published in this repository.", lines[0]) | ||||
| } | ||||
| 
 | ||||
| // Roles are sorted by name, and the name, paths, and KeyIDs are printed.
 | ||||
| func TestPrettyPrintSortedRoles(t *testing.T) { | ||||
| 	var err error | ||||
| 
 | ||||
| 	unsorted := []*data.Role{ | ||||
| 		{Name: "targets/zebra", Paths: []string{"stripes", "black", "white"}, RootRole: data.RootRole{KeyIDs: []string{"101"}, Threshold: 1}}, | ||||
| 		{Name: "targets/aardvark/unicorn/pony", Paths: []string{"rainbows"}, RootRole: data.RootRole{KeyIDs: []string{"135"}, Threshold: 1}}, | ||||
| 		{Name: "targets/bee", Paths: []string{"honey"}, RootRole: data.RootRole{KeyIDs: []string{"246"}, Threshold: 1}}, | ||||
| 		{Name: "targets/bee/wasp", Paths: []string{"honey/sting"}, RootRole: data.RootRole{KeyIDs: []string{"246", "468"}, Threshold: 1}}, | ||||
| 	} | ||||
| 
 | ||||
| 	var b bytes.Buffer | ||||
| 	prettyPrintRoles(unsorted, &b) | ||||
| 	text, err := ioutil.ReadAll(&b) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	expected := [][]string{ | ||||
| 		{"targets/aardvark/unicorn/pony", "rainbows", "135", "1"}, | ||||
| 		{"targets/bee", "honey", "246", "1"}, | ||||
| 		{"targets/bee/wasp", "honey/sting", "246,468", "1"}, | ||||
| 		{"targets/zebra", "stripes,black,white", "101", "1"}, | ||||
| 	} | ||||
| 
 | ||||
| 	lines := strings.Split(strings.TrimSpace(string(text)), "\n") | ||||
| 	assert.Len(t, lines, len(expected)+2) | ||||
| 
 | ||||
| 	// starts with headers
 | ||||
| 	assert.True(t, reflect.DeepEqual(strings.Fields(lines[0]), strings.Fields( | ||||
| 		"ROLE     PATHS      KEY IDS   THRESHOLD"))) | ||||
| 	assert.Equal(t, "----", lines[1][:4]) | ||||
| 
 | ||||
| 	for i, line := range lines[2:] { | ||||
| 		splitted := strings.Fields(line) | ||||
| 		assert.Equal(t, expected[i], splitted) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // If there are no certs in the cert store store, a message that there are no
 | ||||
| // certs should be displayed.
 | ||||
| func TestPrettyPrintZeroCerts(t *testing.T) { | ||||
|  |  | |||
							
								
								
									
										2
									
								
								const.go
								
								
								
								
							
							
						
						
									
										2
									
								
								const.go
								
								
								
								
							|  | @ -2,6 +2,8 @@ package notary | |||
| 
 | ||||
| // application wide constants
 | ||||
| const ( | ||||
| 	// Require a minimum of one threshold for roles, currently we do not support a higher threshold
 | ||||
| 	MinThreshold    = 1 | ||||
| 	PrivKeyPerms    = 0700 | ||||
| 	PubCertPerms    = 0755 | ||||
| 	Sha256HexSize   = 64 | ||||
|  |  | |||
|  | @ -69,8 +69,8 @@ func (cs *CryptoService) Create(role, algorithm string) (data.PublicKey, error) | |||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to add key to filestore: %v", err) | ||||
| 	} | ||||
| 	return nil, fmt.Errorf("keystores would not accept new private keys for unknown reasons") | ||||
| 
 | ||||
| 	return nil, fmt.Errorf("keystores would not accept new private keys for unknown reasons") | ||||
| } | ||||
| 
 | ||||
| // GetPrivateKey returns a private key and role if present by ID.
 | ||||
|  |  | |||
|  | @ -300,6 +300,44 @@ func ParsePEMPrivateKey(pemBytes []byte, passphrase string) (data.PrivateKey, er | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| // ParsePEMPublicKey returns a data.PublicKey from a PEM encoded public key or certificate.
 | ||||
| func ParsePEMPublicKey(pubKeyBytes []byte) (data.PublicKey, error) { | ||||
| 	pemBlock, _ := pem.Decode(pubKeyBytes) | ||||
| 	if pemBlock == nil { | ||||
| 		return nil, errors.New("no valid public key found") | ||||
| 	} | ||||
| 
 | ||||
| 	switch pemBlock.Type { | ||||
| 	case "CERTIFICATE": | ||||
| 		cert, err := x509.ParseCertificate(pemBlock.Bytes) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("could not parse provided certificate: %v", err) | ||||
| 		} | ||||
| 		err = ValidateCertificate(cert) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("invalid certificate: %v", err) | ||||
| 		} | ||||
| 		return CertToKey(cert), nil | ||||
| 	default: | ||||
| 		return nil, fmt.Errorf("unsupported PEM block type %q, expected certificate", pemBlock.Type) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // ValidateCertificate returns an error if the certificate is not valid for notary
 | ||||
| // Currently, this is only a time expiry check
 | ||||
| func ValidateCertificate(c *x509.Certificate) error { | ||||
| 	if (c.NotBefore).After(c.NotAfter) { | ||||
| 		return fmt.Errorf("certificate validity window is invalid") | ||||
| 	} | ||||
| 	now := time.Now() | ||||
| 	tomorrow := now.AddDate(0, 0, 1) | ||||
| 	// Give one day leeway on creation "before" time, check "after" against today
 | ||||
| 	if (tomorrow).Before(c.NotBefore) || now.After(c.NotAfter) { | ||||
| 		return fmt.Errorf("certificate is expired") | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // GenerateRSAKey generates an RSA private key and returns a TUF PrivateKey
 | ||||
| func GenerateRSAKey(random io.Reader, bits int) (data.PrivateKey, error) { | ||||
| 	rsaPrivKey, err := rsa.GenerateKey(random, bits) | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ package data | |||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/Sirupsen/logrus" | ||||
| 	"path" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
|  | @ -109,10 +110,7 @@ func NewRole(name string, threshold int, keyIDs, paths, pathHashPrefixes []strin | |||
| 	} | ||||
| 	if IsDelegation(name) { | ||||
| 		if len(paths) == 0 && len(pathHashPrefixes) == 0 { | ||||
| 			return nil, ErrInvalidRole{ | ||||
| 				Role:   name, | ||||
| 				Reason: "roles with no Paths and no PathHashPrefixes will never be able to publish content", | ||||
| 			} | ||||
| 			logrus.Debugf("role %s with no Paths and no PathHashPrefixes will never be able to publish content until one or more are added", name) | ||||
| 		} | ||||
| 	} | ||||
| 	if threshold < 1 { | ||||
|  |  | |||
|  | @ -63,16 +63,6 @@ func TestSubtractStrSlicesEqual(t *testing.T) { | |||
| 	assert.Len(t, res, 0) | ||||
| } | ||||
| 
 | ||||
| func TestNewRolePathsAndHashPrefixRejection(t *testing.T) { | ||||
| 	_, err := NewRole("targets/level1", 1, []string{"abc"}, nil, nil) | ||||
| 	assert.Error(t, err) | ||||
| 	assert.IsType(t, ErrInvalidRole{}, err) | ||||
| 
 | ||||
| 	_, err = NewRole("targets/level1", 1, []string{"abc"}, []string{""}, []string{""}) | ||||
| 	assert.Error(t, err) | ||||
| 	assert.IsType(t, ErrInvalidRole{}, err) | ||||
| } | ||||
| 
 | ||||
| func TestAddRemoveKeys(t *testing.T) { | ||||
| 	role, err := NewRole("targets", 1, []string{"abc"}, []string{""}, nil) | ||||
| 	assert.NoError(t, err) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue