mirror of https://github.com/docker/docs.git
Merge pull request #528 from docker/delegation-api
Break down client API for delegations
This commit is contained in:
commit
6acb6a1802
|
@ -88,6 +88,7 @@ type TufDelegation struct {
|
|||
RemoveKeys []string `json:"remove_keys,omitempty"`
|
||||
AddPaths []string `json:"add_paths,omitempty"`
|
||||
RemovePaths []string `json:"remove_paths,omitempty"`
|
||||
ClearAllPaths bool `json:"clear_paths,omitempty"`
|
||||
AddPathHashPrefixes []string `json:"add_prefixes,omitempty"`
|
||||
RemovePathHashPrefixes []string `json:"remove_prefixes,omitempty"`
|
||||
}
|
||||
|
|
164
client/client.go
164
client/client.go
|
@ -24,7 +24,6 @@ import (
|
|||
"github.com/docker/notary/tuf/keys"
|
||||
"github.com/docker/notary/tuf/signed"
|
||||
"github.com/docker/notary/tuf/store"
|
||||
"github.com/docker/notary/tuf/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -302,96 +301,6 @@ 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, paths []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(`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),
|
||||
AddPaths: paths,
|
||||
})
|
||||
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, keyIDs, paths []string, removeAll bool) 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)
|
||||
var template *changelist.TufChange
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// AddTarget creates new changelist entries to add a target to the given roles
|
||||
// in the repository when the changelist gets applied at publish time.
|
||||
// If roles are unspecified, the default role is "targets".
|
||||
|
@ -529,79 +438,6 @@ func (r *NotaryRepository) GetChangelist() (changelist.Changelist, error) {
|
|||
return cl, nil
|
||||
}
|
||||
|
||||
// GetDelegationRoles returns the keys and roles of the repository's delegations
|
||||
// Also converts key IDs to canonical key IDs to keep consistent with signing prompts
|
||||
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{Resource: data.CanonicalTargetsRole}
|
||||
}
|
||||
|
||||
// make a copy of top-level Delegations and only show canonical key IDs
|
||||
allDelegations, err := translateDelegationsToCanonicalIDs(targets.Signed.Delegations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// For the return list, update with a copy that includes canonicalKeyIDs
|
||||
canonicalDelegations, err := translateDelegationsToCanonicalIDs(delegationMeta.Signed.Delegations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allDelegations = append(allDelegations, canonicalDelegations...)
|
||||
// Add nested delegations to the exploration list
|
||||
delegationsList = append(delegationsList, delegationMeta.Signed.Delegations.Roles...)
|
||||
}
|
||||
|
||||
// Convert all key IDs to canonical IDs:
|
||||
return allDelegations, nil
|
||||
}
|
||||
|
||||
func translateDelegationsToCanonicalIDs(delegationInfo data.Delegations) ([]*data.Role, error) {
|
||||
canonicalDelegations := make([]*data.Role, len(delegationInfo.Roles))
|
||||
copy(canonicalDelegations, delegationInfo.Roles)
|
||||
delegationKeys := delegationInfo.Keys
|
||||
for i, delegation := range canonicalDelegations {
|
||||
canonicalKeyIDs := []string{}
|
||||
for _, keyID := range delegation.KeyIDs {
|
||||
pubKey, ok := delegationKeys[keyID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Could not translate canonical key IDs for %s", delegation.Name)
|
||||
}
|
||||
canonicalKeyID, err := utils.CanonicalKeyID(pubKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not translate canonical key IDs for %s: %v", delegation.Name, err)
|
||||
}
|
||||
canonicalKeyIDs = append(canonicalKeyIDs, canonicalKeyID)
|
||||
}
|
||||
canonicalDelegations[i].KeyIDs = canonicalKeyIDs
|
||||
}
|
||||
return canonicalDelegations, nil
|
||||
}
|
||||
|
||||
// RoleWithSignatures is a Role with its associated signatures
|
||||
type RoleWithSignatures struct {
|
||||
Signatures []data.Signature
|
||||
|
|
|
@ -1910,10 +1910,10 @@ func testPublishDelegations(t *testing.T, clearCache, x509Keys bool) {
|
|||
// targets/a, because these should execute in order
|
||||
for _, delgName := range []string{"targets/a", "targets/a/b", "targets/c"} {
|
||||
assert.NoError(t,
|
||||
repo1.AddDelegation(delgName, 1, []data.PublicKey{delgKey}, []string{""}),
|
||||
repo1.AddDelegation(delgName, []data.PublicKey{delgKey}, []string{""}),
|
||||
"error creating delegation")
|
||||
}
|
||||
assert.Len(t, getChanges(t, repo1), 3, "wrong number of changelist files found")
|
||||
assert.Len(t, getChanges(t, repo1), 6, "wrong number of changelist files found")
|
||||
|
||||
var rec *passRoleRecorder
|
||||
if clearCache {
|
||||
|
@ -1932,11 +1932,11 @@ func testPublishDelegations(t *testing.T, clearCache, x509Keys bool) {
|
|||
|
||||
// this should not publish, because targets/z doesn't exist
|
||||
assert.NoError(t,
|
||||
repo1.AddDelegation("targets/z/y", 1, []data.PublicKey{delgKey}, []string{""}),
|
||||
repo1.AddDelegation("targets/z/y", []data.PublicKey{delgKey}, []string{""}),
|
||||
"error creating delegation")
|
||||
assert.Len(t, getChanges(t, repo1), 1, "wrong number of changelist files found")
|
||||
assert.Len(t, getChanges(t, repo1), 2, "wrong number of changelist files found")
|
||||
assert.Error(t, repo1.Publish())
|
||||
assert.Len(t, getChanges(t, repo1), 1, "wrong number of changelist files found")
|
||||
assert.Len(t, getChanges(t, repo1), 2, "wrong number of changelist files found")
|
||||
|
||||
if clearCache {
|
||||
rec.assertAsked(t, nil)
|
||||
|
@ -2035,7 +2035,7 @@ func testPublishTargetsDelgationScopeNoFallbackIfNoKeys(t *testing.T, clearCache
|
|||
}
|
||||
|
||||
// ensure that the role exists
|
||||
assert.NoError(t, repo.AddDelegation("targets/a", 1, []data.PublicKey{aPubKey}, []string{""}))
|
||||
assert.NoError(t, repo.AddDelegation("targets/a", []data.PublicKey{aPubKey}, []string{""}))
|
||||
assert.NoError(t, repo.Publish())
|
||||
|
||||
if clearCache {
|
||||
|
@ -2076,7 +2076,7 @@ func TestPublishTargetsDelgationSuccessLocallyHasRoles(t *testing.T) {
|
|||
for _, delgName := range []string{"targets/a", "targets/a/b"} {
|
||||
delgKey := createKey(t, repo, delgName, false)
|
||||
assert.NoError(t,
|
||||
repo.AddDelegation(delgName, 1, []data.PublicKey{delgKey}, []string{""}),
|
||||
repo.AddDelegation(delgName, []data.PublicKey{delgKey}, []string{""}),
|
||||
"error creating delegation")
|
||||
}
|
||||
|
||||
|
@ -2106,7 +2106,7 @@ func TestPublishTargetsDelgationNoTargetsKeyNeeded(t *testing.T) {
|
|||
for _, delgName := range []string{"targets/a", "targets/a/b"} {
|
||||
delgKey := createKey(t, repo, delgName, false)
|
||||
assert.NoError(t,
|
||||
repo.AddDelegation(delgName, 1, []data.PublicKey{delgKey}, []string{""}),
|
||||
repo.AddDelegation(delgName, []data.PublicKey{delgKey}, []string{""}),
|
||||
"error creating delegation")
|
||||
}
|
||||
|
||||
|
@ -2170,10 +2170,10 @@ func TestPublishTargetsDelgationSuccessNeedsToDownloadRoles(t *testing.T) {
|
|||
|
||||
// owner creates delegations, adds the delegated key to them, and publishes them
|
||||
assert.NoError(t,
|
||||
ownerRepo.AddDelegation("targets/a", 1, []data.PublicKey{aKey}, []string{""}),
|
||||
ownerRepo.AddDelegation("targets/a", []data.PublicKey{aKey}, []string{""}),
|
||||
"error creating delegation")
|
||||
assert.NoError(t,
|
||||
ownerRepo.AddDelegation("targets/a/b", 1, []data.PublicKey{bKey}, []string{""}),
|
||||
ownerRepo.AddDelegation("targets/a/b", []data.PublicKey{bKey}, []string{""}),
|
||||
"error creating delegation")
|
||||
|
||||
assert.NoError(t, ownerRepo.Publish())
|
||||
|
@ -2212,7 +2212,7 @@ func TestPublishTargetsDelgationFromTwoRepos(t *testing.T) {
|
|||
|
||||
// delegation includes both keys
|
||||
assert.NoError(t,
|
||||
repo1.AddDelegation("targets/a", 1, []data.PublicKey{key1, key2}, []string{""}),
|
||||
repo1.AddDelegation("targets/a", []data.PublicKey{key1, key2}, []string{""}),
|
||||
"error creating delegation")
|
||||
|
||||
assert.NoError(t, repo1.Publish())
|
||||
|
@ -2282,7 +2282,7 @@ func TestPublishRemoveDelgationKeyFromDelegationRole(t *testing.T) {
|
|||
|
||||
// owner creates delegation, adds the delegated key to it, and publishes it
|
||||
assert.NoError(t,
|
||||
ownerRepo.AddDelegation("targets/a", 1, []data.PublicKey{aKey}, []string{""}),
|
||||
ownerRepo.AddDelegation("targets/a", []data.PublicKey{aKey}, []string{""}),
|
||||
"error creating delegation")
|
||||
assert.NoError(t, ownerRepo.Publish())
|
||||
|
||||
|
@ -2340,7 +2340,7 @@ func TestPublishRemoveDelgation(t *testing.T) {
|
|||
|
||||
// owner creates delegation, adds the delegated key to it, and publishes it
|
||||
assert.NoError(t,
|
||||
ownerRepo.AddDelegation("targets/a", 1, []data.PublicKey{aKey}, []string{""}),
|
||||
ownerRepo.AddDelegation("targets/a", []data.PublicKey{aKey}, []string{""}),
|
||||
"error creating delegation")
|
||||
assert.NoError(t, ownerRepo.Publish())
|
||||
|
||||
|
@ -2351,7 +2351,7 @@ func TestPublishRemoveDelgation(t *testing.T) {
|
|||
// owner removes delegation
|
||||
aKeyCanonicalID, err := utils.CanonicalKeyID(aKey)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, ownerRepo.RemoveDelegation("targets/a", []string{aKeyCanonicalID}, []string{}, false))
|
||||
assert.NoError(t, ownerRepo.RemoveDelegationKeys("targets/a", []string{aKeyCanonicalID}))
|
||||
assert.NoError(t, ownerRepo.Publish())
|
||||
|
||||
// delegated repo can now no longer publish to delegated role
|
||||
|
@ -2374,7 +2374,7 @@ func TestPublishSucceedsDespiteDelegationCorrupt(t *testing.T) {
|
|||
assert.NoError(t, err, "error creating delegation key")
|
||||
|
||||
assert.NoError(t,
|
||||
repo.AddDelegation("targets/a", 1, []data.PublicKey{delgKey}, []string{""}),
|
||||
repo.AddDelegation("targets/a", []data.PublicKey{delgKey}, []string{""}),
|
||||
"error creating delegation")
|
||||
|
||||
testPublishBadMetadata(t, "targets/a", repo, false, true)
|
||||
|
@ -2609,22 +2609,25 @@ func TestAddDelegationChangefileValid(t *testing.T) {
|
|||
targetPubKey := repo.CryptoService.GetKey(targetKeyIds[0])
|
||||
assert.NotNil(t, targetPubKey)
|
||||
|
||||
err := repo.AddDelegation("root", 1, []data.PublicKey{targetPubKey}, []string{""})
|
||||
err := repo.AddDelegation("root", []data.PublicKey{targetPubKey}, []string{""})
|
||||
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}, []string{""})
|
||||
err = repo.AddDelegation("targets/a/b/c", []data.PublicKey{targetPubKey}, []string{""})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// ensure that the changefiles is correct
|
||||
changes := getChanges(t, repo)
|
||||
assert.Len(t, changes, 1)
|
||||
assert.Len(t, changes, 2)
|
||||
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.Equal(t, changelist.ActionCreate, changes[1].Action())
|
||||
assert.Equal(t, "targets/a/b/c", changes[1].Scope())
|
||||
assert.Equal(t, changelist.TypeTargetsDelegation, changes[1].Type())
|
||||
assert.Equal(t, "", changes[1].Path())
|
||||
assert.NotEmpty(t, changes[0].Content())
|
||||
}
|
||||
|
||||
|
@ -2645,10 +2648,10 @@ func TestAddDelegationChangefileApplicable(t *testing.T) {
|
|||
assert.NotNil(t, targetPubKey)
|
||||
|
||||
// this hierarchy has to be right to be applied
|
||||
err := repo.AddDelegation("targets/a", 1, []data.PublicKey{targetPubKey}, []string{""})
|
||||
err := repo.AddDelegation("targets/a", []data.PublicKey{targetPubKey}, []string{""})
|
||||
assert.NoError(t, err)
|
||||
changes := getChanges(t, repo)
|
||||
assert.Len(t, changes, 1)
|
||||
assert.Len(t, changes, 2)
|
||||
|
||||
// ensure that it can be applied correctly
|
||||
err = applyTargetsChange(repo.tufRepo, changes[0])
|
||||
|
@ -2676,7 +2679,7 @@ func TestAddDelegationErrorWritingChanges(t *testing.T) {
|
|||
targetPubKey := repo.CryptoService.GetKey(targetKeyIds[0])
|
||||
assert.NotNil(t, targetPubKey)
|
||||
|
||||
return repo.AddDelegation("targets/a", 1, []data.PublicKey{targetPubKey}, []string{""})
|
||||
return repo.AddDelegation("targets/a", []data.PublicKey{targetPubKey}, []string{""})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -2693,14 +2696,14 @@ func TestRemoveDelegationChangefileValid(t *testing.T) {
|
|||
rootPubKey := repo.CryptoService.GetKey(rootKeyID)
|
||||
assert.NotNil(t, rootPubKey)
|
||||
|
||||
err := repo.RemoveDelegation("root", []string{rootKeyID}, []string{}, false)
|
||||
err := repo.RemoveDelegationKeys("root", []string{rootKeyID})
|
||||
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", []string{rootKeyID}, []string{}, false))
|
||||
assert.NoError(t, repo.RemoveDelegationKeys("targets/a/b/c", []string{rootKeyID}))
|
||||
|
||||
// ensure that the changefile is correct
|
||||
changes := getChanges(t, repo)
|
||||
|
@ -2711,7 +2714,7 @@ func TestRemoveDelegationChangefileValid(t *testing.T) {
|
|||
assert.Equal(t, "", changes[0].Path())
|
||||
}
|
||||
|
||||
// The changefile produced by RemoveDelegation, when applied, actually removes
|
||||
// The changefile produced by RemoveDelegationKeys, 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) {
|
||||
|
@ -2725,10 +2728,11 @@ func TestRemoveDelegationChangefileApplicable(t *testing.T) {
|
|||
assert.NotNil(t, rootPubKey)
|
||||
|
||||
// add a delegation first so it can be removed
|
||||
assert.NoError(t, repo.AddDelegation("targets/a", 1, []data.PublicKey{rootPubKey}, []string{""}))
|
||||
assert.NoError(t, repo.AddDelegation("targets/a", []data.PublicKey{rootPubKey}, []string{""}))
|
||||
changes := getChanges(t, repo)
|
||||
assert.Len(t, changes, 1)
|
||||
assert.Len(t, changes, 2)
|
||||
assert.NoError(t, applyTargetsChange(repo.tufRepo, changes[0]))
|
||||
assert.NoError(t, applyTargetsChange(repo.tufRepo, changes[1]))
|
||||
|
||||
targetRole := repo.tufRepo.Targets[data.CanonicalTargetsRole]
|
||||
assert.Len(t, targetRole.Signed.Delegations.Roles, 1)
|
||||
|
@ -2737,21 +2741,137 @@ func TestRemoveDelegationChangefileApplicable(t *testing.T) {
|
|||
// now remove it
|
||||
rootKeyCanonicalID, err := utils.CanonicalKeyID(rootPubKey)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, repo.RemoveDelegation("targets/a", []string{rootKeyCanonicalID}, []string{}, false))
|
||||
assert.NoError(t, repo.RemoveDelegationKeys("targets/a", []string{rootKeyCanonicalID}))
|
||||
changes = getChanges(t, repo)
|
||||
assert.Len(t, changes, 2)
|
||||
assert.NoError(t, applyTargetsChange(repo.tufRepo, changes[1]))
|
||||
assert.Len(t, changes, 3)
|
||||
assert.NoError(t, applyTargetsChange(repo.tufRepo, changes[2]))
|
||||
|
||||
targetRole = repo.tufRepo.Targets[data.CanonicalTargetsRole]
|
||||
assert.Empty(t, targetRole.Signed.Delegations.Roles)
|
||||
assert.Empty(t, targetRole.Signed.Delegations.Keys)
|
||||
}
|
||||
|
||||
// The changefile with the ClearAllPaths key set, when applied, actually removes
|
||||
// all paths from the specified delegation in the repo (assuming the repo and delegation exist)
|
||||
func TestClearAllPathsDelegationChangefileApplicable(t *testing.T) {
|
||||
gun := "docker.com/notary"
|
||||
ts, _, _ := simpleTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
repo, rootKeyID := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false)
|
||||
defer os.RemoveAll(repo.baseDir)
|
||||
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", []data.PublicKey{rootPubKey}, []string{"abc,123,xyz,path"}))
|
||||
changes := getChanges(t, repo)
|
||||
assert.Len(t, changes, 2)
|
||||
assert.NoError(t, applyTargetsChange(repo.tufRepo, changes[0]))
|
||||
assert.NoError(t, applyTargetsChange(repo.tufRepo, changes[1]))
|
||||
|
||||
// now clear paths it
|
||||
assert.NoError(t, repo.ClearDelegationPaths("targets/a"))
|
||||
changes = getChanges(t, repo)
|
||||
assert.Len(t, changes, 3)
|
||||
assert.NoError(t, applyTargetsChange(repo.tufRepo, changes[2]))
|
||||
|
||||
delgRoles := repo.tufRepo.Targets[data.CanonicalTargetsRole].Signed.Delegations.Roles
|
||||
assert.Len(t, delgRoles, 1)
|
||||
assert.Len(t, delgRoles[0].Paths, 0)
|
||||
}
|
||||
|
||||
// TestFullAddDelegationChangefileApplicable generates a single changelist with AddKeys and AddPaths set,
|
||||
// (in the old style of AddDelegation) and tests that all of its changes are reflected on publish
|
||||
func TestFullAddDelegationChangefileApplicable(t *testing.T) {
|
||||
gun := "docker.com/notary"
|
||||
ts, _, _ := simpleTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
repo, rootKeyID := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false)
|
||||
defer os.RemoveAll(repo.baseDir)
|
||||
rootPubKey := repo.CryptoService.GetKey(rootKeyID)
|
||||
assert.NotNil(t, rootPubKey)
|
||||
|
||||
key2, err := repo.CryptoService.Create("user", data.ECDSAKey)
|
||||
assert.NoError(t, err)
|
||||
|
||||
delegationName := "targets/a"
|
||||
|
||||
// manually create the changelist object to load multiple keys
|
||||
tdJSON, err := json.Marshal(&changelist.TufDelegation{
|
||||
NewThreshold: notary.MinThreshold,
|
||||
AddKeys: data.KeyList([]data.PublicKey{rootPubKey, key2}),
|
||||
AddPaths: []string{"abc", "123", "xyz"},
|
||||
})
|
||||
change := newCreateDelegationChange(delegationName, tdJSON)
|
||||
cl, err := changelist.NewFileChangelist(filepath.Join(repo.tufRepoPath, "changelist"))
|
||||
addChange(cl, change, delegationName)
|
||||
|
||||
changes := getChanges(t, repo)
|
||||
assert.Len(t, changes, 1)
|
||||
assert.NoError(t, applyTargetsChange(repo.tufRepo, changes[0]))
|
||||
|
||||
delgRoles := repo.tufRepo.Targets[data.CanonicalTargetsRole].Signed.Delegations.Roles
|
||||
assert.Len(t, delgRoles, 1)
|
||||
assert.Len(t, delgRoles[0].Paths, 3)
|
||||
assert.Len(t, delgRoles[0].KeyIDs, 2)
|
||||
assert.Equal(t, delgRoles[0].Name, delegationName)
|
||||
}
|
||||
|
||||
// TestFullRemoveDelegationChangefileApplicable generates a single changelist with RemoveKeys and RemovePaths set,
|
||||
// (in the old style of RemoveDelegation) and tests that all of its changes are reflected on publish
|
||||
func TestFullRemoveDelegationChangefileApplicable(t *testing.T) {
|
||||
gun := "docker.com/notary"
|
||||
ts, _, _ := simpleTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
repo, rootKeyID := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false)
|
||||
defer os.RemoveAll(repo.baseDir)
|
||||
rootPubKey := repo.CryptoService.GetKey(rootKeyID)
|
||||
assert.NotNil(t, rootPubKey)
|
||||
|
||||
key2, err := repo.CryptoService.Create("user", data.ECDSAKey)
|
||||
assert.NoError(t, err)
|
||||
key2CanonicalID, err := utils.CanonicalKeyID(key2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
delegationName := "targets/a"
|
||||
|
||||
assert.NoError(t, repo.AddDelegation(delegationName, []data.PublicKey{rootPubKey, key2}, []string{"abc", "123"}))
|
||||
changes := getChanges(t, repo)
|
||||
assert.Len(t, changes, 2)
|
||||
assert.NoError(t, applyTargetsChange(repo.tufRepo, changes[0]))
|
||||
assert.NoError(t, applyTargetsChange(repo.tufRepo, changes[1]))
|
||||
|
||||
targetRole := repo.tufRepo.Targets[data.CanonicalTargetsRole]
|
||||
assert.Len(t, targetRole.Signed.Delegations.Roles, 1)
|
||||
assert.Len(t, targetRole.Signed.Delegations.Keys, 2)
|
||||
|
||||
// manually create the changelist object to load multiple keys
|
||||
tdJSON, err := json.Marshal(&changelist.TufDelegation{
|
||||
RemoveKeys: []string{key2CanonicalID},
|
||||
RemovePaths: []string{"abc", "123"},
|
||||
})
|
||||
change := newUpdateDelegationChange(delegationName, tdJSON)
|
||||
cl, err := changelist.NewFileChangelist(filepath.Join(repo.tufRepoPath, "changelist"))
|
||||
addChange(cl, change, delegationName)
|
||||
|
||||
changes = getChanges(t, repo)
|
||||
assert.Len(t, changes, 3)
|
||||
assert.NoError(t, applyTargetsChange(repo.tufRepo, changes[2]))
|
||||
|
||||
delgRoles := repo.tufRepo.Targets[data.CanonicalTargetsRole].Signed.Delegations.Roles
|
||||
assert.Len(t, delgRoles, 1)
|
||||
assert.Len(t, delgRoles[0].Paths, 0)
|
||||
assert.Len(t, delgRoles[0].KeyIDs, 1)
|
||||
}
|
||||
|
||||
// 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", []string{""}, []string{}, false)
|
||||
return repo.RemoveDelegationKeysAndPaths("targets/a", []string{""}, []string{})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -2859,10 +2979,10 @@ func testPublishTargetsDelgationCanUseUserKeyWithArbitraryRole(t *testing.T, x50
|
|||
|
||||
// owner creates delegations, adds the delegated key to them, and publishes them
|
||||
assert.NoError(t,
|
||||
ownerRepo.AddDelegation("targets/a", 1, []data.PublicKey{aKey}, []string{""}),
|
||||
ownerRepo.AddDelegation("targets/a", []data.PublicKey{aKey}, []string{""}),
|
||||
"error creating delegation")
|
||||
assert.NoError(t,
|
||||
ownerRepo.AddDelegation("targets/a/b", 1, []data.PublicKey{bKey}, []string{""}),
|
||||
ownerRepo.AddDelegation("targets/a/b", []data.PublicKey{bKey}, []string{""}),
|
||||
"error creating delegation")
|
||||
|
||||
assert.NoError(t, ownerRepo.Publish())
|
||||
|
@ -3016,7 +3136,7 @@ func TestListRoles(t *testing.T) {
|
|||
// Create a delegation on the top level
|
||||
aKey := createKey(t, repo, "user", true)
|
||||
assert.NoError(t,
|
||||
repo.AddDelegation("targets/a", 1, []data.PublicKey{aKey}, []string{""}),
|
||||
repo.AddDelegation("targets/a", []data.PublicKey{aKey}, []string{""}),
|
||||
"error creating delegation")
|
||||
|
||||
assert.NoError(t, repo.Publish())
|
||||
|
@ -3053,7 +3173,7 @@ func TestListRoles(t *testing.T) {
|
|||
// Create another delegation, one level further
|
||||
bKey := createKey(t, repo, "user", true)
|
||||
assert.NoError(t,
|
||||
repo.AddDelegation("targets/a/b", 1, []data.PublicKey{bKey}, []string{""}),
|
||||
repo.AddDelegation("targets/a/b", []data.PublicKey{bKey}, []string{""}),
|
||||
"error creating delegation")
|
||||
|
||||
assert.NoError(t, repo.Publish())
|
||||
|
|
|
@ -0,0 +1,310 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/notary"
|
||||
"github.com/docker/notary/client/changelist"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/docker/notary/tuf/store"
|
||||
"github.com/docker/notary/tuf/utils"
|
||||
)
|
||||
|
||||
// AddDelegation creates changelist entries to add provided delegation public keys and paths.
|
||||
// This method composes AddDelegationRoleAndKeys and AddDelegationPaths (each creates one changelist if called).
|
||||
func (r *NotaryRepository) AddDelegation(name string, delegationKeys []data.PublicKey, paths []string) error {
|
||||
if len(delegationKeys) > 0 {
|
||||
err := r.AddDelegationRoleAndKeys(name, delegationKeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(paths) > 0 {
|
||||
err := r.AddDelegationPaths(name, paths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddDelegationRoleAndKeys creates a changelist entry to add provided delegation public keys.
|
||||
// This method is the simplest way to create a new delegation, because the delegation must have at least
|
||||
// one key upon creation to be valid since we will reject the changelist while validating the threshold.
|
||||
func (r *NotaryRepository) AddDelegationRoleAndKeys(name string, 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, notary.MinThreshold, len(delegationKeys))
|
||||
|
||||
// Defaulting to threshold of 1, since we don't allow for larger thresholds at the moment.
|
||||
tdJSON, err := json.Marshal(&changelist.TufDelegation{
|
||||
NewThreshold: notary.MinThreshold,
|
||||
AddKeys: data.KeyList(delegationKeys),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template := newCreateDelegationChange(name, tdJSON)
|
||||
return addChange(cl, template, name)
|
||||
}
|
||||
|
||||
// AddDelegationPaths creates a changelist entry to add provided paths to an existing delegation.
|
||||
// This method cannot create a new delegation itself because the role must meet the key threshold upon creation.
|
||||
func (r *NotaryRepository) AddDelegationPaths(name string, paths []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(`Adding %s paths to delegation %s\n`, paths, name)
|
||||
|
||||
tdJSON, err := json.Marshal(&changelist.TufDelegation{
|
||||
AddPaths: paths,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template := newCreateDelegationChange(name, tdJSON)
|
||||
return addChange(cl, template, name)
|
||||
}
|
||||
|
||||
// RemoveDelegationKeysAndPaths creates changelist entries to remove provided delegation key IDs and paths.
|
||||
// This method composes RemoveDelegationPaths and RemoveDelegationKeys (each creates one changelist if called).
|
||||
func (r *NotaryRepository) RemoveDelegationKeysAndPaths(name string, keyIDs, paths []string) error {
|
||||
if len(paths) > 0 {
|
||||
err := r.RemoveDelegationPaths(name, paths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(keyIDs) > 0 {
|
||||
err := r.RemoveDelegationKeys(name, keyIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveDelegationRole creates a changelist to remove all paths and keys from a role, and delete the role in its entirety.
|
||||
func (r *NotaryRepository) RemoveDelegationRole(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 := newDeleteDelegationChange(name, nil)
|
||||
return addChange(cl, template, name)
|
||||
}
|
||||
|
||||
// RemoveDelegationPaths creates a changelist entry to remove provided paths from an existing delegation.
|
||||
func (r *NotaryRepository) RemoveDelegationPaths(name string, paths []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 %s paths from delegation "%s"\n`, paths, name)
|
||||
|
||||
tdJSON, err := json.Marshal(&changelist.TufDelegation{
|
||||
RemovePaths: paths,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template := newUpdateDelegationChange(name, tdJSON)
|
||||
return addChange(cl, template, name)
|
||||
}
|
||||
|
||||
// RemoveDelegationKeys creates a changelist entry to remove provided keys from an existing delegation.
|
||||
// When this changelist is applied, if the specified keys are the only keys left in the role,
|
||||
// the role itself will be deleted in its entirety.
|
||||
func (r *NotaryRepository) RemoveDelegationKeys(name string, keyIDs []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 %s keys from delegation "%s"\n`, keyIDs, name)
|
||||
|
||||
tdJSON, err := json.Marshal(&changelist.TufDelegation{
|
||||
RemoveKeys: keyIDs,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template := newUpdateDelegationChange(name, tdJSON)
|
||||
return addChange(cl, template, name)
|
||||
}
|
||||
|
||||
// ClearDelegationPaths creates a changelist entry to remove all paths from an existing delegation.
|
||||
func (r *NotaryRepository) ClearDelegationPaths(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 all paths from delegation "%s"\n`, name)
|
||||
|
||||
tdJSON, err := json.Marshal(&changelist.TufDelegation{
|
||||
ClearAllPaths: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template := newUpdateDelegationChange(name, tdJSON)
|
||||
return addChange(cl, template, name)
|
||||
}
|
||||
|
||||
func newUpdateDelegationChange(name string, content []byte) *changelist.TufChange {
|
||||
return changelist.NewTufChange(
|
||||
changelist.ActionUpdate,
|
||||
name,
|
||||
changelist.TypeTargetsDelegation,
|
||||
"", // no path for delegations
|
||||
content,
|
||||
)
|
||||
}
|
||||
|
||||
func newCreateDelegationChange(name string, content []byte) *changelist.TufChange {
|
||||
return changelist.NewTufChange(
|
||||
changelist.ActionCreate,
|
||||
name,
|
||||
changelist.TypeTargetsDelegation,
|
||||
"", // no path for delegations
|
||||
content,
|
||||
)
|
||||
}
|
||||
|
||||
func newDeleteDelegationChange(name string, content []byte) *changelist.TufChange {
|
||||
return changelist.NewTufChange(
|
||||
changelist.ActionDelete,
|
||||
name,
|
||||
changelist.TypeTargetsDelegation,
|
||||
"", // no path for delegations
|
||||
content,
|
||||
)
|
||||
}
|
||||
|
||||
// GetDelegationRoles returns the keys and roles of the repository's delegations
|
||||
// Also converts key IDs to canonical key IDs to keep consistent with signing prompts
|
||||
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{Resource: data.CanonicalTargetsRole}
|
||||
}
|
||||
|
||||
// make a copy of top-level Delegations and only show canonical key IDs
|
||||
allDelegations, err := translateDelegationsToCanonicalIDs(targets.Signed.Delegations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// For the return list, update with a copy that includes canonicalKeyIDs
|
||||
canonicalDelegations, err := translateDelegationsToCanonicalIDs(delegationMeta.Signed.Delegations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allDelegations = append(allDelegations, canonicalDelegations...)
|
||||
// Add nested delegations to the exploration list
|
||||
delegationsList = append(delegationsList, delegationMeta.Signed.Delegations.Roles...)
|
||||
}
|
||||
|
||||
// Convert all key IDs to canonical IDs:
|
||||
return allDelegations, nil
|
||||
}
|
||||
|
||||
func translateDelegationsToCanonicalIDs(delegationInfo data.Delegations) ([]*data.Role, error) {
|
||||
canonicalDelegations := make([]*data.Role, len(delegationInfo.Roles))
|
||||
copy(canonicalDelegations, delegationInfo.Roles)
|
||||
delegationKeys := delegationInfo.Keys
|
||||
for i, delegation := range canonicalDelegations {
|
||||
canonicalKeyIDs := []string{}
|
||||
for _, keyID := range delegation.KeyIDs {
|
||||
pubKey, ok := delegationKeys[keyID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Could not translate canonical key IDs for %s", delegation.Name)
|
||||
}
|
||||
canonicalKeyID, err := utils.CanonicalKeyID(pubKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not translate canonical key IDs for %s: %v", delegation.Name, err)
|
||||
}
|
||||
canonicalKeyIDs = append(canonicalKeyIDs, canonicalKeyID)
|
||||
}
|
||||
canonicalDelegations[i].KeyIDs = canonicalKeyIDs
|
||||
}
|
||||
return canonicalDelegations, nil
|
||||
}
|
|
@ -136,8 +136,14 @@ func changeTargetsDelegation(repo *tuf.Repo, c changelist.Change) error {
|
|||
if err := r.AddPathHashPrefixes(td.AddPathHashPrefixes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clear all paths if we're given the flag, else remove specified paths
|
||||
if td.ClearAllPaths {
|
||||
r.RemovePaths(r.Paths)
|
||||
} else {
|
||||
r.RemovePaths(td.RemovePaths)
|
||||
}
|
||||
r.RemoveKeys(removeTUFKeyIDs)
|
||||
r.RemovePaths(td.RemovePaths)
|
||||
r.RemovePathHashPrefixes(td.RemovePathHashPrefixes)
|
||||
return repo.UpdateDelegations(r, td.AddKeys)
|
||||
case changelist.ActionDelete:
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/docker/notary"
|
||||
notaryclient "github.com/docker/notary/client"
|
||||
"github.com/docker/notary/passphrase"
|
||||
"github.com/docker/notary/trustmanager"
|
||||
|
@ -139,13 +138,19 @@ func (d *delegationCommander) delegationRemove(cmd *cobra.Command, args []string
|
|||
} else {
|
||||
cmd.Println("Confirmed `yes` from flag")
|
||||
}
|
||||
// Delete the entire delegation
|
||||
err = nRepo.RemoveDelegationRole(role)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove delegation: %v", err)
|
||||
}
|
||||
} else {
|
||||
// Remove any keys or paths that we passed in
|
||||
err = nRepo.RemoveDelegationKeysAndPaths(role, keyIDs, paths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove delegation: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
@ -197,9 +202,7 @@ func (d *delegationCommander) delegationAdd(cmd *cobra.Command, args []string) e
|
|||
}
|
||||
|
||||
// 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)
|
||||
err = nRepo.AddDelegation(role, pubKeys, paths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create delegation: %v", err)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue