Merge pull request #373 from cyli/add-remove-delegation

Add support for creating/deleting a delegation to Notary Repository
This commit is contained in:
David Lawrence 2015-12-17 10:06:01 -08:00
commit 90e22ff5ff
3 changed files with 296 additions and 45 deletions

View File

@ -296,6 +296,74 @@ func addChange(cl *changelist.FileChangelist, c changelist.Change, roles ...stri
return nil
}
// AddDelegation creates a new changelist entry to add a delegation to the repository
// when the changelist gets applied at publish time. This does not do any validation
// other than checking the name of the delegation to add - all that will happen
// at publish time.
func (r *NotaryRepository) AddDelegation(name string, threshold int,
delegationKeys []data.PublicKey) error {
if !data.IsDelegation(name) {
return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"}
}
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
if err != nil {
return err
}
defer cl.Close()
logrus.Debugf(`Adding delegation "%s" with threshold %d, and %d keys\n`,
name, threshold, len(delegationKeys))
tdJSON, err := json.Marshal(&changelist.TufDelegation{
NewThreshold: threshold,
AddKeys: data.KeyList(delegationKeys),
})
if err != nil {
return err
}
template := changelist.NewTufChange(
changelist.ActionCreate,
name,
changelist.TypeTargetsDelegation,
"", // no path
tdJSON,
)
return addChange(cl, template, name)
}
// RemoveDelegation creates a new changelist entry to remove a delegation from
// 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 {
if !data.IsDelegation(name) {
return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"}
}
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
if err != nil {
return err
}
defer cl.Close()
logrus.Debugf(`Removing delegation "%s"\n`, name)
template := changelist.NewTufChange(
changelist.ActionDelete,
name,
changelist.TypeTargetsDelegation,
"", // no path
nil,
)
return addChange(cl, template, name)
}
// AddTarget creates new changelist entries to add a target to the given roles
// in the repository when the changelist gets appied at publish time.
// If roles are unspecified, the default role is "target".

View File

@ -602,9 +602,8 @@ func TestAddTargetToSpecifiedInvalidRoles(t *testing.T) {
}
}
// TestAddTargetErrorWritingChanges expects errors writing a change to file
// to be propagated.
func TestAddTargetErrorWritingChanges(t *testing.T) {
// General way to assert that errors writing a changefile are propagated up
func testErrorWritingChangefiles(t *testing.T, writeChangeFile func(*NotaryRepository) error) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
defer os.RemoveAll(tempBaseDir)
@ -617,9 +616,6 @@ func TestAddTargetErrorWritingChanges(t *testing.T) {
repo, _ := initializeRepo(t, data.ECDSAKey, tempBaseDir, gun, ts.URL, false)
target, err := NewTarget("latest", "../fixtures/intermediate-ca.crt")
assert.NoError(t, err, "error creating target")
// first, make the actual changefile unwritable by making the changelist
// directory unwritable
changelistPath := filepath.Join(repo.tufRepoPath, "changelist")
@ -628,7 +624,7 @@ func TestAddTargetErrorWritingChanges(t *testing.T) {
err = os.Chmod(changelistPath, 0600)
assert.NoError(t, err, "could not change permission of changelist dir")
err = repo.AddTarget(target, data.CanonicalTargetsRole)
err = writeChangeFile(repo)
assert.Error(t, err, "Expected an error writing the change")
assert.IsType(t, &os.PathError{}, err)
@ -641,11 +637,21 @@ func TestAddTargetErrorWritingChanges(t *testing.T) {
err = ioutil.WriteFile(changelistPath, []byte("hi"), 0644)
assert.NoError(t, err, "could not write temporary file")
err = repo.AddTarget(target, data.CanonicalTargetsRole)
err = writeChangeFile(repo)
assert.Error(t, err, "Expected an error writing the change")
assert.IsType(t, &os.PathError{}, err)
}
// TestAddTargetErrorWritingChanges expects errors writing a change to file
// to be propagated.
func TestAddTargetErrorWritingChanges(t *testing.T) {
testErrorWritingChangefiles(t, func(repo *NotaryRepository) error {
target, err := NewTarget("latest", "../fixtures/intermediate-ca.crt")
assert.NoError(t, err, "error creating target")
return repo.AddTarget(target, data.CanonicalTargetsRole)
})
}
// TestRemoveTargetToTargetRoleByDefault removes a target without specifying a
// role from a repo. Confirms that the changelist is created correctly for
// the targets scope.
@ -718,7 +724,7 @@ func TestRemoveTargetToSpecifiedInvalidRoles(t *testing.T) {
}
for _, invalidRole := range invalidRoles {
err = repo.RemoveTarget(data.CanonicalTargetsRole, invalidRole)
err = repo.RemoveTarget("latest", data.CanonicalTargetsRole, invalidRole)
assert.Error(t, err, "Expected an ErrInvalidRole error")
assert.IsType(t, data.ErrInvalidRole{}, err)
@ -730,42 +736,9 @@ func TestRemoveTargetToSpecifiedInvalidRoles(t *testing.T) {
// TestRemoveTargetErrorWritingChanges expects errors writing a change to file
// to be propagated.
func TestRemoveTargetErrorWritingChanges(t *testing.T) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
defer os.Remove(tempBaseDir)
assert.NoError(t, err, "failed to create a temporary directory: %s", err)
gun := "docker.com/notary"
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, data.ECDSAKey, tempBaseDir, gun, ts.URL, false)
// first, make the actual changefile unwritable by making the changelist
// directory unwritable
changelistPath := filepath.Join(repo.tufRepoPath, "changelist")
err = os.MkdirAll(changelistPath, 0744)
assert.NoError(t, err, "could not create changelist dir")
err = os.Chmod(changelistPath, 0600)
assert.NoError(t, err, "could not change permission of changelist dir")
err = repo.RemoveTarget(data.CanonicalTargetsRole)
assert.Error(t, err, "Expected an error writing the change")
assert.IsType(t, &os.PathError{}, err)
// then break prevent the changlist directory from being able to be created
err = os.Chmod(changelistPath, 0744)
assert.NoError(t, err, "could not change permission of temp dir")
err = os.RemoveAll(changelistPath)
assert.NoError(t, err, "could not remove changelist dir")
// creating a changelist file so the directory can't be created
err = ioutil.WriteFile(changelistPath, []byte("hi"), 0644)
assert.NoError(t, err, "could not write temporary file")
err = repo.RemoveTarget(data.CanonicalTargetsRole)
assert.Error(t, err, "Expected an error writing the change")
assert.IsType(t, &os.PathError{}, err)
testErrorWritingChangefiles(t, func(repo *NotaryRepository) error {
return repo.RemoveTarget("latest", data.CanonicalTargetsRole)
})
}
// TestListTarget fakes serving signed metadata files over the test's
@ -1475,3 +1448,178 @@ func TestRemoteServerUnavailableNoLocalCache(t *testing.T) {
assert.Error(t, err)
assert.IsType(t, store.ErrServerUnavailable{}, err)
}
// AddDelegation creates a valid changefile (rejects invalid delegation names,
// but does not check the delegation hierarchy). When applied, the change adds
// a new delegation role with the correct keys.
func TestAddDelegationChangefileValid(t *testing.T) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
defer os.RemoveAll(tempBaseDir)
assert.NoError(t, err, "failed to create a temporary directory: %s", err)
gun := "docker.com/notary"
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, data.ECDSAKey, tempBaseDir, gun, ts.URL, false)
targetKeyIds := repo.CryptoService.ListKeys(data.CanonicalTargetsRole)
assert.NotEmpty(t, targetKeyIds)
targetPubKey := repo.CryptoService.GetKey(targetKeyIds[0])
assert.NotNil(t, targetPubKey)
err = repo.AddDelegation("root", 1, []data.PublicKey{targetPubKey})
assert.Error(t, err)
assert.IsType(t, data.ErrInvalidRole{}, err)
assert.Empty(t, getChanges(t, repo))
// to show that adding does not care about the hierarchy
err = repo.AddDelegation("targets/a/b/c", 1, []data.PublicKey{targetPubKey})
assert.NoError(t, err)
// ensure that the changefiles is correct
changes := getChanges(t, repo)
assert.Len(t, changes, 1)
assert.Equal(t, changelist.ActionCreate, 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.NotEmpty(t, changes[0].Content())
}
// The changefile produced by AddDelegation, when applied, actually adds
// the delegation to the repo (assuming the delegation hierarchy is correct -
// tests for change application validation are in helpers_test.go)
func TestAddDelegationChangefileApplicable(t *testing.T) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
defer os.RemoveAll(tempBaseDir)
assert.NoError(t, err, "failed to create a temporary directory: %s", err)
gun := "docker.com/notary"
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, data.ECDSAKey, tempBaseDir, gun, ts.URL, false)
targetKeyIds := repo.CryptoService.ListKeys(data.CanonicalTargetsRole)
assert.NotEmpty(t, targetKeyIds)
targetPubKey := repo.CryptoService.GetKey(targetKeyIds[0])
assert.NotNil(t, targetPubKey)
// this hierarchy has to be right to be applied
err = repo.AddDelegation("targets/a", 1, []data.PublicKey{targetPubKey})
assert.NoError(t, err)
changes := getChanges(t, repo)
assert.Len(t, changes, 1)
// ensure that it can be applied correctly
err = applyTargetsChange(repo.tufRepo, changes[0])
assert.NoError(t, err)
targetRole := repo.tufRepo.Targets[data.CanonicalTargetsRole]
assert.Len(t, targetRole.Signed.Delegations.Roles, 1)
assert.Len(t, targetRole.Signed.Delegations.Keys, 1)
_, ok := targetRole.Signed.Delegations.Keys[targetPubKey.ID()]
assert.True(t, ok)
newDelegationRole := targetRole.Signed.Delegations.Roles[0]
assert.Len(t, newDelegationRole.KeyIDs, 1)
assert.Equal(t, targetPubKey.ID(), newDelegationRole.KeyIDs[0])
assert.Equal(t, "targets/a", newDelegationRole.Name)
}
// TestAddDelegationErrorWritingChanges expects errors writing a change to file
// to be propagated.
func TestAddDelegationErrorWritingChanges(t *testing.T) {
testErrorWritingChangefiles(t, func(repo *NotaryRepository) error {
targetKeyIds := repo.CryptoService.ListKeys(data.CanonicalTargetsRole)
assert.NotEmpty(t, targetKeyIds)
targetPubKey := repo.CryptoService.GetKey(targetKeyIds[0])
assert.NotNil(t, targetPubKey)
return repo.AddDelegation("targets/a", 1, []data.PublicKey{targetPubKey})
})
}
// RemoveDelegation rejects attempts to remove invalidly-named delegations,
// but otherwise does not validate the name of the delegation to remove. This
// test ensures that the changefile generated by RemoveDelegation is correct.
func TestRemoveDelegationChangefileValid(t *testing.T) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
defer os.RemoveAll(tempBaseDir)
assert.NoError(t, err, "failed to create a temporary directory: %s", err)
gun := "docker.com/notary"
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, rootKeyID := initializeRepo(t, data.ECDSAKey, tempBaseDir, gun, ts.URL, false)
rootPubKey := repo.CryptoService.GetKey(rootKeyID)
assert.NotNil(t, rootPubKey)
err = repo.RemoveDelegation("root")
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"))
// 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, "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
// the delegation from the repo (assuming the repo exists - tests for
// change application validation are in helpers_test.go)
func TestRemoveDelegationChangefileApplicable(t *testing.T) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
defer os.RemoveAll(tempBaseDir)
assert.NoError(t, err, "failed to create a temporary directory: %s", err)
gun := "docker.com/notary"
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, rootKeyID := initializeRepo(t, data.ECDSAKey, tempBaseDir, gun, ts.URL, false)
rootPubKey := repo.CryptoService.GetKey(rootKeyID)
assert.NotNil(t, rootPubKey)
// add a delegation first so it can be removed
assert.NoError(t, repo.AddDelegation("targets/a", 1, []data.PublicKey{rootPubKey}))
changes := getChanges(t, repo)
assert.Len(t, changes, 1)
assert.NoError(t, applyTargetsChange(repo.tufRepo, changes[0]))
targetRole := repo.tufRepo.Targets[data.CanonicalTargetsRole]
assert.Len(t, targetRole.Signed.Delegations.Roles, 1)
assert.Len(t, targetRole.Signed.Delegations.Keys, 1)
// now remove it
assert.NoError(t, repo.RemoveDelegation("targets/a"))
changes = getChanges(t, repo)
assert.Len(t, changes, 2)
assert.NoError(t, applyTargetsChange(repo.tufRepo, changes[1]))
targetRole = repo.tufRepo.Targets[data.CanonicalTargetsRole]
assert.Empty(t, targetRole.Signed.Delegations.Roles)
assert.Empty(t, targetRole.Signed.Delegations.Keys)
}
// TestRemoveDelegationErrorWritingChanges expects errors writing a change to
// file to be propagated.
func TestRemoveDelegationErrorWritingChanges(t *testing.T) {
testErrorWritingChangefiles(t, func(repo *NotaryRepository) error {
return repo.RemoveDelegation("targets/a")
})
}

View File

@ -728,3 +728,38 @@ func TestApplyTargetsDelegationCreate2Deep(t *testing.T) {
assert.Equal(t, "targets/level1/level2", role.Name)
assert.Equal(t, "level1/level2", role.Paths[0])
}
// Applying a delegation whose parent doesn't exist fails.
func TestApplyTargetsDelegationParentDoesntExist(t *testing.T) {
_, repo, cs := testutils.EmptyRepo()
// make sure a key exists for the previous level, so it's not a missing
// key error, but we don't care about this key
_, err := cs.Create("targets/level1", data.ED25519Key)
assert.NoError(t, err)
newKey, err := cs.Create("targets/level1/level2", data.ED25519Key)
assert.NoError(t, err)
// create delegation
kl := data.KeyList{newKey}
td := &changelist.TufDelegation{
NewThreshold: 1,
AddKeys: kl,
}
tdJSON, err := json.Marshal(td)
assert.NoError(t, err)
ch := changelist.NewTufChange(
changelist.ActionCreate,
"targets/level1/level2",
changelist.TypeTargetsDelegation,
"",
tdJSON,
)
err = applyTargetsChange(repo, ch)
assert.Error(t, err)
assert.IsType(t, data.ErrInvalidRole{}, err)
}