-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
Implement external credential process. (RFC 2730) #8934
Conversation
(rust-highfive has picked a reviewer for you, use r? to override) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good to me, thanks!
I don't know much about the specific FFI interfaces used here but all the crates look good to me modulo protocol-specific bugs.
- run: cargo build --manifest-path crates/credential/cargo-credential-macos-keychain/Cargo.toml | ||
if: matrix.os == 'macos-latest' | ||
- run: cargo build --manifest-path crates/credential/cargo-credential-wincred/Cargo.toml | ||
if: matrix.os == 'windows-latest' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm only just starting, but for convenience it'd be nice if these crates built on all platforms and then at runtime just returned errors on the wrong platform, that way things like cargo test --all
would work well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little uncomfortable doing that since for example the gnome package won't work if libsecret isn't installed, and I think it could cause confusing problems if it silently ignored the absence of that library.
What do you think about adding that kind of logic to Cargo? That is, you could somehow specify a binary only works on specific targets, and is otherwise ignored (similar to required-features). I suspect it will be a long while before Cargo can use a workspace (due to the lack of nested workspaces), so I imagine this won't be a concern for a while.
If you'd really prefer them to always compile, I'm fine with doing that, I just feel it could lead to confusing problems.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nah it's not a strong preference on my part, mostly just cleaning up CI config and ideally making integration elsewhere easier. (rustbuild will need to encode these platform rules, right?)
FWIW I was imagining that we wouldn't silently ignore the lack of something like libsecret, but we would ignore winapi when building for Linux for example. As for target-specific binaries, seems like a reasonable feature to me to add!
.map(|arg| { | ||
arg.replace("{action}", action_str) | ||
.replace("{name}", name) | ||
.replace("{api_url}", api_url) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a slight forward-compatibility hazard in that there's no escaping availble to call the process with {action}
literally as a string somewhere. Not really the end of the world and would be sort of a pain to handle, but figured it was at least worth mentioning.
exe.display() | ||
) | ||
})?; | ||
if let Some(end) = buffer.find('\n') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd expect a call to .trim()
here, but is this intentionally ignoring the second-and-after lines of output?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, yea, I changed it so that it will return an error if there is more than one line.
} | ||
|
||
fn doit(credential: impl Credential) -> Result<(), Error> { | ||
let which = std::env::args() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This behavior of discovering the action may be worth mentioning in the crate docs?
version = "0.1.0" | ||
authors = ["The Rust Project Developers"] | ||
edition = "2018" | ||
license = "MIT OR Apache-2.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is intended for publication, right? If so want to fill in some metadata here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(same for the other crates)
struct WindowsCredential; | ||
|
||
fn wstr(s: &str) -> Vec<u16> { | ||
OsStr::new(s).encode_wide().chain(Some(0)).collect() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may want to return an error as well if s
contains a 0, because otherwise I think it'll get silently truncated
@@ -0,0 +1,3 @@ | |||
fn main() { | |||
pkg_config::probe_library("libsecret-1").unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is one of my bigger worries about shipping these executables with rust-lang/rust. We typically don't make any assumptions about the execution environment, but this execution will require libsecret.so
or something like that to run the process, right?
Or is there a way we could ask this to link statically when we build distribution binaries?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although one though I had reading this, these crates should all be trivial to cargo install
, so we could perhaps manage it that way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, this is a dynamic dependency, and the user will have to install the appropriate package on their system. I would feel a little uneasy statically linking this, primarily since I'm not very familiar with the GNOME ecosystem, but also because I'm not really sure it would work. There is infrastructure (the "Secret Service" and the gnome-keyring) that the user needs to have installed, and I suspect that the library that talks to them needs to be somewhat compatible.
I'm also not sure at this point how feasible it is to build this on rust-lang/rust, since they tend to use old Linux distributions which can be painful to use. I haven't yet tried. If it is too difficult to build, then I think we'll just have to tell the user to build it themselves (with cargo install
), in which case they'll have to install libsecret anyways.
My feeling is that this will be an issue for documentation (it can include some sample instructions on what to install for various distributions).
In general, I see these sample implementations as a very "experimental" status. I wanted to see what issues may crop up, and have that guide how to proceed. If they don't work for some users, then they just won't work. Perhaps others can step up to either provide patches, or publish their own implementations on crates.io. If they stop working and go unmaintained, I think it would be reasonable just to delete them. (I would hope it doesn't come to that, I just want to emphasize that I think there is some flexibility here.) I have no idea how many people use GNOME secret storage, perhaps nobody will. 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok makes sense, if we don't ship these with rust-lang/rust (which I initially thought we would), then this seems fine to ignore. I'm curious then, though, if we ship via Cargo and cargo install
, do we still need the libexec
logic? Or should we just look in $PATH
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to ship these wrappers, just not the libsecret one. I checked, and the Linux used on rust-lang/rust is too old, so I updated the documentation to indicate that it needs to be installed manually.
if !error.is_null() { | ||
return Err(format!( | ||
"failed to get token: {}", | ||
CStr::from_ptr((*error).message).to_str()? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This technically looks like we'd need to free the error after this, right?
Given that this is a very short-lived process, though, seems fine to ignore destructors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, I think there is a g_error_free
. I can add that if you want, though I wasn't too concerned about leaking in a one-shot process.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nah yeah just wanted to note this, but skipping dtors seems fine.
SecretSchema { | ||
name: b"org.rust-lang.cargo.registry\0".as_ptr() as *const gchar, | ||
flags: SecretSchemaFlags::None, | ||
attributes, | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just had a general Rust question here. Is there a way to specify this struct literal that doesn't require so much awkwardness? In C, it would be defined as:
static const SecretSchema the_schema = {
"org.rust-lang.cargo.registry", SECRET_SCHEMA_NONE,
{
{ "registry", SECRET_SCHEMA_ATTRIBUTE_STRING },
{ "url", SECRET_SCHEMA_ATTRIBUTE_STRING },
{ "NULL", 0 },
}
};
However, I couldn't think of a way to do the equivalent in Rust. Since the attributes are an array of length 32, I don't know how to fill out the 29 other entries.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Making this a const
function may actually work? Other than that though I don't think there's a smaller method than this.
Oh I meant to offer some answers to your questions last time but forgot:
I'd go for
At least for the
I think this is fine. They're pretty small and easy to review, and we'd just need to be vigilant as reviewers that any modifications are tested by the contributor or tested locally by us.
This seems fine by me. If we're extra intrepid I'd open a feature request with them, though.
I think I agree that it'd be best if we could avoid stdin. My ideal solution would be the separate file descriptor/handle (and then we'd specify the fd or handle in an env var or something like that), and perhaps an external crate could ease the boilerplate? I may also be naive assuming something like this exists. That being said I personally would think that the token being in an env var also isn't the end of the world. |
Change it so that if both are specified, it is an error just to be safer for now. If token is specified for a registry, ignore the global credential-process. I'm still uncertain if this is the best behavior, but I think we can tweak it later if needed.
The rust-lang/rust build infrastructure uses a version of Linux that is too old to support building this.
I waffled, but I feel like keeping it as-is. awscli uses the singular version (
I changed it to use the more specific one, and error if both are specified. I think it can be tweaked if that is too awkward.
Hm, I looked at this for a while, and it just seems extraordinarily difficult on windows (like maybe 100+ lines of code?). AIUI, you create a pipe with the bInheritHandle security attribute. Pray that no other processes or threads are spawned while you hold the pipe open. Convert the RawHandle to a string, and pass it in to the child. Have the child convert the string to a I....don't feel like doing all that. Would it be OK to maybe defer this to the future? The current implementation works now, and is pretty simple. I think if we wanted to use the file-descriptor route, we could extend it later by adding a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think you're right about how it works on Windows, and you're not wrong that it feels a bit odd! In any case everything looks and sounds great to me!
Did you have any final bits or is this good to go? I added an unresolved question to the tracking issue about the {action}
syntax and escaping.
I think this is good to go for now. I added some other notes to the tracking issue. |
@bors: r+ |
📌 Commit eabef56 has been approved by |
☀️ Test successful - checks-actions |
Update cargo 4 commits in d274fcf862b89264fa2c6b917b15230705257317..a3c2627fbc2f5391c65ba45ab53b81bf71fa323c 2020-12-07 23:08:44 +0000 to 2020-12-14 17:21:26 +0000 - Check if rerun-if-changed points to a directory. (rust-lang/cargo#8973) - Implement external credential process. (RFC 2730) (rust-lang/cargo#8934) - Revert recent build-std vendoring changes (rust-lang/cargo#8968) - Fix the unit dependency graph with dev-dependency `links` (rust-lang/cargo#8969)
This adds a config setting for an external process to run to fetch the token for a registry. See
unstable.md
for more details.As part of this, it adds a new
logout
command. This is currently gated on nightly with the appropriate-Z
flag.I have included four sample wrappers that integrate with the macOS Keychain, Windows Credential Manager, GNOME libsecret, and 1password. I'm not sure if we'll ultimately ship these, but I would like to. Primarily this provided a proof-of-concept to see if the design works.
Patch Walkthrough
This is a brief overview of the changes:
logout
command. Withcargo logout -Z unstable-options
, this allows removing thetoken
from.cargo/credentials
. Withcargo logout -Z credential-process
, this launches the process with theerase
argument to remove the token from storage.ops/registry/auth.rs
module. I think it is pretty straightforward, it just launches the process with the appropriate store/get/erase argument.ops::registry::registry()
now returns theRegistryConfig
to make it easier to pass the config information around.crates/credential/cargo-credential
is a helper crate for writing credential processes.cargo:
prefix for a credential process will launch the named process from thelibexec
directory in the sysroot (or, more specifically, thelibexec
directory next to thecargo
process). For examplecredential-process = "cargo:macos-keychain"
. My intent is to bundle these in the pre-built rust-lang distributions, and this should "just work" when used with rustup. I'm not sure how that will work with other Rust distributions, but I'm guessing they can figure it out. This should make it much easier for users to get started, but does add some integration complexity.Questions
credential-process
vscredentials-process
, which sounds more natural? (Or something else?)token
andcredential-process
is specified (seewarn_both_token_and_process
test). Currently it issues a warning and usestoken
. Does that make sense? What about the case where you haveregistries.foo.token
for a specific registry, but then have a generalregistry.credential-process
for the default (it currently warns and uses the token, maybe it should not warn?)?cargo login
and require the user to manually enter the token in the 1password GUI. I don't think the concern is too large (1password themselves seem to think it is acceptable). Should this be OK?cargo login
, where it passes the token in stdin. If the wrapper requires stdin for user interaction (such as entering a password to unlock), this is quite awkward. There is a hack in the 1password example that demonstrates using/dev/tty
andCONIN$
, which seems to work, but I'm worried is fragile. I'm not very comfortable passing the token in environment variables, because those can be visible to other processes (like CLI args), but in some situations that shouldn't be too risky. Another option is to use a separate file descriptor/handle to pass the token in. Implementing that in Rust in a cross-platform way is not terribly easy, so I wanted to open this up for discussion first.