Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TUF Offline Functionality #2363

Closed
wants to merge 6 commits into from

Conversation

emboman13
Copy link

Fixes #2359

This implements an offline configuration for Python TUF. It allows for the updater to exclusively function off of local metadata, given that a configuration in its constructor is set.

Changes are located within ngclient; specifically to config.py, updater.py, and to _internal/trusted_metadata_set.py. These changes include adding an additional configuration option to config.py, changing refresh() within updater.py to use said configuration option, and changing the load functions to support exclusive usage of local data.

Test implementation of this within sigstore can be found here:
https://github.com/emilejbm/sigstore-python/tree/offline-flag

Copy link
Member

@jku jku left a comment

Choose a reason for hiding this comment

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

I'm writing my first review comments as they come to mind -- I may have some additional thoughts but would appreciate your takes on these.

  • I like that it's quite clean already: I was worried it may make the code too cluttered but I think we can work with this...
  • I would like for this feature to be very difficult to misuse -- that you could not e.g. accidentally download targets using an "offline updater": that seems like a user mistake. I think this can easily happen with this design
  • What do you think about making "offline" something you enable at TrustedMetadataSet construction time (which practically means Updater construction time)?
    • then the TrustedMetadataSet could have an offline attribute (or skip_expiry attribute to be more descriptive)
    • it might mean some test setup changes, but the actual code may be simpler as the methods no longer need the arg and you could use the attribute in lower layers of the code (see a code comment)
    • Updater could then fail loudly in download_target() if its being used with a TrustedMetadataSet that is offline
  • I haven't looked closely at the test yet but one additional thing we do need to test is the full use case: basically make sure we can get the locally cached target even if metadata has expired:
    • (setup: make sure metadata and a target are locally cached, and that metadata is now considered expired)
    • create offline Updater
    • get_targetinfo() for the target
    • find_cached_target()

I'm open to additional tests happening in other PRs if that seems to complicate this one too much

tuf/ngclient/updater.py Outdated Show resolved Hide resolved
Comment on lines 343 to 358
if not offline:
self._check_final_snapshot()
else:
snapshot_meta = self.timestamp.signed.snapshot_meta
if self.snapshot.signed.version != snapshot_meta.version:
raise exceptions.BadVersionNumberError(
f"Expected snapshot version {snapshot_meta.version}, "
f"got {self.snapshot.signed.version}"
)
Copy link
Member

@jku jku Apr 26, 2023

Choose a reason for hiding this comment

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

I usually don't mind duplicating a bit of code but this has a bit of a smell... I think we could avoid this if the whole TrustedMetadataSet instance was "offline": then the lower level code (in this instance _check_final_snapshot()) could have the differing code paths and this code here would not need changes

even in he simpler cases, like _check_final_timestamp(), it might make the code easier to understand if the "offline" check was made inside the function (instead of in two separate places where the function is called)...

Copy link
Author

Choose a reason for hiding this comment

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

Yea we were going back and forth for a little while on how we should deal with this. We'll modify _check_final_snapshot() to have differing codepaths

Copy link

@emilejbm emilejbm May 4, 2023

Choose a reason for hiding this comment

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

Thanks for all the feedback! We've made changes to TrustedMetadataSet so that it allows the offline flag to be set within this object and all methods now take advantage of this.

@jku
Copy link
Member

jku commented Apr 26, 2023

I'll keep an eye on this PR too but if you want to discuss things more interactively the python-tuf slack channel is a good choice (I know slack is a pain if you're not on it already, sorry)

On the test results:

  • squashing to a single commit is fine by me for something as well contained as your work is (then you'll only have to deal with DCO signed-off-by in that one commit and not try to edit all commits)
  • linting is available locally as well, it just doesn't run automatically when the actual test suite runs: tox -e lint

@emboman13
Copy link
Author

@jku we've implemented the changes you've suggested and ran a lint on our code, is there anything else we need to do?

@jku
Copy link
Member

jku commented May 5, 2023

Have not looked at code yet but quick comments:

  • the doc build failure is unrelated, don't mind that
  • something wen't wrong with the signed-off-by: it looks like you've added signed-off-bys to commits that already exist in develop branch: this is not ok. Squashing your changes to single commit is ok if you'd rather do that.

@jku
Copy link
Member

jku commented May 5, 2023

Code looks pretty good.

  • linter is correctly flagging some expression-not-assigned -- I think these are just leftovers from the first version
  • linter complaint too-many-instance-attributes can be disabled for this specific case in config.py. git grep "pylint: disable" will show examples of this
  • I would like download_target() to fail if updater is offline (RuntimeError("Cannot download when offline") maybe?)
  • what happens when you do get_targetinfo() while offline, when the target is delegated, and our local cache does not contain the delegated roles metadata? I think this should fail instead of downloading the metadata.

I've not tested this yet myself or looked at the test code, was planning to do that once lint and tests are passing -- let me know if you'd like assistance before that

@jku jku mentioned this pull request May 5, 2023
@emboman13
Copy link
Author

Have not looked at code yet but quick comments:

  • the doc build failure is unrelated, don't mind that

  • something wen't wrong with the signed-off-by: it looks like you've added signed-off-bys to commits that already exist in develop branch: this is not ok. Squashing your changes to single commit is ok if you'd rather do that.

Yea, believe it's an issue with catching commits that were picked up when syncing our fork. We should be able to revert back to before the retroactive signing and just do a final signed squash

@emboman13
Copy link
Author

Code looks pretty good.

  • linter is correctly flagging some expression-not-assigned -- I think these are just leftovers from the first version

  • linter complaint too-many-instance-attributes can be disabled for this specific case in config.py. git grep "pylint: disable" will show examples of this

  • I would like download_target() to fail if updater is offline (RuntimeError("Cannot download when offline") maybe?)

  • what happens when you do get_targetinfo() while offline, when the target is delegated, and our local cache does not contain the delegated roles metadata? I think this should fail instead of downloading the metadata.

I've not tested this yet myself or looked at the test code, was planning to do that once lint and tests are passing -- let me know if you'd like assistance before that

Will take a look into this first chance I get.

@emboman13
Copy link
Author

Sorry for the delay. We have been busy with graduation/post-graduation travels. We will be looking into this this weekend.

omartounsi7 and others added 3 commits May 27, 2023 16:21
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

fixed error

Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

nglclient updater refresh method checks for offline mode

reformat of Jussi's lazy refresh closed PR. Addition of checking for
offline flag passed in from client. Will try to load from local
metadata if either is set. If offline is set and local metadata is
expired or not available, ExpiredMetadataError will be raised.

Tests added for offline being set with and without valid local
metadata. (needs review).

Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

refresh method fix. 164 tests pass, 3 warnings

Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

build(deps): bump ossf/scorecard-action from 2.1.2 to 2.1.3

Bumps [ossf/scorecard-action](https://github.com/ossf/scorecard-action) from 2.1.2 to 2.1.3.
- [Release notes](https://github.com/ossf/scorecard-action/releases)
- [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md)
- [Commits](ossf/scorecard-action@e38b190...80e868c)

---
updated-dependencies:
- dependency-name: ossf/scorecard-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

build(deps): bump pylint from 2.17.1 to 2.17.2

Bumps [pylint](https://github.com/PyCQA/pylint) from 2.17.1 to 2.17.2.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](pylint-dev/pylint@v2.17.1...v2.17.2)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

build(deps): bump pypa/gh-action-pypi-publish from 1.8.3 to 1.8.5

Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.3 to 1.8.5.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](pypa/gh-action-pypi-publish@48b317d...0bf742b)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

build(deps): bump mypy from 1.1.1 to 1.2.0

Bumps [mypy](https://github.com/python/mypy) from 1.1.1 to 1.2.0.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](python/mypy@v1.1.1...v1.2.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

build(deps): bump coverage from 7.2.2 to 7.2.3

Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.2.2 to 7.2.3.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](nedbat/coveragepy@7.2.2...7.2.3)

---
updated-dependencies:
- dependency-name: coverage
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

build(deps): bump github/codeql-action from 2.2.9 to 2.2.11

Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2.2.9 to 2.2.11.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@04df126...d186a2a)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

build(deps): bump actions/github-script from 6.4.0 to 6.4.1

Bumps [actions/github-script](https://github.com/actions/github-script) from 6.4.0 to 6.4.1.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](actions/github-script@98814c5...d7906e4)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

Removed lazy refresh config

Signed-off-by: Emerson Rounds <65183924+emboman13@users.noreply.github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

Removed lazy refresh configuration option

Signed-off-by: Emerson Rounds <65183924+emboman13@users.noreply.github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

Removed lazy refresh tests and updated offline tests

Signed-off-by: Emerson Rounds <65183924+emboman13@users.noreply.github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

build(deps): bump cryptography from 40.0.1 to 40.0.2

Bumps [cryptography](https://github.com/pyca/cryptography) from 40.0.1 to 40.0.2.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](pyca/cryptography@40.0.1...40.0.2)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

build(deps): bump actions/checkout from 3.5.0 to 3.5.2

Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.0 to 3.5.2.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@8f4b7f8...8e5e7e5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

build(deps): bump github/codeql-action from 2.2.11 to 2.2.12

Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2.2.11 to 2.2.12.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@d186a2a...7df0ce3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

disabled expiry checks in offline mode. updated testing

Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

NetworkUnavailableError exception added

More descriptive exception added for offline mode error case.

Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

build(deps): bump securesystemslib[crypto,pynacl] from 0.27.0 to 0.28.0

Bumps [securesystemslib[crypto,pynacl]](https://github.com/secure-systems-lab/securesystemslib) from 0.27.0 to 0.28.0.
- [Release notes](https://github.com/secure-systems-lab/securesystemslib/releases)
- [Changelog](https://github.com/secure-systems-lab/securesystemslib/blob/main/CHANGELOG.md)
- [Commits](secure-systems-lab/securesystemslib@v0.27.0...v0.28.0)

---
updated-dependencies:
- dependency-name: securesystemslib[crypto,pynacl]
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

build(deps): bump actions/setup-python from 4.5.0 to 4.6.0

Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4.5.0 to 4.6.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](actions/setup-python@d27e3f3...57ded4d)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

build(deps): bump github/codeql-action from 2.2.12 to 2.3.0

Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2.2.12 to 2.3.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@7df0ce3...b2c19fb)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

build(deps): bump pylint from 2.17.2 to 2.17.3

Bumps [pylint](https://github.com/PyCQA/pylint) from 2.17.2 to 2.17.3.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](pylint-dev/pylint@v2.17.2...v2.17.3)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

Removed vestigial code from updater

Signed-off-by: Emerson Rounds <65183924+emboman13@users.noreply.github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

Removed unused error

Signed-off-by: Emerson Rounds <65183924+emboman13@users.noreply.github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

build(deps): bump coverage from 7.2.3 to 7.2.4

Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.2.3 to 7.2.4.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](nedbat/coveragepy@7.2.3...7.2.4)

---
updated-dependencies:
- dependency-name: coverage
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

build(deps): bump github/codeql-action from 2.3.0 to 2.3.2

Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2.3.0 to 2.3.2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@b2c19fb...f3feb00)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

build(deps): bump requests from 2.28.2 to 2.29.0

Bumps [requests](https://github.com/psf/requests) from 2.28.2 to 2.29.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](psf/requests@v2.28.2...v2.29.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

build(deps): bump coverage from 7.2.4 to 7.2.5

Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.2.4 to 7.2.5.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](nedbat/coveragepy@7.2.4...7.2.5)

---
updated-dependencies:
- dependency-name: coverage
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

offline mode added to TrustedMetadataSet

creation of TrustedMetadataSet object can take additional argument
that describes whether client wants to be in offline mode. Code linted
according to tox -e lint.

Signed-off-by: Emile Baez <ebaezmunne@gmail.com>
…downloading data in offline mode

Signed-off-by: Emerson Rounds <65183924+emboman13@users.noreply.github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>
Signed-off-by: omartounsi7 <62721212+omartounsi7@users.noreply.github.com>
Signed-off-by: Emile Baez <ebaezmunne@gmail.com>
@emboman13
Copy link
Author

We've made the changes you recommended; are there any other fixes you want us to make before this is in a state to merge? We do think our test cases are a little rudimentary at the moment and could use some outside input. @jku

…oo-many-instance-attributes

Signed-off-by: Emile Baez <ebaezmunne@gmail.com>

typo fix

Signed-off-by: Emile Baez <ebaezmunne@gmail.com>
@jku
Copy link
Member

jku commented May 29, 2023

Yes the test case(s) do need a bit of work...

  • there seems to be some confusion about how TestCase.assertRaises() TestCase.assertTrue() work. The test file has usage examples and unittest docs are decent: https://docs.python.org/3/library/unittest.html
  • the usage of this new API within the test seems a bit weird now after the latest changes as the updater is created with default config (I assume this creates TrustedMetadataSet with offline=False argument)... and then afterwards updater.config.offline is set. This seems wrong and I'd rather see the configuration being a constructor argument to Updater -- in the test code you could add an optional "offline" argument to _init_updater()
  • find_cached_target() should also be tested while offline

but lets's get the PR to an otherwise good state first: there is still something wrong in the first commit. It now contains changes that are clearly unrelated -- I'm guessing these are merged commits from upstream develop to your branch that were all squashed into one commit with your changes? I did say that "Squashing your changes to single commit is ok" but the effective term there was your changes :) Please try to fix that. If it feels like you can't salvage it, let me know and I'll give it a shot.

Rough outline of what I'd try:

  • rebase the branch on top of current develop -- expect a few conflicts because of the strange changes
  • fix any conflicts and finish the rebase
  • check that unrelated files are not being changed in the commits in your branch (basically git diff --stat origin/main..<mybranch> assuming origin is upstream python-tuf repo) -- if there are still changes are, remove them in a new commit
  • final check that changes in the branch look correct

For next PR I would suggest using a topic branch (like in this case a branch called "offline" instead of "develop"): keeping rebases and merges in check is easier when branches don't all have the same name

@emboman13
Copy link
Author

Yes the test case(s) do need a bit of work...

  • there seems to be some confusion about how TestCase.assertRaises() TestCase.assertTrue() work. The test file has usage examples and unittest docs are decent: https://docs.python.org/3/library/unittest.html

  • the usage of this new API within the test seems a bit weird now after the latest changes as the updater is created with default config (I assume this creates TrustedMetadataSet with offline=False argument)... and then afterwards updater.config.offline is set. This seems wrong and I'd rather see the configuration being a constructor argument to Updater -- in the test code you could add an optional "offline" argument to _init_updater()

  • find_cached_target() should also be tested while offline

but lets's get the PR to an otherwise good state first: there is still something wrong in the first commit. It now contains changes that are clearly unrelated -- I'm guessing these are merged commits from upstream develop to your branch that were all squashed into one commit with your changes? I did say that "Squashing your changes to single commit is ok" but the effective term there was your changes :) Please try to fix that. If it feels like you can't salvage it, let me know and I'll give it a shot.

Rough outline of what I'd try:

  • rebase the branch on top of current develop -- expect a few conflicts because of the strange changes

  • fix any conflicts and finish the rebase

  • check that unrelated files are not being changed in the commits in your branch (basically git diff --stat origin/main..<mybranch> assuming origin is upstream python-tuf repo) -- if there are still changes are, remove them in a new commit

  • final check that changes in the branch look correct

For next PR I would suggest using a topic branch (like in this case a branch called "offline" instead of "develop"): keeping rebases and merges in check is easier when branches don't all have the same name

I should be able to directly fix the conflicts via text editor here. I'm pretty sure just discarding all the code on this fork that's conflicting with the main repo should fix the issue

Signed-off-by: Emerson Rounds <65183924+emboman13@users.noreply.github.com>
Copy link
Member

@jku jku left a comment

Choose a reason for hiding this comment

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

The following suggestions about using the _trusted_set offline state instead of config.offline are related to the confusion that I think is going on in the test case.

The suggestions should not affect functionality if caller is doing the right thing, but if the user incorrectly modifies the config during the lifetime of the updater (like I believe current test does), our understanding of "offline" should not change: The TrustedMetadataSet offline state (and thus Updater offline state) should be immutable.

I'll have a closer look at testing next next week but the comments so far might already be useful

@@ -129,6 +131,17 @@ def refresh(self) -> None:
DownloadError: Download of a metadata file failed in some way
"""

if self.config.offline:
Copy link
Member

Choose a reason for hiding this comment

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

This should probably check self._trusted_set.offline

Copy link
Author

Choose a reason for hiding this comment

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

Would that be necessary? That is just set as self.config,offline on line 106-107?

Copy link
Member

Choose a reason for hiding this comment

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

self.config.offline can be modified by the user after the Updater has been initialized but before the other methods are called. This does not make a lot of sense, but technically it is possible. In other words, if we check check self.config.offline here it may well be False even though self._trusted_set has been initialized as offline.

The current test case seems to be a good example of this.


Returns:
Local path to downloaded file
"""

if self.config.offline:
Copy link
Member

Choose a reason for hiding this comment

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

This should probably check self._trusted_set.offline

@@ -386,6 +403,9 @@ def _load_targets(self, role: str, parent_role: str) -> Metadata[Targets]:
logger.debug("Local %s is valid: not downloading new one", role)
return delegated_targets
except (OSError, exceptions.RepositoryError) as e:
# fails if local data is unavalible and in offline mode
if self.config.offline:
Copy link
Member

Choose a reason for hiding this comment

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

This should probably check self._trusted_set.offline

@emilejbm
Copy link

emilejbm commented Jun 7, 2023

Thanks for all the feedback! We've drafted a correction adding the optional flag for _init_updater() as well as correctly using assertRaises() and checking for the offline state of _trusted_set instead of the Updater.

We realized the first case we had written might not be needed at all. Here, we wanted to make sure the Updater (without metadata) behaved as it did before, with the addition of having the offline flag (set to False). Before committing anything, do you think we should remove this case altogether, given that it should technically already be tested for?

self.sim.fetch_tracker.metadata.clear()
with patch("datetime.datetime", mock_time):
    updater = self._init_updater()
    with self.assertRaises(
            (OSError, RepositoryError, DownloadError),
        ):
        updater.refresh()

Regarding the test for find_cached_target() while offline, is doing the following sound?

  1. Initialize the Updater (offline=True)
  2. Get the target info
  3. Make sure download_target() raises RuntimeError
  4. Assert the updater.find_cached_target(info) is None

@jku
Copy link
Member

jku commented Jun 13, 2023

We realized the first case we had written might not be needed at all. Here, we wanted to make sure the Updater (without metadata) behaved as it did before, with the addition of having the offline flag (set to False). Before committing anything, do you think we should remove this case altogether, given that it should technically already be tested for?

I'm not quite sure how to answer since the test as is does not quite work... I'll write some thoughts down hope it helps.

  • only mock the time (by patching datetime) if you need to, and when you do please add comment why
  • note that you can create multiple updaters in a single test: e.g. first an updater to populate the cache without offline, then another offline one to test a specific scenario
  • possible test cases for test_updater_top_level_update:
    • initialize new Updater with offline=True, refresh. Expect failure because top level metadata is missing locally
    • initialize an Updater, refresh to get metadata in local cache. initialize new Updater with offline=True, refresh. Expect success, use fetch_tracker.metadata to ensure nothing was downloaded while offline
    • initialize an Updater, refresh. mock time so that some metadata is now expired. Initialize new Updater with offline=True, refresh. Expect success because expiry is no longer a failure case when offline. use fetch_tracker.metadata to ensure nothing was downloaded
  • possible test cases for artifact download tests (assume test repository that contains target files artifactA in top level targets and artifactB in a delegated role):
    • initialize an Updater, refresh to get local metadata. initialize new Updater with offline=True. Run get_targetinfo() on artifactA, then find_cached_target(). Axpect failure because the artifact is not locally cached.
    • initialize an Updater, refresh() to get local metadata. initialize new Updater with offline=True. Run get_targetinfo() on artifactB , expect failure because the delegated metadata is not locally cached.
    • initialize an Updater, run get_targetinfo+download_target() on artifactB to get the toplevel and delegated metadata and target file in local cache. initialize a new Updater with offline=True, run get_targetinfo+find_cached_target() on artifactB. Expect Success because the target and all metadata is in cache

@woodruffw
Copy link
Contributor

Gentle ping on this: sigstore/sigstore-python#483 is blocked upstream by an offline configuration for TUF; it'd be great to have an expected/supported API here rather than brute-forcing offline support in on our end 🙂

@jku
Copy link
Member

jku commented Jul 27, 2023

Gentle ping on this: sigstore/sigstore-python#483 is blocked upstream by an offline configuration for TUF; it'd be great to have an expected/supported API here rather than brute-forcing offline support in on our end slightly

Thanks. I'll be out for next week but I can put a bit of effort in this after that.

Can you confirm that #2359 (comment) describes the functionality you want? There's clearly multiple possible definitions of "offline" and I don't want python-tuf to end up with one that isn't useful: adding complexity to the verification code is not great so I want to be sure it's at least useful complexity

@woodruffw
Copy link
Contributor

Can you confirm that #2359 (comment) describes the functionality you want? There's clearly multiple possible definitions of "offline" and I don't want python-tuf to end up with one that isn't useful: adding complexity to the verification code is not great so I want to be sure it's at least useful complexity

Yep, I just took a look and left a follow-up there -- I think it's 99% of what we want, with one open question about whether we should "fail open" in offline mode with expired metadata.

Signed-off-by: Emile Baez <ebaezmunne@gmail.com>
@jku
Copy link
Member

jku commented Sep 5, 2023

I've dropped the ball on this one (I think I was waiting for some tests to appear), sorry. I will have a look this week

@jku
Copy link
Member

jku commented Oct 2, 2023

This has been superseded by #2472 which contains this branch contents, loads of tests and some improvements from me.

I'm sorry for not including the original commits in that branch: this because of some merge issues in this branch. I've preserved the author info with a "Co-Authored-By", I hope that is acceptable to everyone. If there's feedback on this or anything else, please comment in #2472.

@jku jku closed this Oct 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implementation of Offline mode for TUF
5 participants