diff --git a/client/client.go b/client/client.go index 9c830ef2b8..26aeea8f1c 100644 --- a/client/client.go +++ b/client/client.go @@ -127,9 +127,16 @@ func repositoryFromKeystores(baseDir, gun, baseURL string, rt http.RoundTripper, // Target represents a simplified version of the data TUF operates on, so external // applications don't have to depend on tuf data types. type Target struct { - Name string - Hashes data.Hashes - Length int64 + Name string // the name of the target + Hashes data.Hashes // the hash of the target + Length int64 // the size in bytes of the target +} + +// TargetWithRole represents a Target that exists in a particular role - this is +// produced by ListTargets and GetTargetByName +type TargetWithRole struct { + Target + Role string } // NewTarget is a helper method that returns a Target @@ -411,7 +418,7 @@ func (r *NotaryRepository) RemoveTarget(targetName string, roles ...string) erro // its entries will be strictly shadowed by those in other parts of the "targets/a" // subtree and also the "targets/x" subtree, as we will defer parsing it until // we explicitly reach it in our iteration of the provided list of roles. -func (r *NotaryRepository) ListTargets(roles ...string) ([]*Target, error) { +func (r *NotaryRepository) ListTargets(roles ...string) ([]*TargetWithRole, error) { c, err := r.bootstrapClient() if err != nil { return nil, err @@ -428,7 +435,7 @@ func (r *NotaryRepository) ListTargets(roles ...string) ([]*Target, error) { if len(roles) == 0 { roles = []string{data.CanonicalTargetsRole} } - targets := make(map[string]*Target) + targets := make(map[string]*TargetWithRole) for _, role := range roles { // we don't need to do anything special with removing role from // roles because listSubtree always processes role and only excludes @@ -436,7 +443,7 @@ func (r *NotaryRepository) ListTargets(roles ...string) ([]*Target, error) { r.listSubtree(targets, role, roles...) } - var targetList []*Target + var targetList []*TargetWithRole for _, v := range targets { targetList = append(targetList, v) } @@ -444,7 +451,7 @@ func (r *NotaryRepository) ListTargets(roles ...string) ([]*Target, error) { return targetList, nil } -func (r *NotaryRepository) listSubtree(targets map[string]*Target, role string, exclude ...string) { +func (r *NotaryRepository) listSubtree(targets map[string]*TargetWithRole, role string, exclude ...string) { excl := make(map[string]bool) for _, r := range exclude { excl[r] = true @@ -460,7 +467,8 @@ func (r *NotaryRepository) listSubtree(targets map[string]*Target, role string, } for name, meta := range tgts.Signed.Targets { if _, ok := targets[name]; !ok { - targets[name] = &Target{Name: name, Hashes: meta.Hashes, Length: meta.Length} + targets[name] = &TargetWithRole{ + Target: Target{Name: name, Hashes: meta.Hashes, Length: meta.Length}, Role: role} } } for _, d := range tgts.Signed.Delegations.Roles { @@ -478,7 +486,7 @@ func (r *NotaryRepository) listSubtree(targets map[string]*Target, role string, // the target entry found in the subtree of the highest priority role // will be returned // See the IMPORTANT section on ListTargets above. Those roles also apply here. -func (r *NotaryRepository) GetTargetByName(name string, roles ...string) (*Target, error) { +func (r *NotaryRepository) GetTargetByName(name string, roles ...string) (*TargetWithRole, error) { c, err := r.bootstrapClient() if err != nil { return nil, err @@ -495,13 +503,11 @@ func (r *NotaryRepository) GetTargetByName(name string, roles ...string) (*Targe if len(roles) == 0 { roles = append(roles, data.CanonicalTargetsRole) } - var ( - meta *data.FileMeta - ) for _, role := range roles { - meta = c.TargetMeta(role, name, roles...) + meta, foundRole := c.TargetMeta(role, name, roles...) if meta != nil { - return &Target{Name: name, Hashes: meta.Hashes, Length: meta.Length}, nil + return &TargetWithRole{ + Target: Target{Name: name, Hashes: meta.Hashes, Length: meta.Length}, Role: foundRole}, nil } } return nil, fmt.Errorf("No trust data for %s", name) diff --git a/client/client_test.go b/client/client_test.go index 06773d32fa..d99e7750aa 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -869,7 +869,7 @@ func fakeServerData(t *testing.T, repo *NotaryRepository, mux *http.ServeMux, } // We want to sort by name, so we can guarantee ordering. -type targetSorter []*Target +type targetSorter []*TargetWithRole func (k targetSorter) Len() int { return len(k) } func (k targetSorter) Swap(i, j int) { k[i], k[j] = k[j], k[i] } @@ -910,18 +910,25 @@ func testListTarget(t *testing.T, rootType string) { sort.Stable(targetSorter(targets)) + // the targets should both be found in the targets role + for _, foundTarget := range targets { + assert.Equal(t, data.CanonicalTargetsRole, foundTarget.Role) + } + // current should be first - assert.Equal(t, currentTarget, targets[0], "current target does not match") - assert.Equal(t, latestTarget, targets[1], "latest target does not match") + assert.True(t, reflect.DeepEqual(*currentTarget, targets[0].Target), "current target does not match") + assert.True(t, reflect.DeepEqual(*latestTarget, targets[1].Target), "latest target does not match") // Also test GetTargetByName newLatestTarget, err := repo.GetTargetByName("latest") assert.NoError(t, err) - assert.Equal(t, latestTarget, newLatestTarget, "latest target does not match") + assert.Equal(t, data.CanonicalTargetsRole, newLatestTarget.Role) + assert.True(t, reflect.DeepEqual(*latestTarget, newLatestTarget.Target), "latest target does not match") newCurrentTarget, err := repo.GetTargetByName("current") assert.NoError(t, err) - assert.Equal(t, currentTarget, newCurrentTarget, "current target does not match") + assert.Equal(t, data.CanonicalTargetsRole, newCurrentTarget.Role) + assert.True(t, reflect.DeepEqual(*currentTarget, newCurrentTarget.Target), "current target does not match") } func testListTargetWithDelegates(t *testing.T, rootType string) { @@ -982,48 +989,66 @@ func testListTargetWithDelegates(t *testing.T, rootType string) { targets, err := repo.ListTargets() assert.NoError(t, err) - // Should be two targets + // Should be four targets assert.Len(t, targets, 4, "unexpected number of targets returned by ListTargets") sort.Stable(targetSorter(targets)) // current should be first. - assert.Equal(t, currentTarget, targets[0], "current target does not match") - assert.Equal(t, latestTarget, targets[1], "latest target does not match") - assert.Equal(t, level2Target, targets[2], "level2 target does not match") - assert.Equal(t, otherTarget, targets[3], "other target does not match") + assert.True(t, reflect.DeepEqual(*currentTarget, targets[0].Target), "current target does not match") + assert.Equal(t, data.CanonicalTargetsRole, targets[0].Role) + + assert.True(t, reflect.DeepEqual(*latestTarget, targets[1].Target), "latest target does not match") + assert.Equal(t, data.CanonicalTargetsRole, targets[1].Role) + + assert.True(t, reflect.DeepEqual(*level2Target, targets[2].Target), "level2 target does not match") + assert.Equal(t, "targets/level2", targets[2].Role) + + assert.True(t, reflect.DeepEqual(*otherTarget, targets[3].Target), "other target does not match") + assert.Equal(t, "targets/level1", targets[3].Role) // test listing with priority specified targets, err = repo.ListTargets("targets/level1", data.CanonicalTargetsRole) assert.NoError(t, err) - // Should be two targets + // Should be four targets assert.Len(t, targets, 4, "unexpected number of targets returned by ListTargets") sort.Stable(targetSorter(targets)) - // current should be first - assert.Equal(t, delegatedTarget, targets[0], "current target does not match") - assert.Equal(t, latestTarget, targets[1], "latest target does not match") - assert.Equal(t, level2Target, targets[2], "level2 target does not match") - assert.Equal(t, otherTarget, targets[3], "other target does not match") + // current (in delegated role) should be first + assert.True(t, reflect.DeepEqual(*delegatedTarget, targets[0].Target), "current target does not match") + assert.Equal(t, "targets/level1", targets[0].Role) + + assert.True(t, reflect.DeepEqual(*latestTarget, targets[1].Target), "latest target does not match") + assert.Equal(t, data.CanonicalTargetsRole, targets[1].Role) + + assert.True(t, reflect.DeepEqual(*level2Target, targets[2].Target), "level2 target does not match") + assert.Equal(t, "targets/level2", targets[2].Role) + + assert.True(t, reflect.DeepEqual(*otherTarget, targets[3].Target), "other target does not match") + assert.Equal(t, "targets/level1", targets[3].Role) // Also test GetTargetByName newLatestTarget, err := repo.GetTargetByName("latest") assert.NoError(t, err) - assert.Equal(t, latestTarget, newLatestTarget, "latest target does not match") + assert.True(t, reflect.DeepEqual(*latestTarget, newLatestTarget.Target), "latest target does not match") + assert.Equal(t, data.CanonicalTargetsRole, newLatestTarget.Role) newCurrentTarget, err := repo.GetTargetByName("current", "targets/level1", "targets") assert.NoError(t, err) - assert.Equal(t, delegatedTarget, newCurrentTarget, "current target does not match") + assert.True(t, reflect.DeepEqual(*delegatedTarget, newCurrentTarget.Target), "current target does not match") + assert.Equal(t, "targets/level1", newCurrentTarget.Role) newOtherTarget, err := repo.GetTargetByName("other") assert.NoError(t, err) - assert.True(t, reflect.DeepEqual(otherTarget, newOtherTarget), "other target does not match") + assert.True(t, reflect.DeepEqual(*otherTarget, newOtherTarget.Target), "other target does not match") + assert.Equal(t, "targets/level1", newOtherTarget.Role) newLevel2Target, err := repo.GetTargetByName("level2") assert.NoError(t, err) - assert.True(t, reflect.DeepEqual(level2Target, newLevel2Target), "level2 target does not match") + assert.True(t, reflect.DeepEqual(*level2Target, newLevel2Target.Target), "level2 target does not match") + assert.Equal(t, "targets/level2", newLevel2Target.Role) } // TestValidateRootKey verifies that the public data in root.json for the root @@ -1270,19 +1295,21 @@ func assertPublishToRolesSucceeds(t *testing.T, repo1 *NotaryRepository, sort.Stable(targetSorter(targets)) - assert.Equal(t, currentTarget, targets[0], "current target does not match") - assert.Equal(t, latestTarget, targets[1], "latest target does not match") + assert.True(t, reflect.DeepEqual(*currentTarget, targets[0].Target), "current target does not match") + assert.Equal(t, role, targets[0].Role) + assert.True(t, reflect.DeepEqual(*latestTarget, targets[1].Target), "latest target does not match") + assert.Equal(t, role, targets[1].Role) // Also test GetTargetByName - if role == data.CanonicalTargetsRole { - newLatestTarget, err := repo.GetTargetByName("latest") - assert.NoError(t, err) - assert.Equal(t, latestTarget, newLatestTarget, "latest target does not match") + newLatestTarget, err := repo.GetTargetByName("latest", role) + assert.NoError(t, err) + assert.True(t, reflect.DeepEqual(*latestTarget, newLatestTarget.Target), "latest target does not match") + assert.Equal(t, role, newLatestTarget.Role) - newCurrentTarget, err := repo.GetTargetByName("current") - assert.NoError(t, err) - assert.Equal(t, currentTarget, newCurrentTarget, "current target does not match") - } + newCurrentTarget, err := repo.GetTargetByName("current", role) + assert.NoError(t, err) + assert.True(t, reflect.DeepEqual(*currentTarget, newCurrentTarget.Target), "current target does not match") + assert.Equal(t, role, newCurrentTarget.Role) } } } @@ -1320,7 +1347,7 @@ func testPublishAfterPullServerHasSnapshotKey(t *testing.T, rootType string) { // list, so that the snapshot metadata is pulled from server targets, err := repo.ListTargets(data.CanonicalTargetsRole) assert.NoError(t, err) - assert.Equal(t, []*Target{published}, targets) + assert.Equal(t, []*TargetWithRole{{Target: *published, Role: data.CanonicalTargetsRole}}, targets) // listing downloaded the timestamp and snapshot metadata info assertRepoHasExpectedMetadata(t, repo, data.CanonicalTimestampRole, true) assertRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, true) diff --git a/cmd/notary/prettyprint.go b/cmd/notary/prettyprint.go index 191220b7f7..96a3ef8225 100644 --- a/cmd/notary/prettyprint.go +++ b/cmd/notary/prettyprint.go @@ -130,7 +130,7 @@ func prettyPrintKeys(keyStores []trustmanager.KeyStore, writer io.Writer) { // --- pretty printing targets --- -type targetsSorter []*client.Target +type targetsSorter []*client.TargetWithRole func (t targetsSorter) Len() int { return len(t) } func (t targetsSorter) Swap(i, j int) { t[i], t[j] = t[j], t[i] } @@ -140,7 +140,7 @@ func (t targetsSorter) Less(i, j int) bool { // Given a list of KeyStores in order of listing preference, pretty-prints the // root keys and then the signing keys. -func prettyPrintTargets(ts []*client.Target, writer io.Writer) { +func prettyPrintTargets(ts []*client.TargetWithRole, writer io.Writer) { if len(ts) == 0 { writer.Write([]byte("\nNo targets present in this repository.\n\n")) return @@ -148,13 +148,14 @@ func prettyPrintTargets(ts []*client.Target, writer io.Writer) { sort.Stable(targetsSorter(ts)) - table := getTable([]string{"Name", "Digest", "Size (bytes)"}, writer) + table := getTable([]string{"Name", "Digest", "Size (bytes)", "Role"}, writer) for _, t := range ts { table.Append([]string{ t.Name, hex.EncodeToString(t.Hashes["sha256"]), fmt.Sprintf("%d", t.Length), + t.Role, }) } table.Render() diff --git a/cmd/notary/prettyprint_test.go b/cmd/notary/prettyprint_test.go index 1231c57fde..afb14dffcc 100644 --- a/cmd/notary/prettyprint_test.go +++ b/cmd/notary/prettyprint_test.go @@ -147,7 +147,7 @@ func TestPrettyPrintRootAndSigningKeys(t *testing.T) { // are no targets. func TestPrettyPrintZeroTargets(t *testing.T) { var b bytes.Buffer - prettyPrintTargets([]*client.Target{}, &b) + prettyPrintTargets([]*client.TargetWithRole{}, &b) text, err := ioutil.ReadAll(&b) assert.NoError(t, err) @@ -157,7 +157,7 @@ func TestPrettyPrintZeroTargets(t *testing.T) { } -// Targets are sorted by name, and the name, SHA256 digest, and size are +// Targets are sorted by name, and the name, SHA256 digest, size, and role are // printed. func TestPrettyPrintSortedTargets(t *testing.T) { hashes := make([][]byte, 3) @@ -166,10 +166,11 @@ func TestPrettyPrintSortedTargets(t *testing.T) { hashes[i], err = hex.DecodeString(letter) assert.NoError(t, err) } - unsorted := []*client.Target{ - {Name: "zebra", Hashes: data.Hashes{"sha256": hashes[0]}, Length: 8}, - {Name: "abracadabra", Hashes: data.Hashes{"sha256": hashes[1]}, Length: 1}, - {Name: "bee", Hashes: data.Hashes{"sha256": hashes[2]}, Length: 5}, + unsorted := []*client.TargetWithRole{ + {Target: client.Target{Name: "zebra", Hashes: data.Hashes{"sha256": hashes[0]}, Length: 8}, Role: "targets/b"}, + {Target: client.Target{Name: "aardvark", Hashes: data.Hashes{"sha256": hashes[1]}, Length: 1}, + Role: "targets"}, + {Target: client.Target{Name: "bee", Hashes: data.Hashes{"sha256": hashes[2]}, Length: 5}, Role: "targets/a"}, } var b bytes.Buffer @@ -178,9 +179,9 @@ func TestPrettyPrintSortedTargets(t *testing.T) { assert.NoError(t, err) expected := [][]string{ - {"abracadabra", "b012", "1"}, - {"bee", "c012", "5"}, - {"zebra", "a012", "8"}, + {"aardvark", "b012", "1", "targets"}, + {"bee", "c012", "5", "targets/a"}, + {"zebra", "a012", "8", "targets/b"}, } lines := strings.Split(strings.TrimSpace(string(text)), "\n") @@ -188,7 +189,7 @@ func TestPrettyPrintSortedTargets(t *testing.T) { // starts with headers assert.True(t, reflect.DeepEqual(strings.Fields(lines[0]), strings.Fields( - "NAME DIGEST SIZE (BYTES)"))) + "NAME DIGEST SIZE (BYTES) ROLE"))) assert.Equal(t, "----", lines[1][:4]) for i, line := range lines[2:] { diff --git a/tuf/client/client.go b/tuf/client/client.go index 98b630d2d6..fbb19e9266 100644 --- a/tuf/client/client.go +++ b/tuf/client/client.go @@ -525,7 +525,7 @@ func (c Client) RoleTargetsPath(role string, hashSha256 string, consistent bool) // TargetMeta ensures the repo is up to date. It assumes downloadTargets // has already downloaded all delegated roles -func (c Client) TargetMeta(role, path string, excludeRoles ...string) *data.FileMeta { +func (c Client) TargetMeta(role, path string, excludeRoles ...string) (*data.FileMeta, string) { excl := make(map[string]bool) for _, r := range excludeRoles { excl[r] = true @@ -548,16 +548,16 @@ func (c Client) TargetMeta(role, path string, excludeRoles ...string) *data.File meta = c.local.TargetMeta(curr, path) if meta != nil { // we found the target! - return meta + return meta, curr } - delegations := c.local.TargetDelegations(role, path, pathHex) + delegations := c.local.TargetDelegations(curr, path, pathHex) for _, d := range delegations { if !excl[d.Name] { roles = append(roles, d.Name) } } } - return meta + return meta, "" } // DownloadTarget downloads the target to dst from the remote diff --git a/tuf/client/client_test.go b/tuf/client/client_test.go index d62525cb17..57488631e6 100644 --- a/tuf/client/client_test.go +++ b/tuf/client/client_test.go @@ -3,6 +3,7 @@ package client import ( "crypto/sha256" "encoding/json" + "strconv" "testing" "time" @@ -660,3 +661,58 @@ func TestDownloadSnapshotBadChecksum(t *testing.T) { err = client.downloadSnapshot() assert.IsType(t, ErrChecksumMismatch{}, err) } + +// TargetMeta returns the file metadata for a file path in the role subtree, +// if it exists. It also returns the role in that subtree in which the target +// was found. If the path doesn't exist in that role subtree, returns +// nil and an empty string. +func TestTargetMeta(t *testing.T) { + kdb, repo, cs := testutils.EmptyRepo() + localStorage := store.NewMemoryStore(nil, nil) + client := NewClient(repo, nil, kdb, localStorage) + + delegations := []string{ + "targets/level1", + "targets/level1/a", + "targets/level1/a/i", + } + + k, err := cs.Create("", data.ED25519Key) + assert.NoError(t, err) + + hash := sha256.Sum256([]byte{}) + f := data.FileMeta{ + Length: 1, + Hashes: map[string][]byte{ + "sha256": hash[:], + }, + } + + for i, r := range delegations { + // create role + role, err := data.NewRole(r, 1, []string{k.ID()}, []string{""}, nil) + assert.NoError(t, err) + + // add role to repo + repo.UpdateDelegations(role, []data.PublicKey{k}) + repo.InitTargets(r) + + // add a target to the role + _, err = repo.AddTargets(r, data.Files{strconv.Itoa(i): f}) + assert.NoError(t, err) + } + + // returns the right level + fileMeta, role := client.TargetMeta("targets", "1") + assert.Equal(t, &f, fileMeta) + assert.Equal(t, "targets/level1/a", role) + + // looks only in subtree + fileMeta, role = client.TargetMeta("targets/level1/a", "0") + assert.Nil(t, fileMeta) + assert.Equal(t, "", role) + + fileMeta, role = client.TargetMeta("targets/level1/a", "2") + assert.Equal(t, &f, fileMeta) + assert.Equal(t, "targets/level1/a/i", role) +}