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

Refactor: CLI wrapper, export and print services, single entrypoint #105

Merged
merged 8 commits into from
Jan 12, 2023

Conversation

rocodes
Copy link
Contributor

@rocodes rocodes commented Oct 5, 2022

Refactor export PR.

  • Consolidation of main.py and entrypoint.py
  • CLI wrapper instead of embedding commandline calls in export
  • No more *ExportAction and *PrintAction classes; export and print are now each contained in a class called Service, which has public methods exposing their capabilities (export(), check_disk_format() , etc)
  • The Export Service class is compatible with the current version of the client. However, you'll see new_status.py and new_service.py , which correspond more to @gonzalo-bulnes eventual dialog rewrite. The CLI returns the new Status values; the existing (legacy) export service wraps & returns backward-compatible old statuses that the current client is expecting. (When we migrate, we will get rid of the files now called service.py and status.py)
  • Light refactoring of printer routines to mirror the Service style
  • Refactor of control flow in main so that all entry/exit from the service happens in main.py
  • Refactor of tests
  • Test coverage up to 100% on current files (test coverage WIP on the new_service)

This will address the following open issues:

Closes #107, closes #84, closes #44, closes #114, closes #9, closes #25
Towards #70, freedomofpress/securedrop-workstation#265
freedomofpress/securedrop-client#1734 will be implemented but will need corresponding UI changes in the client in order for the change to be user-visible

Test Plan

Setup
From an up-to-date SDW machine:

  • clone sd-large-bullseye-template, let's call it test-sd-large-bullseye-template. Then, in dom0, qvm-tags sd-large-bullseye-template del sd-workstation.
  • Install the test .deb in test-sd-large-bullseye-template using dpkg. To do this, use either the prebuilt .deb file that I will upload and sign when ready for review, or better yet, build your own .deb from the tip of this branch. (If you have a clean dispvm builder environment as described here, put this script in the builder vm, then run ./builder.sh securedrop-export 105 and you'll get a test .deb built and you'll be prompted to copy it into the vm of your choice :) ).
  • Shut down test-sd-large-bullseye-template. Then use either the cli or dom0 Template Manager, and set the test-sd-large-bullseye-template as the templateVM for sd-devices-dvm.
  • Ensure sd-devices (and sd-devices-dvm) are powered down.
  • Start the client.

USB Acceptance tests
Pro tip: Export works in offline mode once a file is already downloaded. If the Tor weather is bad, you will thank me for mentioning this ;)

  • No USB yields "Please insert a USB" screen
  • Non-LUKS USB yields "Either not a LUKS drive, or something else is wrong" (encryption not supported)
  • Multi-partitioned USB yields "Either not a LUKS drive, or something else is wrong" (encryption not supported)
  • Attempt to unlock LUKS drive with wrong passphrase yields passphrase error message, can still retry with good password
  • LUKS single-partition USB yields passphrase prompt and successful export (assuming enough disk space)
  • LUKS single-partition USB that faces error (eg, not enough space on device) yields error message and successful cleanup of tmp (/tmp/xxxx) directory containing export materials on sd-devices dvm
  • Success screen shows at end of export routine

Additional testing

securedrop_export.disk.cli:264(_get_mountpoint) DEBUG: Checking mountpoint
securedrop_export.disk.cli:291(mount_volume) INFO: The device is already mounted
securedrop_export.disk.cli:293(mount_volume) WARNING: Mountpoint was inaccurate, updating

The warning line is optional but will show up if the user manually mounts a drive somewhere we weren't expecting (eg via Nautilus, like in this test plan).

Printer acceptance tests

  • No printer yields "Please connect your printer" screen
  • Test page prints successfully
  • Multi-page prints successfully
  • Unsupported printer yields error screen

@rocodes
Copy link
Contributor Author

rocodes commented Nov 2, 2022

(mild puzzlement since all tests pass locally, but will figure it out)

@rocodes
Copy link
Contributor Author

rocodes commented Nov 21, 2022

(Blocked by/dependent on freedomofpress/securedrop-client#1594)

@rocodes
Copy link
Contributor Author

rocodes commented Nov 29, 2022

Everything's passing locally, but the tests that are failing on CI are the ones where SystemExit is expected to be raised. Will investigate tomorrow.

----------- coverage: platform linux, python 3.9.2-final-0 -----------
Name                                    Stmts   Miss  Cover   Missing
---------------------------------------------------------------------
securedrop_export/__init__.py               1      0   100%
securedrop_export/archive.py               59      0   100%
securedrop_export/command.py                9      0   100%
securedrop_export/directory_util.py        63      0   100%
securedrop_export/disk/__init__.py          0      0   100%
securedrop_export/disk/cli.py             193      0   100%
securedrop_export/disk/new_service.py      60     60     0%   1-120
securedrop_export/disk/new_status.py       14      0   100%
securedrop_export/disk/service.py          78      0   100%
securedrop_export/disk/status.py           13      0   100%
securedrop_export/disk/volume.py           26      0   100%
securedrop_export/exceptions.py            11      0   100%
securedrop_export/main.py                 101      0   100%
securedrop_export/print/__init__.py         0      0   100%
securedrop_export/print/service.py        143      1    99%   92
securedrop_export/print/status.py          13      0   100%
securedrop_export/status.py                 3      0   100%
---------------------------------------------------------------------
TOTAL                                     787     61    92%
Coverage HTML written to dir htmlcov

=================================================================================== 150 passed, 1 skipped in 2.01s ====================================================================================

@rocodes
Copy link
Contributor Author

rocodes commented Nov 29, 2022

There's a problem with our check-testing-requirements CI step here. The bookworm requirements are being checked against bullseye and vice-versa.

Edit: will be fixed in #115

@rocodes rocodes force-pushed the refactor-cli branch 4 times, most recently from 2738085 to 19d18d7 Compare November 29, 2022 23:04
@rocodes rocodes marked this pull request as ready for review November 29, 2022 23:12
@rocodes
Copy link
Contributor Author

rocodes commented Nov 30, 2022

The following zip file contains a test artifact signed with my public key. However, I recommend skipping the trust exercise and building your own .deb per the method in the PR ;)

securedrop_export_test_deb_and_build_logs.tar.gz

  • build logs (+detached signature) for a test .deb, and the test .deb built from the tip of this branch (+ detached signature).

Copy link
Contributor

@gonzalo-bulnes gonzalo-bulnes left a comment

Choose a reason for hiding this comment

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

As discussed offline, I added some nitpicks, identified by "nit".

I've been up to and stopped before revieweing the tests directory.

Otherwise, in reading order:

  • I find the docstrings of the CLI methods very useful and well calibrated. ⭐
  • CLI.mount_volume handles abstracts nicely the fact that volumes might or not have been previously mounted!
  • The entire CLI class uses a very consistent pattern of returning a usable object or raising an exception, that helps a lot forming expectations, I love it.
  • I marked a few places to think about sanitizing input. The device name doesn't obviously look like user input, but given the devices are user-provided, and their name is used in subsequent commands, I think it might be prudent to treat it as such? (@L3th3 @lsd-cat)
  • The import blocks thorough the change set are super clean/shallow! At first glance, I take at that as a sign that the separation of concerns is very likely sound. (The names make sense too, so I think it is indeed sound.)
  • The entire disk.Volume is very neat.
  • The main module is pretty clean too, I like it.

Not having run anything yet, I feel really good about this PR. To be continued!

@@ -47,7 +47,7 @@ rules:
languages:
- python
severity: ERROR
message: Possible path traversal or insecure directory and file permissions through os.mkdir(). Use securedrop_export.utils.safe_mkdir instead.
message: Possible path traversal or insecure directory and file permissions through os.mkdir(). Use securedrop_export.directory_util.safe_mkdir instead.
Copy link
Contributor

Choose a reason for hiding this comment

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

Naming nit: could it be securedrop_export.directory.safe_mkdir?

Picking on util as a smell, and thinking that directory seems perfectly legit both in the context of the other modules and with respect to what the module contains. What does _util bring to the name?

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 reason I called it this way, which may or may not be valid, is that it's not a class - it's a series of functions (utilities) that are imported individually to classes that need them, but are unfortunately used in multiple places so I didn't want to make them class methods since that wouldn't be DRY. Up to you if you want it changed or not.

Copy link
Contributor

@gonzalo-bulnes gonzalo-bulnes Dec 1, 2022

Choose a reason for hiding this comment

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

I get that. I'd say that the logging module defines functions, it is not called logging_util for that. Nor is os called os_utils.

I see a convention, but I think it is not a two-way convention: when defining a class (e.g. Dog) it is nice for the module to be called dog (file: dog.py), but the fact that a module is called cat doesn't imply that a Cat class must be defined in it.

In any case, I was suggesting renaming the file only, not changing anything inside.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in ed6ea1e

@@ -0,0 +1,101 @@
#!/usr/bin/env python3
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this needed? I'd think it is if part of the script is executed directly, but I don't see any __main__ function that suggests that. (I may also not be aware of other uses!)

Copy link
Contributor

Choose a reason for hiding this comment

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

A: was there, isn't new. May or not be needed, out of scope.

is_removable = False
try:
removable = subprocess.check_output(
["cat", f"/sys/class/block/{device}/removable"],
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there an opportunity to sanitize device here?

Copy link
Contributor Author

@rocodes rocodes Jan 11, 2023

Choose a reason for hiding this comment

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

device here is the device identifier assigned by the OS. (cat /sys/class/block/xvdc/removable, for example). I do not think sanitization is required.

Copy link
Contributor

@gonzalo-bulnes gonzalo-bulnes Jan 11, 2023

Choose a reason for hiding this comment

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

I'm on the fence because that string originates outside of the scope of the function. But I'm not overly concerned by it either.

cc: @L3th3 @lsd-cat in case you've got any advice to give us. (I'd otherwise second @rocodes' suggestion that we keep this as it is, with no sanitization.)

Edit: note to selves :P if we decide to take action on this, there are a few occurrences to follow up on.

Comment on lines 5 to 7
import logging

logger = logging.getLogger(__name__)
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 used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, my bad -removed 🤦

from securedrop_export.exceptions import ExportException

from .volume import EncryptionScheme, Volume
from .new_status import Status
Copy link
Contributor

Choose a reason for hiding this comment

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

Naming nit: If the new_*.py modules were instead called status.py and service.py, and the other ones called legacy_*.py:

  • while both exist, it would be clear that the legacy code shouldn't be built upon
  • when time comes to remove the legacy code, there would be no need to touch the files that define or import the code that won't be removed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 5e12476

Comment on lines 13 to 14
from securedrop_export.disk.service import Service as ExportService
from securedrop_export.print.service import Service as PrintService
Copy link
Contributor

Choose a reason for hiding this comment

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

Since the services are intended to be used outside the disk and print modules, I would export them in disk/__init__.py (resp. print/__init__.py) as follows:

# __init__.py

from .service import Service  # noqa: F401

That would allow to import them without reaching into the internals of the modules:

Suggested change
from securedrop_export.disk.service import Service as ExportService
from securedrop_export.print.service import Service as PrintService
from securedrop_export.disk import Service as ExportService
from securedrop_export.print import Service as PrintService

I see that as documenting which elements of a module are meant to be used / constitute its API. Accordingly, an import path with more than one dot is a smell to me.

Copy link
Contributor Author

@rocodes rocodes Jan 11, 2023

Choose a reason for hiding this comment

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

Thank you! addressed in eaef241

securedrop_export/main.py Show resolved Hide resolved
securedrop_export/main.py Show resolved Hide resolved
securedrop_export/main.py Outdated Show resolved Hide resolved
securedrop_export/print/service.py Outdated Show resolved Hide resolved
@gonzalo-bulnes
Copy link
Contributor

gonzalo-bulnes commented Dec 6, 2022

USB Acceptance tests
Pro tip: Export works in offline mode once a file is already downloaded. If the Tor weather is bad, you will thank me for mentioning this ;)

  • No USB yields "Please insert a USB" screen
  • Non-LUKS USB yields "Either not a LUKS drive, or something else is wrong" (encryption not supported)
  • Multi-partitioned USB yields "Either not a LUKS drive, or something else is wrong" (encryption not supported)
  • Attempt to unlock LUKS drive with wrong passphrase yields passphrase error message, can still retry with good password
  • LUKS single-partition USB
    • yields passphrase prompt
    • successful export (assuming enough disk space) 🟥
      Note: This fails with the securedrop-client repo main b27c9a07 and f55606fc... (but succeeds with 0.8.1 see below) - this was fixed in freedomofpress/securedrop-client@ecb6070 🟢
      • ExportStatus.UNEXPECTED_RETURN_STATUS: See your administrator for help.
      • That happens whether the sd-devices template is test-sd-large-* or the regular sd-large-*.
      • The files are exported correctly to the drive.
      • The logs in sd-log look clean: Copying file..., File copied successfully..., Syncing..., Unmounting..., Locking luks... Deleting temporary...
      • The logs in sd-app only say: INFO: Exporting file..., ERROR: Export failed
    • successful export (assuming enough disk space) 🟢
      Note: succeeds with securedrop-client 0.8.1
  • LUKS single-partition USB that faces error (eg, not enough space on device) yields error message 🟥 and successful cleanup of tmp (/tmp/xxxx) directory containing export materials on sd-devices dvm
    Note: This case's handling is missing a bit, let's talk.
  • Success screen shows at end of export routine

Additional testing

securedrop_export.disk.cli:264(_get_mountpoint) DEBUG: Checking mountpoint
securedrop_export.disk.cli:291(mount_volume) INFO: The device is already mounted
securedrop_export.disk.cli:293(mount_volume) WARNING: Mountpoint was inaccurate, updating

The warning line is optional but will show up if the user manually mounts a drive somewhere we weren't expecting (eg via Nautilus, like in this test plan).

Printer acceptance tests

  • No printer yields "Please connect your printer" screen
  • SKIP Test page prints successfully
  • SKIP Multi-page prints successfully
  • SKIP Unsupported printer yields error screen

@gonzalo-bulnes
Copy link
Contributor

Heads up @rocodes I've got no printer to test the three conditions I skipped at the end of the list above. (I'm working on all the other ones.)

gonzalo-bulnes
gonzalo-bulnes previously approved these changes Dec 7, 2022
Copy link
Contributor

@gonzalo-bulnes gonzalo-bulnes left a comment

Choose a reason for hiding this comment

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

Test plan check out 🚀

Two comments:

  • I haven't tested the scenarios involving a printer. (Marked as SKIP in my report.)
  • I've noted that two items of Improve error handling for unsupported drive configurations #70 are not handled (and handling them wasn't the intention in this PR), and that the issue wasn't marked to be closed, so all is great 🙂 I'll edit the issue to take note of the progress.
  • I have a few suggestions for changes in the test suite, but I think those can perfectly be addressed as follow ups, as they don't affect the efficacy of the test suite. Given we've tested the branch as-is, I'd merge it and refactor what we want later.

Base on the above, I'll approve the PR. @rocodes I let you either update the status of the printer items, or we can talk about how to get them covered 🙂


P.S.: Thank you for the thorough test plan, and the script to build the package 😍 those made review a breeze.

@rocodes
Copy link
Contributor Author

rocodes commented Dec 7, 2022

Thanks for your review :) I'll add printer tests shortly so let's hold off on merging. I'm also going to address some of your review feedback, then squash commits once you've had the chance to take a peek.

Copy link
Contributor

@gonzalo-bulnes gonzalo-bulnes left a comment

Choose a reason for hiding this comment

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

since you'll take some of the review comments in consideration now, I'll add the few I mentioned yesterday. Again, I wouldn't mind taking that to follow up time, I let you judge when is a good time.

Can't be attached to the changes:

tests/disk/test_cli.py Show resolved Hide resolved
tests/disk/test_cli.py Show resolved Hide resolved
tests/disk/test_cli.py Show resolved Hide resolved
Comment on lines 162 to 164
self.mock_cli.get_connected_devices.side_effect = ExportException(
sdstatus=NewStatus.ERROR_MOUNT
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Naming nit: In general terms, I think that naming all mocks mock_* is unnecessary. The readability improvement is debatable, and the mock_* prefix doesn't add any new information. From the moment you assign to it or assert on invocations, the thing is being treated as controlled entity, a mock, anyway.

I would say that this block is meant to be understood as:

"Given self.cli.get_connected_devices's side effect is ExportException(...)"

And that 1. is closer to that than 2.:

1. self.cli.get_connected_devices.side_effect = ExportException(...) 

2. self.mock_cli.get_connected_devices.side_effect = ExportException(...) 

Isn't it?

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 value in indicating which components are stubs/mocks and which ones are 'live', personally, just to make it explicit that we are using a 'fake' cli and not the real one.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fair. That's something we can talk about if we'd prefer having a consistent convention. I'd usually not prefix variables, and go for shorter names the closer usage is from the declaration. (In Python I don't go all the way to the one letter convention in Go, but in my experience that scope-size-to-name-length heuristic does improve readability, so I follow that spirit.)

@rocodes
Copy link
Contributor Author

rocodes commented Jan 11, 2023

Just stepped through printer acceptance tests on Brother HL-L2360DW ✔️ Will address @gonzalo-bulnes' review comments, file naming etc, and clean up commits

@rocodes
Copy link
Contributor Author

rocodes commented Jan 11, 2023

(Sorry, had to rebase. The last 5 commits address review feedback, the rest is unchanged. Once you've seen these I will squash/clean up.)

Exceptions.

Reorganize Print actions into methods in service class.

Use commands to separate different types of export and print actions.

Rename SDExport to Archive. Use methods instead of classes for each export routine.

Move ExportException to common directory and add Command enum for supported export commands.
Refactor test suite to match new export and print services and CLI wrapper.
semgrep rules.

i#add directory_util.py test coverage, show new name in semgrep rules
…nger inherits from ExportException. Small fix to export metadata and get_partitioned_devices.
…ferred.

Log exceptions when they occur instead of during graceful exit.

Rename old service and status files to legacy_*; address review
feedback.
Copy link
Contributor

@gonzalo-bulnes gonzalo-bulnes left a comment

Choose a reason for hiding this comment

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

I'll approve and merge this PR, based on:

:shipit:

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