Skip to content
This repository has been archived by the owner on Feb 14, 2024. It is now read-only.

Delegation Support #27

Merged
merged 19 commits into from
Jul 6, 2017
Merged

Delegation Support #27

merged 19 commits into from
Jul 6, 2017

Conversation

murphybytes
Copy link
Contributor

WIP This PR adds delegation support per the TUF spec. Note that currently the save and backup file handling only supports the fixed roles root, timestamp, snapshot, and targets. targets will now need handle a tree path structure so I'm in the process of handling that. I'm also in the process of adding more tests, and test scenarios.

Copy link
Contributor

@groob groob left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a few comments regarding style/structure of the PR.

I'll make a separate review to address correctness but I have to set some time to assess how to traverse the target delegates graph first.

}
defer f.Close()
var result Targets
err = json.NewDecoder(f).Decode(&result)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, but if there's a single return and that return is an error, you should do the error handling on the same line.

if err := ... ; err != nil {

}

func TestMockReader(t *testing.T) {
rdr := mockLocalRepoReader{"test/delegation/0/"}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrap these in a table with a subtest.

{"targets/bar", 3},
}
for _, tc := range tt {
targ, ok := root.targetLookup[tc.role]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use t.Run inside the loop to create subtests for each case. It helps when the test fails because you know which of the cases is causing the error.

}
}

func setupValidationTest(testRoot string) (*Root, *Snapshot, *RootTarget, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when creating a test helper, you can add *testing.T as a required argument, and then call t.Fatal for error handling.

setupValidationTest(t *testing.T, testRoot string) (*Root, *Snapshot, *RootTarget) {
// do some setup
require.Nil(t, err)
}

w.Write(buff)
}))
defer svr.Close()
rootRole, snapshotRole, rootTarget, err := setupValidationTest(testRootPath)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see my comment about passing testing.T. It would remove the need for the following 4 lines of test setup.

if !ok {
return nil
}
// 4.3. **Check for a rollback attack.** The version number of the previous
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

tuf/tuf.go Outdated
errLengthIncorrect = errors.New("file length incorrect")
errNoSuchTarget = errors.New("no such target")
errNotFound = errors.New("remote resource does not exist")
errMaxDelegationsExceeded = errors.New("too many delegations")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is this different from line 30?

tuf/tuf.go Outdated
errMaxDelegationsExceeded = errors.New("too many delegations")
errTargetSeen = errors.New("target already seen in tree")
errFailedIntegrityCheck = errors.New("target file fails integrity check")
errTooManyDelegates = errors.Errorf("number of delegates exceeds max %d", maxDelegationCount)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is max delegationCount in the spec?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep

tuf/roles.go Outdated
return newFim
}

func (f FileIntegrityMeta) equal(fim *FileIntegrityMeta) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the first argument a value but the second a pointer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pass the receiver by value to indicate that it won't be modified. clone creates a new structure so imho it should follow customary Go constructor semantics which usually involve making something and returning a pointer to the new thing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the comment is about equal not clone.

tuf/client.go Outdated
@@ -116,7 +116,7 @@ func NewClient(settings *Settings, opts ...Option) (*Client, error) {
// Note that we expect that we do not use consistent snapshots and delegations are
// not supported because for our purposes, both are unnecessary.
// See https://github.com/theupdateframework/tuf/blob/904fa9b8df8ab8c632a210a2b05fd741e366788a/docs/tuf-spec.txt
func (c *Client) Update() (files map[string]FileIntegrityMeta, latest bool, err error) {
func (c *Client) Update() (files fimMap, latest bool, err error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't return a private type in a public method. It makes it impossible to pass files to a function outside this package.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thank you.

@murphybytes murphybytes requested review from zwass and marpaia July 3, 2017 21:46
Copy link
Contributor

@groob groob left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nits and a few questions.

Still need to review the actual delegation feature at a later time.

tuf/client.go Outdated

client *http.Client
maxResponseSize int64
klock clock.Clock
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change the field to clock

tuf/client.go Outdated
//
// Update gets the current metadata from the notary repository and performs
// requisite checks and validations as specified in the TUF spec section 5.1 'The Client Application'.
// Note that we expect that we do not use consistent snapshots and delegations are
// not supported because for our purposes, both are unnecessary.
// See https://github.com/theupdateframework/tuf/blob/904fa9b8df8ab8c632a210a2b05fd741e366788a/docs/tuf-spec.txt
func (c *Client) Update() (files map[string]FileIntegrityMeta, latest bool, err error) {
latest, err = c.manager.refresh()
func (c *Client) Update() ([]string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind clarifying why it's a []string now? Why is it a change for the better. How would the caller access hashes etc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose of the caller accessing hashes was to detect changes. This change detection process now happens upstream in the processing flow at the end of refreshTargets, where it was happening anyway. In this way you don't have to try to detect changes in two places. Also the change detection process is more straight forward and removes the need to maintain state in the monitor loop. In addition, the earlier way of doing it was broken. The behavior we wanted for monitoring, is that when it starts you want to pull down changes immediately. This was broken because the initial comparison hash was empty and so you have to go through a full refresh cycle to see if any changes happen.

if client.watchedTarget != "" {
go client.monitorTarget()
}
return &client, nil
}

// Update updates the local TUF metadata from a remote repository. If the update is successful,
// a dictionary of available files will be returned.
// a list of files that have changed will be returned.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a list of files that have changed suggests that if someone else calls 'Update' I will not see the local files, so I might get a empty slice of changes, even though actual changes happened. Is that correct?

A bit confused about the rationale.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the set of all targets in your respository, you will get a subset of targets that have changed. So if I understand your question, the answer is no, when changes to a target happens, that target will always be included in the list. There is one exception to this rule. If you have two or more delegates, each having the same target, one delegate will have higher precedence than the other. In this case we only detect changes in the highest precedence target. This behavior is due to TUF specifying a preorder depth first search of the delegate tree, cataloguing the first target it encounters and discarding equal targets as it continues to visit each target in the traversal.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My question is more about Update being invoked 2 times in a row by the same caller vs being invoked by two different callers independently. Who gets the slice? both?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point. In the case of two or more independent callers, this would not work. The first update call will get changes, the second will not. Back to the drawing board.

tuf/client.go Outdated
return errors.Wrap(err, "downloading target")
}
return nil
}

func (c *Client) monitorTarget() {
var hash string
ticker := time.NewTicker(c.checkFrequency).C
ticker := c.klock.NewTicker(c.checkFrequency)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

call .Chan() here, and keep ticker a channel.

tuf/client.go Outdated
if old == "" || old == new {
return
func hasTarget(target string, changes []string) bool {
if changes != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no reason to check if changes is nil. Drop the conditional and just use the for loop.

return errors.Wrap(err, "marshalling role")
}
rolePath := filepath.Join(tufRoot, fileName)
f, err := os.OpenFile(rolePath, os.O_CREATE|os.O_WRONLY, 0644)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If buff is not a reader you're better off using ioutil.WriteFile I think.


rrs := notaryTargetFetcherSettings{
gun: testRootPath,
client: &http.Client{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's a helper so you don't have to create this struct each time.

func TestEndToEnd(t *testing.T) {
testTime, _ := time.Parse(time.UnixDate, "Sat Jul 1 18:00:00 CST 2017")
mockClock := clock.NewMockClock(testTime)
client := &http.Client{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use existing helper for insecure http client.

MirrorURL: mirrorServer.URL,
GUN: testGUN,
}
tc.testCase(t, &settings, client, stagingDir, mockClock)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO the 30+ lines of setup really hurt the readability of this test. You could move all that code to a helper like

settings, cleanup := setupEndToEndTest(t, args,...)
defer cleanup()

tuf/client.go Outdated
for {
files, _, err := c.Update()
changes, err := c.Update()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted in another comment, but this assumes Update is only ever called from inside the for loop.

Update can be called by anyone at any time, so the next time the timer ticks changes will be empty.

Copy link
Contributor

@groob groob left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a few more nits, but this LGTM overall.

tuf/client.go Outdated
if err != nil {
c.notificationHandler("", err)
}
if hasTarget(c.watchedTarget, changes) {
c.download()
new, ok := files[c.watchedTarget]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New is a go keyword. Let's try to avoid using it as a variable.

func restoreTUFRepo(tufRoot, tag string) error {
err := checkForDirectoryPresence(tufRoot)
if err != nil {
var err error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why declare var err error? You only need to create the err in the if scope each time.

if err := ...
That is common across most Go codebases and is also the pattern shown in Effective Go.
Am I missing a specific reason to do that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know why I was thinking I needed these, I don't. I'll change before I merge.

// Restores local TUF repository finding all backup files with a matching tag
// and copying them to normal TUF files.
func restoreTUFRepo(tufRoot, tag string) error {
var err error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the var declaration


// Remove backups files that are older than the time duration specified by age.
func removeAgedBackups(tufRoot string, age time.Duration) error {
var err error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the var declaration

rootRole *Root
snapshotRole *Snapshot
localRootTarget *RootTarget
klock clock.Clock
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename klock to clock

tuf/repo.go Outdated
for _, role := range target.Signed.Delegations.Roles {
err = getDelegatedTarget(rdr, root, role.Name)
// prevent cycles
if err != nil && err == errTargetSeen {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's no possible case where err == nil && err == errTargetSeen. Can we drop the first part of the conditional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true, but that's not what the code on 180 is.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by "not what the code on 180 is"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's if err != nil && err == errTargetSeen look at the first condition.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My comment says that if you shorten that to if err == errTargetSeen you should get the same outcome.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh duh! You're right.

tuf/repo.go Outdated
return &root, nil
}

func getDelegatedTarget(rdr roleFetcher, root *RootTarget, roleName string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rdr should be f or fetcher

}
root.append(string(roleTargets), targ)

for _, delegation := range root.Signed.Delegations.Roles {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible for any part of the root.Signed.Delegations.Roles path to be nil and cause a panic?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, all the child structs are values, not pointers. I suppose it's possible for the composition struct *Target could be nil but in that case it would panic higher in the call stack.

tuf/client.go Outdated
for {
// Get the state of our current files from the local repo
if old == nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

old will only be nil on the first iteration. Can it be created outside the loop?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants