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.
|
// the repository when the changelist gets applied at publish time.
|
||||||
// This does not validate that the delegation exists, since one might exist
|
// This does not validate that the delegation exists, since one might exist
|
||||||
// after applying all changes.
|
// 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) {
|
if !data.IsDelegation(name) {
|
||||||
return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role 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()
|
defer cl.Close()
|
||||||
|
|
||||||
logrus.Debugf(`Removing delegation "%s"\n`, name)
|
logrus.Debugf(`Removing delegation "%s"\n`, name)
|
||||||
|
var template *changelist.TufChange
|
||||||
|
|
||||||
template := changelist.NewTufChange(
|
// We use the Delete action only for force removal, Update is used for removing individual keys and paths
|
||||||
changelist.ActionDelete,
|
if removeAll {
|
||||||
name,
|
template = changelist.NewTufChange(
|
||||||
changelist.TypeTargetsDelegation,
|
changelist.ActionDelete,
|
||||||
"", // no path
|
name,
|
||||||
nil,
|
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)
|
return addChange(cl, template, name)
|
||||||
}
|
}
|
||||||
|
|
@ -514,6 +535,45 @@ func (r *NotaryRepository) GetChangelist() (changelist.Changelist, error) {
|
||||||
return cl, nil
|
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
|
// Publish pushes the local changes in signed material to the remote notary-server
|
||||||
// Conceptually it performs an operation similar to a `git rebase`
|
// Conceptually it performs an operation similar to a `git rebase`
|
||||||
func (r *NotaryRepository) Publish() error {
|
func (r *NotaryRepository) Publish() error {
|
||||||
|
|
|
||||||
|
|
@ -2312,7 +2312,7 @@ func TestPublishRemoveDelgation(t *testing.T) {
|
||||||
assert.NoError(t, delgRepo.Publish())
|
assert.NoError(t, delgRepo.Publish())
|
||||||
|
|
||||||
// owner removes delegation
|
// 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())
|
assert.NoError(t, ownerRepo.Publish())
|
||||||
|
|
||||||
// delegated repo can now no longer publish to delegated role
|
// delegated repo can now no longer publish to delegated role
|
||||||
|
|
@ -2654,23 +2654,22 @@ func TestRemoveDelegationChangefileValid(t *testing.T) {
|
||||||
rootPubKey := repo.CryptoService.GetKey(rootKeyID)
|
rootPubKey := repo.CryptoService.GetKey(rootKeyID)
|
||||||
assert.NotNil(t, rootPubKey)
|
assert.NotNil(t, rootPubKey)
|
||||||
|
|
||||||
err := repo.RemoveDelegation("root")
|
err := repo.RemoveDelegation("root", []string{rootKeyID}, []string{}, false)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.IsType(t, data.ErrInvalidRole{}, err)
|
assert.IsType(t, data.ErrInvalidRole{}, err)
|
||||||
assert.Empty(t, getChanges(t, repo))
|
assert.Empty(t, getChanges(t, repo))
|
||||||
|
|
||||||
// to demonstrate that so long as the delegation name is valid, the
|
// to demonstrate that so long as the delegation name is valid, the
|
||||||
// existence of the delegation doesn't matter
|
// 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
|
// ensure that the changefile is correct
|
||||||
changes := getChanges(t, repo)
|
changes := getChanges(t, repo)
|
||||||
assert.Len(t, changes, 1)
|
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, "targets/a/b/c", changes[0].Scope())
|
||||||
assert.Equal(t, changelist.TypeTargetsDelegation, changes[0].Type())
|
assert.Equal(t, changelist.TypeTargetsDelegation, changes[0].Type())
|
||||||
assert.Equal(t, "", changes[0].Path())
|
assert.Equal(t, "", changes[0].Path())
|
||||||
assert.Empty(t, changes[0].Content())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The changefile produced by RemoveDelegation, when applied, actually removes
|
// 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)
|
assert.Len(t, targetRole.Signed.Delegations.Keys, 1)
|
||||||
|
|
||||||
// now remove it
|
// 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)
|
changes = getChanges(t, repo)
|
||||||
assert.Len(t, changes, 2)
|
assert.Len(t, changes, 2)
|
||||||
assert.NoError(t, applyTargetsChange(repo.tufRepo, changes[1]))
|
assert.NoError(t, applyTargetsChange(repo.tufRepo, changes[1]))
|
||||||
|
|
@ -2711,7 +2710,7 @@ func TestRemoveDelegationChangefileApplicable(t *testing.T) {
|
||||||
// file to be propagated.
|
// file to be propagated.
|
||||||
func TestRemoveDelegationErrorWritingChanges(t *testing.T) {
|
func TestRemoveDelegationErrorWritingChanges(t *testing.T) {
|
||||||
testErrorWritingChangefiles(t, func(repo *NotaryRepository) error {
|
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"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
|
|
@ -85,13 +86,13 @@ func changeTargetsDelegation(repo *tuf.Repo, c changelist.Change) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// role existed
|
// role existed, attempt to merge paths and keys
|
||||||
return data.ErrInvalidRole{
|
if err := r.AddPaths(td.AddPaths); err != nil {
|
||||||
Role: c.Scope(),
|
return err
|
||||||
Reason: "cannot create a role that already exists",
|
|
||||||
}
|
}
|
||||||
|
return repo.UpdateDelegations(r, td.AddKeys)
|
||||||
}
|
}
|
||||||
// role doesn't exist, create brand new
|
// create brand new role
|
||||||
r, err = td.ToNewRole(c.Scope())
|
r, err = td.ToNewRole(c.Scope())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -107,7 +108,12 @@ func changeTargetsDelegation(repo *tuf.Repo, c changelist.Change) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err := r.AddPaths(td.AddPaths); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -225,12 +225,18 @@ func TestApplyTargetsDelegationCreateDelete(t *testing.T) {
|
||||||
assert.Equal(t, "level1", role.Paths[0])
|
assert.Equal(t, "level1", role.Paths[0])
|
||||||
|
|
||||||
// delete delegation
|
// delete delegation
|
||||||
|
td = &changelist.TufDelegation{
|
||||||
|
RemoveKeys: []string{newKey.ID()},
|
||||||
|
}
|
||||||
|
|
||||||
|
tdJSON, err = json.Marshal(td)
|
||||||
|
assert.NoError(t, err)
|
||||||
ch = changelist.NewTufChange(
|
ch = changelist.NewTufChange(
|
||||||
changelist.ActionDelete,
|
changelist.ActionDelete,
|
||||||
"targets/level1",
|
"targets/level1",
|
||||||
changelist.TypeTargetsDelegation,
|
changelist.TypeTargetsDelegation,
|
||||||
"",
|
"",
|
||||||
nil,
|
tdJSON,
|
||||||
)
|
)
|
||||||
|
|
||||||
err = applyTargetsChange(repo, ch)
|
err = applyTargetsChange(repo, ch)
|
||||||
|
|
@ -307,13 +313,18 @@ func TestApplyTargetsDelegationCreate2SharedKey(t *testing.T) {
|
||||||
assert.Equal(t, "targets/level2", role2.Name)
|
assert.Equal(t, "targets/level2", role2.Name)
|
||||||
assert.Equal(t, "level2", role2.Paths[0])
|
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(
|
ch = changelist.NewTufChange(
|
||||||
changelist.ActionDelete,
|
changelist.ActionDelete,
|
||||||
"targets/level1",
|
"targets/level1",
|
||||||
changelist.TypeTargetsDelegation,
|
changelist.TypeTargetsDelegation,
|
||||||
"",
|
"",
|
||||||
nil,
|
tdJSON,
|
||||||
)
|
)
|
||||||
|
|
||||||
err = applyTargetsChange(repo, ch)
|
err = applyTargetsChange(repo, ch)
|
||||||
|
|
@ -328,7 +339,7 @@ func TestApplyTargetsDelegationCreate2SharedKey(t *testing.T) {
|
||||||
"targets/level2",
|
"targets/level2",
|
||||||
changelist.TypeTargetsDelegation,
|
changelist.TypeTargetsDelegation,
|
||||||
"",
|
"",
|
||||||
nil,
|
tdJSON,
|
||||||
)
|
)
|
||||||
|
|
||||||
err = applyTargetsChange(repo, ch)
|
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
|
// we have sufficient checks elsewhere we don't need to confirm that
|
||||||
// creating fresh works here via more asserts.
|
// creating fresh works here via more asserts.
|
||||||
|
|
||||||
// when attempting to create the same role again, assert we receive
|
extraKey, err := cs.Create("targets/level1", data.ED25519Key)
|
||||||
// an ErrInvalidRole because an existing role can't be "created"
|
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)
|
err = applyTargetsChange(repo, ch)
|
||||||
assert.Error(t, err)
|
assert.NoError(t, err)
|
||||||
assert.IsType(t, data.ErrInvalidRole{}, 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) {
|
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))
|
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 (
|
// Splits a string into lines, and returns any lines that are not empty (
|
||||||
// striped of whitespace)
|
// striped of whitespace)
|
||||||
func splitLines(chunk string) []string {
|
func splitLines(chunk string) []string {
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,13 @@ func setupCommand(notaryCmd *cobra.Command) {
|
||||||
retriever: retriever,
|
retriever: retriever,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmdDelegationGenerator := &delegationCommander{
|
||||||
|
configGetter: parseConfig,
|
||||||
|
retriever: retriever,
|
||||||
|
}
|
||||||
|
|
||||||
notaryCmd.AddCommand(cmdKeyGenerator.GetCommand())
|
notaryCmd.AddCommand(cmdKeyGenerator.GetCommand())
|
||||||
|
notaryCmd.AddCommand(cmdDelegationGenerator.GetCommand())
|
||||||
notaryCmd.AddCommand(cmdCert)
|
notaryCmd.AddCommand(cmdCert)
|
||||||
notaryCmd.AddCommand(cmdTufInit)
|
notaryCmd.AddCommand(cmdTufInit)
|
||||||
notaryCmd.AddCommand(cmdTufList)
|
notaryCmd.AddCommand(cmdTufList)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/notary/client"
|
"github.com/docker/notary/client"
|
||||||
|
|
@ -142,8 +143,17 @@ func (t targetsSorter) Less(i, j int) bool {
|
||||||
return t[i].Name < t[j].Name
|
return t[i].Name < t[j].Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Given a list of KeyStores in order of listing preference, pretty-prints the
|
// --- pretty printing roles ---
|
||||||
// root keys and then the signing keys.
|
|
||||||
|
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) {
|
func prettyPrintTargets(ts []*client.TargetWithRole, writer io.Writer) {
|
||||||
if len(ts) == 0 {
|
if len(ts) == 0 {
|
||||||
writer.Write([]byte("\nNo targets present in this repository.\n\n"))
|
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()
|
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 ---
|
// --- pretty printing certs ---
|
||||||
|
|
||||||
// cert by repo name then expiry time. Don't bother sorting by fingerprint.
|
// 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
|
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
|
// If there are no certs in the cert store store, a message that there are no
|
||||||
// certs should be displayed.
|
// certs should be displayed.
|
||||||
func TestPrettyPrintZeroCerts(t *testing.T) {
|
func TestPrettyPrintZeroCerts(t *testing.T) {
|
||||||
|
|
|
||||||
2
const.go
2
const.go
|
|
@ -2,6 +2,8 @@ package notary
|
||||||
|
|
||||||
// application wide constants
|
// application wide constants
|
||||||
const (
|
const (
|
||||||
|
// Require a minimum of one threshold for roles, currently we do not support a higher threshold
|
||||||
|
MinThreshold = 1
|
||||||
PrivKeyPerms = 0700
|
PrivKeyPerms = 0700
|
||||||
PubCertPerms = 0755
|
PubCertPerms = 0755
|
||||||
Sha256HexSize = 64
|
Sha256HexSize = 64
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,8 @@ func (cs *CryptoService) Create(role, algorithm string) (data.PublicKey, error)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to add key to filestore: %v", err)
|
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.
|
// 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
|
// GenerateRSAKey generates an RSA private key and returns a TUF PrivateKey
|
||||||
func GenerateRSAKey(random io.Reader, bits int) (data.PrivateKey, error) {
|
func GenerateRSAKey(random io.Reader, bits int) (data.PrivateKey, error) {
|
||||||
rsaPrivKey, err := rsa.GenerateKey(random, bits)
|
rsaPrivKey, err := rsa.GenerateKey(random, bits)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -109,10 +110,7 @@ func NewRole(name string, threshold int, keyIDs, paths, pathHashPrefixes []strin
|
||||||
}
|
}
|
||||||
if IsDelegation(name) {
|
if IsDelegation(name) {
|
||||||
if len(paths) == 0 && len(pathHashPrefixes) == 0 {
|
if len(paths) == 0 && len(pathHashPrefixes) == 0 {
|
||||||
return nil, ErrInvalidRole{
|
logrus.Debugf("role %s with no Paths and no PathHashPrefixes will never be able to publish content until one or more are added", name)
|
||||||
Role: name,
|
|
||||||
Reason: "roles with no Paths and no PathHashPrefixes will never be able to publish content",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if threshold < 1 {
|
if threshold < 1 {
|
||||||
|
|
|
||||||
|
|
@ -63,16 +63,6 @@ func TestSubtractStrSlicesEqual(t *testing.T) {
|
||||||
assert.Len(t, res, 0)
|
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) {
|
func TestAddRemoveKeys(t *testing.T) {
|
||||||
role, err := NewRole("targets", 1, []string{"abc"}, []string{""}, nil)
|
role, err := NewRole("targets", 1, []string{"abc"}, []string{""}, nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue