-
Notifications
You must be signed in to change notification settings - Fork 14
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
re-organize CLI config files #727
Conversation
- rip out old config stuff - only create the Client when we need it
- test the auth commands under various conditions
We talked about this in DMs, but to sum up: I was skeptical of the need to have the SDK be able to read from the same credentials config files as the CLI, because I am used to SDKs having a more explicit mechanism for specifying credentials. Usually I see env vars plus client constructor params for passing in a token (and in our case a host). The TS and Go SDKs already have the constructor params. However, seeing that both the GCP and AWS client libraries support authing from the credentials set up by their respective CLIs, I concede it's probably useful and users may expect this from us. So, my suggestions were:
|
@david-crespo I added the raw token auth method; still more work needed, but thanks for the suggestion. |
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.
Nice! I'll be able to have the terraform provider use the profiles as well when this is merged. This will really improve UX all around :)
cli/src/cmd_auth.rs
Outdated
// TODO seems pointless | ||
/// Read token from standard input. | ||
#[clap(long)] | ||
with_token: bool, |
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.
Sadly, we can't stop people from grabbing a token from the credentials file of a machine and pasting it to the credentials file of another. By passing the token they already have through this flag we can deter them from manually editing the credentials file.
cli/src/cmd_auth.rs
Outdated
// TODO what's the point of this? | ||
let client_id = Uuid::new_v4(); |
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 think this was initially written by @plotnick , I'm pretty sure there was a good reason
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.
Reconstructing from distant memories and a quick perusal of the spec... We don't currently assign clients any kind of unique identifier, so a random UUID is about the best we can do here. It's not holding any weight, but it's not optional in the OAuth protocol.
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. The question was to myself. I saw its use by the oauth dance and wanted to investigate if there's something meaningful here or if we should document it in the code as unimportant.
cli/src/cmd_auth.rs
Outdated
std::fs::write(credentials_path, credentials.to_string()) | ||
.expect("unable to write credentials.toml"); | ||
|
||
println!("{}", credentials); |
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.
Could you add a comment at the start of the file warning users not to manually edit this file?
@karencfv I'll be happy to let you know when this is ready for review. |
In good news, updating to this version worked without any changes for I'm not sure about opting in to reading files by default. I don't think it is terribly onerous to ask the caller to use |
/// | ||
/// Alternatively, pass in a token on standard input by using `--with-token`. | ||
/// | ||
/// # start interactive setup | ||
/// $ oxide auth login | ||
/// Authenticate with an Oxide Silo | ||
/// | ||
/// # authenticate against a specific Oxide instance by reading the token | ||
/// # from a file | ||
/// $ oxide auth login --with-token --host oxide.internal < mytoken.txt | ||
/// | ||
/// # authenticate with a specific Oxide instance | ||
/// # authenticate with a specific Oxide silo | ||
/// $ oxide auth login --host oxide.internal | ||
/// | ||
/// # authenticate with an insecure Oxide instance (not recommended) | ||
/// # authenticate with an insecure Oxide silo (not recommended) | ||
/// $ oxide auth login --host http://oxide.internal |
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 hear you, but I really wanted to make it easy to build a custom consumer. For example, if we know where creds are kept, why not make that the default? Perhaps put another way, why unintentional use might we see happening that would create friction for users of the SDK (or CLI)? |
@karencfv and @david-crespo this is ready for your review. I see this as the foundation to fix a few outstanding issues:
....etc |
Maybe you could split the difference by making it opt-in (through a special constructor or whatever) to read from the default location, but you don’t have to give a path — it would know. I admit that the Bad Thing (reading from the CLI config files when you don’t want to) is relatively unlikely, because if someone wants to specify creds they will probably do so and override that default behavior. It still feels a little weird to me for a library (as opposed to a CLI) to do something like that. |
I think a common use case is going to be 1. |
@@ -79,8 +84,8 @@ | |||
"subcommands": [ | |||
{ | |||
"name": "login", | |||
"about": "Authenticate with an Oxide host.", | |||
"long_about": "Authenticate with an Oxide host.\n\nAlternatively, pass in a token on standard input by using `--with-token`.\n\n # start interactive setup\n $ oxide auth login\n\n # authenticate against a specific Oxide instance by reading the token\n # from a file\n $ oxide auth login --with-token --host oxide.internal < mytoken.txt\n\n # authenticate with a specific Oxide instance\n $ oxide auth login --host oxide.internal\n\n # authenticate with an insecure Oxide instance (not recommended)\n $ oxide auth login --host http://oxide.internal", | |||
"about": "Authenticate with an Oxide Silo", |
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.
Will the average dev/SRE user know what a silo is? I was under the impression the term "silo" was mostly hidden from them and only really relevant to operators.
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 think Silo is a term that we're educating users about. Certainly it's more precise than a "host" ... which is a term of whose definition I'm very unsure.
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, this is a tough one. In the web console, we generally say that for the developer user without fleet permissions, the silo is their universe and we try to avoid talking about the silo. However, we do say it right in the top left corner, and (as of recently) we call silo images "silo images". So it's not completely hidden.
Notably silos are also the first thing in the key concepts guide. So I think we've definitely softened the "first rule of silos is never talk about silos" idea.
https://docs.oxide.computer/guides/key-entities-and-concepts
/// Read token from standard input. | ||
#[clap(long)] | ||
with_token: bool, |
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.
Is this not something we want for users that already have a token so they don't manually edit the credentials file?
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 think my aversion is more to unused- or seldom-used functionality, and I don't have the same aversion as I infer you do to people editing config files.
// If the profile already has a token, alert the user. | ||
if ctx.cred_file().profile.contains_key(profile_name) | ||
&& !yes(format!( | ||
"The profile \"{}\" is already authenticated. {}", | ||
profile_name, "Do you want to re-authenticate?", | ||
))? | ||
{ |
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.
Are tokens revoked when a user re-authenticates? I may be wrong but I think they will still be valid :/
If that's the case should we even allow re-authentication? Users may have a false sense of security and think that by re-authenticating the previous token will be revoked automatically.
If I'm wrong, just dismiss this comment :)
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.
Note that we haven't checked the validity of the credentials at this point. It may be that people want to re-authenticate because the existing creds are invalid (... for reasons?). Note that this is preserving existing functionality; if we want to change it, could we do so outside the scope of this PR?
/// The host of the Oxide instance to authenticate with. | ||
/// This assumes https; specify an `http://` prefix if needed. | ||
#[clap(short = 'H', long, value_parser = parse_host)] | ||
host: url::Url, | ||
|
||
/// Override the default browser when opening the authentication URL. | ||
#[clap(long, group = "browser-options")] | ||
#[clap(long)] |
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.
Would it be useful to have a flag to optionally set this profile as default?
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.
Maybe! Can we do that later?
.filter_map(|(name, info)| (info.host == self.host.as_str()).then_some(name)) | ||
.next() | ||
{ | ||
// If the host is already present in a profile, alert the user. |
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.
Maybe we can also handle the case where the user chose a profile name that already exists?
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 not sure I follow; wouldn't that lead us down the path where we ask if the user wants to re-authenticate?
} | ||
} | ||
pub async fn logout(&self, ctx: &Context) -> Result<()> { | ||
if !self.force && !yes("Confirm authentication information deletion:")? { |
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.
It'd be nice to confirm that the user is deleting the profile they intend to delete. How about
"Confirm authentication information deletion for profile {PROFILE NAME}:"
let Some(profile_name) = profile else { | ||
bail!("No profile specified and no default profile"); | ||
}; |
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.
Does this mean that if no profile name is selected, it'll just remove information of the default profile? Maybe we could add a warning or something? Or disallow logging out without setting the profile to be logged out of?
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.
Does this PR change things in that regard?
} | ||
}; | ||
} else { | ||
for (profile_name, profile_info) in &ctx.cred_file().profile { |
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.
It'd be nice to have a way to check the status of a single profile. WDYT?
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.
Sure! What do you propose? Can we do it as a follow-on?
I do agree that using the CLI to create a token and then using the SDK is a common use case today. Some of my hesitance is that the current CLI authentication feels more of a result of a limitation in the product (i.e. device OAuth is the only method to create tokens). It also add profiles as a SDK concept, which then asks how can profiles be managed via the SDK instead of files (though we could add this later). It may also just be me being paranoid, but something doesn't feel correct about an SDK that reads all of the credentials in a config file (even if AWS does it) when they may not be relevant to the executing program. I think Google handles this model a lot better, in that it requests that the user explicitly create an Application Default Credentials file (via a CLI command) which encodes a single credential. Their SDKs then load this file from an expected location. |
I confess that I don't fully understand your concern. If you're building a SDK consumer for your own use, requiring more steps (or, pragmatically, probably just a larger copy-paste) seems onerous. If you're using an SDK consumer that someone else built... well they could easy write the code to consume CLI config files for authentication. The GCP model you describe sounds like more of a pain in the neck, but if you think we should go down that path let's discuss. I'd note that the SDK today basically pulls data out of the hosts.toml file. |
Initial run through of basic commands with the CLI seemed to work (mostly just reads). Generally worked as expected. Nicely handled having two different silos with the same name Two minor suggestions:
Maybe a newline here between the statements? or something else to make Was there a specific reason to make the profile flag non-global? I initially placed it at the end of the call:
expecting behavior like the AWS CLI. Given that I know the behavior of clap it was easy enough to realize that I should move it to the front, though that may not be common knowledge. |
I actually tinkered with that, but the docs put |
We should be able to fix that in the docs somehow. I think the UX benefit of being able to put it anywhere is important. I would be absolutely baffled by that error. Think we could get a Edit: https://docs.rs/clap/latest/clap/struct.Arg.html#method.is_global_set |
[here](https://github.com/oxidecomputer/oxide.rs/releases). Look for the most | ||
recent release whose version suffix matches the version of Oxide software | ||
currently deployed. For example, if you're running the `20240502.0` release, | ||
use `0.5.0+20240502.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.
There might not be anything to change here about this, but 20240502.0
is the API version but "v8" is the primary release version. Travis just asked me the other day whether there was a way to know in the console what release you're on. Not only is there no way to do that, but even if you did know you were on v8, there isn't a way to know that that corresponds to 20240502.0
, or vice versa. It's worth noting that there is a note in the v8 release notes about 0.5.0 being the relevant CLI version.
Like I said, probably a bigger problem we can't fix in the readme for the CLI, but this reminded me of the problem — something to mull over as we keep tweaking this stuff.
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.
My hope is that our docs would send you to the right CLI version and that the CLI itself will be able to point you to the right version based on a response from the server. We're going to have the generated client send the version as a header. The server response can include an indication if you're on a mismatched version (ie with work in dropshot / omicron)
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 concede
If I missed anything from the review above, please file as an issue and I'll attend to it. |
This moves us from a
hosts.toml
indexed by the "host" used to connect to the Oxide API server to a "profile"-based approach. Profile data will span theconfig.toml
andcredentials.toml
, the former being primarily user-managed, the latter CLI/auth managed. To use a profile, users will specify--profile XXX
; there is adefault-profile
set inconfig.toml
. Specifying the profile rather than the host will, I expect, be much more user friendly.Note that this also simplifies the SDK <-> CLI boundary. We add the
credentials.toml
configuration mechanism to the SDK so that SDK consumers can use that same authentication information. But we move the CLI-specific context information back to the CLI (since it's not really a generally useful mechanism appropriate for the SDK).Closes #6, #163, #250, #254, #301, #391, #508, #737.