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

Error handling improvements #70

Merged
merged 28 commits into from
Nov 24, 2021
Merged

Error handling improvements #70

merged 28 commits into from
Nov 24, 2021

Conversation

adobeDan
Copy link
Collaborator

Changes to fix #40 - clean up error handling.

The basic idea is that errors have both a platform-independent code (to make it easier for clients to figure out what's happening) and the underlying platform error that occurred (so the client can dig into it if desired).

Right now the error codes are being determined by where in the fetch/set process the error occurs: opening the credential store, searching for the credential, reading/writing the credential, etc. This is marked draft because I am still adding detailed underlying error analysis of the platform errors to make sure the coding is correct. But it works on all three platforms so I welcome review.

NOTE: These are based on the platform reformulation done in #67, which has not been merged and introduces some minor breaking API changes. If you have issues with that platform reformulation, please make those suggestions in #67. The changes here could conceivably be back-ported to the existing release with a fully backward-compatible API, but I need to discuss with the other maintainers whether that's worth it or not.

brotskydotcom and others added 12 commits November 9, 2021 05:31
Add get_password_for_target on Windows.
Keyrings now just keep the platform-specific attributes needed to store and retrieve the corresponding platform keyring item.  This simplifies the platform-specific code and allows the actual keyring implementation to be platform independent (and thus documentable properly).

In addition, you can now create a Keyring by passing in a mapper function which specifies exactly what platform attributes should be used given the service and username.  This allows compatibility with 3rd-party keyring clients.
1. Use Strings rather than strs to simplify lifetime management of the LinuxIdentity attributes.
2. Remove the mac keychain feature, which cannot be implemented due to deprecation of separate keychains on mac.
1. add username into the map
2. remove platform conditionals in platform-specific files
Get updates to GitHub publish workflow.
…th Mac)

1. Make the error a structure with a code and a cause (underlying error).
2. Set the code whenever an error is constructed.
3. Provide the underlying error when there is one.

This work only on Mac for now.
Also stopped surfacing the underlying encoding errors, because they can be in both utf8 and utf16 depending on platform, and that doesn't allow for a platform-independent interface.
Merge upstream master to pick up latest release.
@adobeDan
Copy link
Collaborator Author

I believe this PR also fixes #51

* Add analysis of the errors on each platform.  This fixes #65 and #39 and #54.
* Change the encoding error to have the raw bytes that couldn't be encoded.  This means we get the same treatment for both UTF-16 and UTF-8 errors.  This fixes #53.
* Rename `PlatformIdentity` to `PlatformCredential` to make clear what it represents.
* Update the README and make the code in it be a doctest so it stays up to date.  This fixes #28.
* Extend `PlatformCredential` so that it has all the attributes in the credential that we set.
* Change `get_password` to use a mutable clone of the platform credential and have it fill all the fields of the credential from the keychain.
* Add a variant of `get_password` called `get_password_and_credential` that hands back the platform credential so the client can read all the values.  This fixes #60 including the enhancements requested by @1Dragoon.

What's left to do before we can release this?

1. A lot more tests, especially of error conditions.  I'd like to get to 95% automated coverage or better.
2. Tests using custom credential mappers for various conditions.
3. A lot of documentation for all the new features.

I think we are just about ready for a beta release, though.
1. There was a missing method in the decode_error function on Mac.
2. Improve the examples to show how errors are described.
@adobeDan adobeDan self-assigned this Nov 14, 2021
1. There was a missing method in the decode_error function on Mac.
2. Improve the examples to show how errors are described.
Copy link
Collaborator

@phillipCouto phillipCouto left a comment

Choose a reason for hiding this comment

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

Overall the code looks good, one comment with the struct type for error handling is it does make error handling a little less convenient at least with my current knowledge of rust. I may not be aware of rust syntax that can make this easier but I had to change the following code in our application from:

let payload = match ring.get_password() {
  Ok(s) => s,
  Err(KeyringError::NoPasswordFound) => return Ok(None),
  Err(e) => return Err(Box::new(e)),
};

to:

let payload = match ring.get_password() {
  Ok(s) => s,
  Err(e) => {
    if let keyring::ErrorCode::NoEntry = e.code {
      return Ok(None);
    }
    return Err(Box::new(e));
   }
};

For me this isn't that much of a pain but there may be others who are checking for more of the keyring error codes that may find this a negative change.

README.md Outdated Show resolved Hide resolved
1. Remove use of bytes crate, so no general depedencies.
2. Add the keychain domain to the MacCredential, so the user can specify the use of System and Common (and Dynamic) keychains.
@adobeDan
Copy link
Collaborator Author

adobeDan commented Nov 15, 2021

@phillipCouto great review, thanks. WRT to the question about client code changing, I believe you can use a destructuring pattern to minimize your code changes like this:

let payload = match ring.get_password() {
  Ok(s) => s,
  Err(keyring::Error { code: keyring::ErrorCode::NoEntry, .. })  => return Ok(None),
  Err(e) => return Err(Box::new(e)),
};

I also introduced type conversions between Errors and ErrorCodes, so you can easily extract the error code using .into(), but that won't work in the pattern matching environment.

@adobeDan adobeDan marked this pull request as ready for review November 15, 2021 00:35
@adobeDan
Copy link
Collaborator Author

Hi @hwchen and @phillipCouto, I believe all that's left to do now is more tests and documentation, so please give this a thorough review, especially as to issues around naming and structuring.

One serious question I have, which was raised indirectly by Phillip's comment above, is whether we want to let the error object be just an enum of codes (called "KeyringError" in the old code, and "ErrorCode" in the new code) rather than a structure. We can always embed the platform error in the enum codes that need them, and they can even be optional. That would really keep the API much more similar in shape to what it used to be (although I think we should call it just "Error" not "KeyringError").

adobeDan and others added 6 commits November 15, 2021 00:00
1. Add the ability to specify non-default keychains on Linux.
2. Fix typo in README.
3. Show how to use destructuring in the example to check for a particular code in the return value.
1. Turned the Error type back into an enum, with underlying platform errors attached to the error codes which indicate a runtime error in the platform.  This really simplified code in a lot of places, and will make it a lot easier for clients to upgrade.
2. Renamed `Keyring` to `Entry`, since that structure really is one entry in the (global) key ring.  So now you say `keyring::Entry::new(...)` to create a new entry.
3. Renamed the `attrs` module to `credential` since that's actually what it's about.
4. Completed a full draft of the code-level documentation.

Once the tests are expanded, this should be ready to release.
1. Move integration tests to a separate file.
2. Add unit tests for targets.
3. Add integration tests for target stability.
1. make default_mapper have the CredentialMapper signature.
2. add unit tests for input and output validation on Windows.
1. refactor map to use a function for decoding passwords.
2. add encoding error tests on Mac.
Add bad-password test on Linux.
@brotskydotcom
Copy link
Collaborator

Hi @hwchen and @phillipCouto, this PR is now complete and ready to merge. I'm going to do some code coverage analysis on the tests while you give it a final review.

@brotskydotcom
Copy link
Collaborator

This PR:

@hwchen
Copy link
Owner

hwchen commented Nov 20, 2021

I'll be taking a look at this this weekend. Just wanted to pop up and say I didn't disappear, but just had a tough week.

@brotskydotcom
Copy link
Collaborator

I'll be taking a look at this this weekend. Just wanted to pop up and say I didn't disappear, but just had a tough week.

No worries! I completely understand.

Also, I realize I want to revamp the examples to show the new capabilities and error handling, so let's not merge quite yet. I'm going to put this PR back in draft to indicate that.

@brotskydotcom brotskydotcom marked this pull request as draft November 20, 2021 18:12
1. Reworked the CLI example to have much better error messages, allow keychain switching, etc.
2. Based on that rework, revisit the notion of "credential mappers" and change to allowing direct specification of the platform credential for a keyring entry.  This simplified a ton of things, and avoided the whole question of passing closures into the entry creation process.
3. Having gotten rid of custom mappers, introduce a new constructor to specify a keychain as well as a service and a username.
4. Simplified the tests to match the simplified interface.
@hwchen
Copy link
Owner

hwchen commented Nov 21, 2021

I think it's fine to have the Error be an enum. Now that platform errors have been consolidated, it looks clean and an enum is probably a bit more straightforward to use.

One general comment: I'm actually not sure why I originally treated passwords as utf8 strings. Perhaps it's better to treat passwords as bytes and leave encoding to the user.

Or perhaps have a Password type, so users could choose as_bytes() or to_string() (and we could also impl From both both strings and bytes). With this approach, users wouldn't have to find String::from_utf8 themselves, and keyring would also be able to remove its BadEncoding error variant (since we'd just defer to the regular Utf8Error from to_string()).

If we want to remove String, we can do it in another PR before releasing. Just wanted to mention it here because I was thinking about the BadEncoding variant.

More breaking changes, but we're already changing Error so this might be worth cleaning up also.

@brotskydotcom
Copy link
Collaborator

brotskydotcom commented Nov 21, 2021

@hwchen Thanks for the review!

With respect to non-UTF8 passwords, my thinking is that this is an edge case we should not worry about in this release. No one has ever filed an issue saying that they need to be using arbitrary byte strings as passwords, and I suspect no one ever will. The modern trend is increasingly to represent binary data using a UTF8-compatible serialization format (such as base 64 or JSON), rather than to store binary data directly in the platform (which requires platform-specific implementations in order to deal with different word sizes and endian-ness on different platforms).

If we heard people asking for it, it would be trivial to add set_password_bytes and get_password_bytes calls to the interface, and they would be a backwards-compatible extension that could be done as a single-dot release (assuming we are talking about 1.x here).

It's also not the case that adding support for byte strings allows getting rid of the BadEncoding error, because they are needed as long as we keep the string-based calls in the interface (in order to inform the caller and also return the data).

I think a much more interesting point of real extension (maybe for a 2.x release :) would be to have a Keyring trait with generic set and get variants that take all sorts of types. This would allow people to plug in their own backends for the Keyring other than the ones we support, such as are available in the Python Keyring package. These other back ends could then support any types they wanted, and we could extend our current back end to support other types as well via encodings.

I had considered making this example a binary rather than an example, so that it was always available with the package, but the Rust folks haven't figured out the dependency hell around this yet, so I'm saying with it as an example and leaving it published under its current name.
Also remove the `clap` dependency, now that we are using structopt.
Now that each of the tests uses a separate credential, there is no collision between testing threads.
1. Rename `new_in_keychain` to `new_with_target` and use the target on all platforms.
2. Enhacen tests to test the new functionality.
3. Revamp the CLI example to use new functionality, maintaining backward compatibility.
4. Revamp the docs and the readme to talk about what happens on each platform.
5. Test to make sure there is backward compatibility with earlier releases.
@brotskydotcom brotskydotcom marked this pull request as ready for review November 24, 2021 06:38
@brotskydotcom
Copy link
Collaborator

I'm going to merge this, so we can have all the fixes ready. I'll do a pre-release tag so we can test with it and @hwchen can do a 1.0 release when ready.

@hwchen
Copy link
Owner

hwchen commented Nov 25, 2021

Great, thanks!

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.

4 participants