Skip to content

Store Hex username in credentials file and improve key revocation error message #4446

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 32 additions & 7 deletions compiler-cli/src/hex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod auth;
use crate::{cli, http::HttpClient};
use gleam_core::{
Error, Result,
error::wrap,
hex::{self, RetirementReason},
io::HttpClient as _,
paths::ProjectPaths,
Expand Down Expand Up @@ -99,7 +100,7 @@ pub(crate) fn authenticate() -> Result<()> {
let previous = auth.read_stored_api_key()?;

if previous.is_some() {
let question = "You already have a local Hex API token. Would you like to replace it
let question = "You already have a local Hex API key. Would you like to replace it
with a new one?";
if !cli::confirm(question)? {
return Ok(());
Expand All @@ -109,13 +110,37 @@ with a new one?";
let new_key = auth.create_and_store_api_key()?;

if let Some(previous) = previous {
if previous.username != new_key.username {
if let Some(previous_username) = previous.username {
let text = wrap(&format!(
"
Your previous Hex API key was created with username `{}` which is different from the username
used to create the new Hex API key. You have to delete the key `{}` manually at https://hex.pm",
Copy link
Member

Choose a reason for hiding this comment

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

We wrap before 80 columns! Could you call the wrap function on this please.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done!

previous_username, previous.name
));

println!("{text}");
return Ok(());
}
}

println!("Deleting previous key `{}` from Hex", previous.name);
runtime.block_on(hex::remove_api_key(
&previous.name,
&config,
&new_key.unencrypted,
&http,
))?;
if runtime
.block_on(hex::remove_api_key(
&previous.name,
&config,
&new_key.unencrypted,
&http,
))
.is_err()
{
let text = wrap(&format!(
"There was an error deleting key `{}` from Hex. You have to delete the key manually at https://hex.pm",
Copy link
Member

Choose a reason for hiding this comment

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

Same for this also please.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done!

previous.name
));

println!("{text}");
};
}
Ok(())
}
84 changes: 68 additions & 16 deletions compiler-cli/src/hex/auth.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use crate::{cli, fs::ConsoleWarningEmitter, http::HttpClient};
use camino::Utf8Path;
use gleam_core::{
Error, Result, Warning, encryption, hex, paths::global_hexpm_credentials_path,
warning::WarningEmitter,
};
use serde::{Deserialize, Serialize};
use std::{rc::Rc, time::SystemTime};

pub const USER_PROMPT: &str = "https://hex.pm username";
Expand All @@ -12,15 +14,17 @@ pub const LOCAL_PASS_PROMPT: &str = "Local password";
pub const PASS_ENV_NAME: &str = "HEXPM_PASS";
pub const API_ENV_NAME: &str = "HEXPM_API_KEY";

#[derive(Debug)]
#[derive(Debug, Serialize, Deserialize)]
pub struct EncryptedApiKey {
pub name: String,
pub encrypted: String,
pub username: Option<String>,
}

#[derive(Debug)]
pub struct UnencryptedApiKey {
pub unencrypted: String,
pub username: Option<String>,
}

pub struct HexAuthentication<'runtime> {
Expand Down Expand Up @@ -74,11 +78,18 @@ encrypt your Hex API key.
detail: e.to_string(),
})?;

crate::fs::write(&path, &format!("{name}\n{encrypted}"))?;
let encrypted = EncryptedApiKey {
name,
encrypted,
username: Some(username.clone()),
};

encrypted.save(&path)?;
println!("Encrypted Hex API key written to {path}");

Ok(UnencryptedApiKey {
unencrypted: api_key,
username: Some(username),
})
}

Expand Down Expand Up @@ -117,7 +128,12 @@ encrypt your Hex API key.
}

fn read_and_decrypt_stored_api_key(&mut self) -> Result<Option<UnencryptedApiKey>> {
let Some(EncryptedApiKey { encrypted, .. }) = self.read_stored_api_key()? else {
let Some(EncryptedApiKey {
encrypted,
username,
..
}) = self.read_stored_api_key()?
else {
return Ok(None);
};

Expand All @@ -127,26 +143,19 @@ encrypt your Hex API key.
detail: e.to_string(),
})?;

Ok(Some(UnencryptedApiKey { unencrypted }))
Ok(Some(UnencryptedApiKey {
unencrypted,
username,
}))
}

pub fn read_stored_api_key(&self) -> Result<Option<EncryptedApiKey>> {
let path = global_hexpm_credentials_path();
if !path.exists() {
return Ok(None);
}
let text = crate::fs::read(&path)?;
let mut chunks = text.splitn(2, '\n');
let Some(name) = chunks.next() else {
return Ok(None);
};
let Some(encrypted) = chunks.next() else {
return Ok(None);
};
Ok(Some(EncryptedApiKey {
name: name.to_string(),
encrypted: encrypted.to_string(),
}))

EncryptedApiKey::load(&path)
}
}

Expand All @@ -158,6 +167,49 @@ impl Drop for HexAuthentication<'_> {
}
}

impl EncryptedApiKey {
pub fn save(&self, path: &Utf8Path) -> Result<()> {
let text = toml::to_string(self).map_err(|_| Error::InvalidCredentialsFile {
path: path.to_string(),
})?;

crate::fs::write(path, &text)
}

pub fn load(path: &Utf8Path) -> Result<Option<Self>> {
let text = crate::fs::read(path)?;

toml::from_str(&text).or_else(|_| {
// fallback from old format
let mut chunks = text.splitn(2, '\n');

let Some(name) = chunks.next() else {
return Err(Error::InvalidCredentialsFile {
path: path.to_string(),
});
};

let Some(encrypted) = chunks.next() else {
return Err(Error::InvalidCredentialsFile {
path: path.to_string(),
});
};

let key = Self {
name: name.to_string(),
encrypted: encrypted.to_string(),
username: None,
};

// try to save the file in the new format, but let if fail silently,
// we do not want the load operation to fail because of a write.
let _ = key.save(path);

Ok(Some(key))
})
}
}

fn ask_local_password(warnings: &mut Vec<Warning>) -> std::result::Result<String, Error> {
std::env::var(PASS_ENV_NAME)
.inspect(|_| {
Expand Down
15 changes: 15 additions & 0 deletions compiler-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,9 @@ file_names.iter().map(|x| x.as_str()).join(", "))]

#[error("Failed to decrypt local Hex API key")]
FailedToDecryptLocalHexApiKey { detail: String },

#[error("Invalid Credentials file")]
InvalidCredentialsFile { path: String },
}

/// This is to make clippy happy and not make the error variant too big by
Expand Down Expand Up @@ -1487,6 +1490,18 @@ The error from the encryption library was:
}]
}

Error::InvalidCredentialsFile {path}=> {
let text = wrap_format!("Your credentials file at {path} is in the wrong format. Try deleting the file and authenticate again.");

vec![Diagnostic {
title: "Invalid credentials file".into(),
text,
level: Level::Error,
location: None,
hint: None
}]
}

Error::NonUtf8Path { path } => {
let text = format!(
"Encountered a non UTF-8 path '{}', but only UTF-8 paths are supported.",
Expand Down
Loading