mirror of https://github.com/docker/docs.git
Merge pull request #373 from cyli/add-remove-delegation
Add support for creating/deleting a delegation to Notary Repository
This commit is contained in:
commit
90e22ff5ff
|
@ -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".
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue