Skip to content

Commit

Permalink
Allow default Twine upload behavior
Browse files Browse the repository at this point in the history
Allow for users to publish without password/token and provide more control over credentials file writes
  • Loading branch information
cnpryer committed Feb 24, 2024
1 parent 993f42f commit aedfe1a
Showing 1 changed file with 85 additions and 68 deletions.
153 changes: 85 additions & 68 deletions rye/src/cli/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ pub struct Args {
/// Path to alternate CA bundle.
#[arg(long)]
cert: Option<PathBuf>,
/// Skip saving to credentials file.
#[arg(long)]
skip_save_credentials: bool,
/// Skip prompts.
#[arg(short, long)]
yes: bool,
Expand All @@ -68,6 +71,10 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
None => vec![project.workspace_path().join("dist").join("*")],
};

// TODO(cnpryer): Updating resolution
// - Allow twine dispatch without passwords
// - Control over save creds, better flow

// a. Get token from arguments and offer encryption, then store in credentials file.
// b. Get token from ~/.rye/credentials keyed by provided repository and provide decryption option.
// c. Otherwise prompt for token and provide encryption option, storing the result in credentials.
Expand Down Expand Up @@ -97,67 +104,81 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
bail!("invalid pypi url {} (use -h for help)", repository_url);
}

let username = match cmd.username {
Some(username) => username,
None => credentials
let username = cmd.username.or(credentials
.get(repository)
.and_then(|table| table.get("username"))
.map(Item::to_string)
.map(escape_string));

let mut password = cmd.token.map(Secret::new);

if password.is_none() {
if let Some(it) = credentials
.get(repository)
.and_then(|table| table.get("username"))
.map(|username| username.to_string())
.and_then(|table| table.get("token"))
.map(Item::to_string)
.map(escape_string)
.unwrap_or("__token__".to_string()),
};
.map(Secret::new)
{
let phrase = if cmd.yes {
Secret::new(String::new())
} else {
prompt_decrypt_passphrase()?
};
password = Some(decrypt(&it, &phrase)?)
}
}

let token = if let Some(token) = cmd.token {
let secret = Secret::new(token);
let maybe_encrypted = maybe_encrypt(&secret, cmd.yes)?;
let maybe_encoded = maybe_encode(&secret, &maybe_encrypted);
credentials[repository]["token"] = Item::Value(maybe_encoded.expose_secret().into());
write_credentials(&credentials)?;
if password.is_none() && !cmd.yes {
// TODO(cnpryer): Reword?
echo!("No access token found, generate one at: https://pypi.org/manage/account/token/");
let token = Secret::new(prompt_for_token()?);
password = Some(token)
};

secret
} else if let Some(token) = credentials
.get(repository)
.and_then(|table| table.get("token"))
.map(|token| token.to_string())
.map(escape_string)
{
let secret = Secret::new(token);
if !cmd.skip_save_credentials {
if let Some(pwd) = password.as_ref() {
// Encryption step can be skipped using an empty passphrase with --yes form cli
let phrase = if cmd.yes {
Secret::new(String::new())
} else {
prompt_encrypt_passphrase()?
};

let maybe_encrypted = encrypt(pwd, phrase)?;
let maybe_encoded = maybe_encode(pwd, &maybe_encrypted);
credentials[repository]["token"] = Item::Value(maybe_encoded.expose_secret().into());
}

maybe_decrypt(&secret, cmd.yes)?
} else {
echo!("No access token found, generate one at: https://pypi.org/manage/account/token/");
let token = if !cmd.yes {
prompt_for_token()?
} else {
"".to_string()
};
if token.is_empty() {
bail!("an access token is required")
if let Some(usr) = username.as_ref() {
credentials[repository]["username"] = Item::Value(usr.clone().into());
}
let secret = Secret::new(token);
let maybe_encrypted = maybe_encrypt(&secret, cmd.yes)?;
let maybe_encoded = maybe_encode(&secret, &maybe_encrypted);
credentials[repository]["token"] = Item::Value(maybe_encoded.expose_secret().into());

secret
};
credentials[repository]["repository-url"] = Item::Value(repository_url.to_string().into());

credentials[repository]["repository-url"] = Item::Value(repository_url.to_string().into());
credentials[repository]["username"] = Item::Value(username.clone().into());
write_credentials(&credentials)?;
write_credentials(&credentials)?;
}

let mut publish_cmd = Command::new(get_venv_python_bin(&venv));
// TODO(cnpryer): Add --non-interactive if --yes?

publish_cmd
.arg("-mtwine")
.arg("--no-color")
.arg("upload")
.args(files)
.arg("--username")
.arg(username)
.arg("--password")
.arg(token.expose_secret())
.arg("--repository-url")
.arg(repository_url.to_string());

// If a username is provided use it, if a password is provided without a username then use __token__ with Twine.
if let Some(usr) = username.or(password.as_ref().map(|_| "__token__".to_string())) {
publish_cmd.arg("--username").arg(usr);
}

if let Some(pwd) = password.as_ref() {
publish_cmd.arg("--password").arg(pwd.expose_secret());
}

if cmd.sign {
publish_cmd.arg("--sign");
}
Expand Down Expand Up @@ -188,18 +209,7 @@ fn prompt_for_token() -> Result<String, Error> {
Ok(token)
}

fn maybe_encrypt(secret: &Secret<String>, yes: bool) -> Result<Secret<Vec<u8>>, Error> {
let phrase = if !yes {
dialoguer::Password::with_theme(tui_theme())
.with_prompt("Encrypt with passphrase (optional)")
.allow_empty_password(true)
.report(false)
.interact()
.map(Secret::new)?
} else {
Secret::new("".to_string())
};

fn encrypt(secret: &Secret<String>, phrase: Secret<String>) -> Result<Secret<Vec<u8>>, Error> {
let token = if phrase.expose_secret().is_empty() {
secret.expose_secret().as_bytes().to_vec()
} else {
Expand All @@ -216,18 +226,16 @@ fn maybe_encrypt(secret: &Secret<String>, yes: bool) -> Result<Secret<Vec<u8>>,
Ok(Secret::new(token.to_vec()))
}

fn maybe_decrypt(secret: &Secret<String>, yes: bool) -> Result<Secret<String>, Error> {
let phrase = if !yes {
dialoguer::Password::with_theme(tui_theme())
.with_prompt("Decrypt with passphrase (optional)")
.allow_empty_password(true)
.report(false)
.interact()
.map(Secret::new)?
} else {
Secret::new("".to_string())
};
fn prompt_encrypt_passphrase() -> Result<Secret<String>, Error> {
Ok(dialoguer::Password::with_theme(tui_theme())
.with_prompt("Encrypt with passphrase (optional)")
.allow_empty_password(true)
.report(false)
.interact()
.map(Secret::new)?)
}

fn decrypt(secret: &Secret<String>, phrase: &Secret<String>) -> Result<Secret<String>, Error> {
if phrase.expose_secret().is_empty() {
return Ok(secret.clone());
}
Expand All @@ -237,7 +245,7 @@ fn maybe_decrypt(secret: &Secret<String>, yes: bool) -> Result<Secret<String>, E
if let Decryptor::Passphrase(decryptor) = Decryptor::new(bytes.as_slice())? {
// Do the decryption
let mut decrypted = vec![];
let mut reader = decryptor.decrypt(&phrase, None)?;
let mut reader = decryptor.decrypt(phrase, None)?;
reader.read_to_end(&mut decrypted)?;

let token = String::from_utf8(decrypted).context("failed to parse utf-8")?;
Expand All @@ -249,6 +257,15 @@ fn maybe_decrypt(secret: &Secret<String>, yes: bool) -> Result<Secret<String>, E
bail!("failed to decrypt")
}

fn prompt_decrypt_passphrase() -> Result<Secret<String>, Error> {
Ok(dialoguer::Password::with_theme(tui_theme())
.with_prompt("Decrypt with passphrase (optional)")
.allow_empty_password(true)
.report(false)
.interact()
.map(Secret::new)?)
}

fn get_trimmed_user_input() -> Result<String, Error> {
std::io::stderr().flush()?;
let mut input = String::new();
Expand Down

0 comments on commit aedfe1a

Please sign in to comment.