mirror of
https://github.com/pesde-pkg/pesde.git
synced 2024-12-12 11:00:36 +00:00
feat: improve auth system for registry changes
This commit is contained in:
parent
66a885b4e6
commit
c7c1daab36
15 changed files with 183 additions and 270 deletions
|
@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
- Support full version requirements in workspace version field by @daimond113
|
- Support full version requirements in workspace version field by @daimond113
|
||||||
|
- Improved authentication system for registry changes by @daimond113
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Correct `pesde.toml` inclusion message in `publish` command by @daimond113
|
- Correct `pesde.toml` inclusion message in `publish` command by @daimond113
|
||||||
|
|
|
@ -1,23 +1,51 @@
|
||||||
use crate::cli::config::{read_config, write_config};
|
use crate::cli::config::{read_config, write_config};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use gix::bstr::BStr;
|
||||||
use keyring::Entry;
|
use keyring::Entry;
|
||||||
use serde::Deserialize;
|
use reqwest::header::AUTHORIZATION;
|
||||||
|
use serde::{ser::SerializeMap, Deserialize, Serialize};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
pub fn get_token() -> anyhow::Result<Option<String>> {
|
#[derive(Debug, Clone)]
|
||||||
match std::env::var("PESDE_TOKEN") {
|
pub struct Tokens(pub BTreeMap<gix::Url, String>);
|
||||||
Ok(token) => return Ok(Some(token)),
|
|
||||||
Err(std::env::VarError::NotPresent) => {}
|
impl Serialize for Tokens {
|
||||||
Err(e) => return Err(e.into()),
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::ser::Serializer,
|
||||||
|
{
|
||||||
|
let mut map = serializer.serialize_map(Some(self.0.len()))?;
|
||||||
|
for (k, v) in &self.0 {
|
||||||
|
map.serialize_entry(&k.to_bstring().to_string(), v)?;
|
||||||
|
}
|
||||||
|
map.end()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Tokens {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::de::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
Ok(Tokens(
|
||||||
|
BTreeMap::<String, String>::deserialize(deserializer)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| gix::Url::from_bytes(BStr::new(&k)).map(|k| (k, v)))
|
||||||
|
.collect::<Result<_, _>>()
|
||||||
|
.map_err(serde::de::Error::custom)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_tokens() -> anyhow::Result<Tokens> {
|
||||||
let config = read_config()?;
|
let config = read_config()?;
|
||||||
if let Some(token) = config.token {
|
if !config.tokens.0.is_empty() {
|
||||||
return Ok(Some(token));
|
return Ok(config.tokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
match Entry::new("token", env!("CARGO_PKG_NAME")) {
|
match Entry::new("tokens", env!("CARGO_PKG_NAME")) {
|
||||||
Ok(entry) => match entry.get_password() {
|
Ok(entry) => match entry.get_password() {
|
||||||
Ok(token) => return Ok(Some(token)),
|
Ok(token) => return serde_json::from_str(&token).context("failed to parse tokens"),
|
||||||
Err(keyring::Error::PlatformFailure(_) | keyring::Error::NoEntry) => {}
|
Err(keyring::Error::PlatformFailure(_) | keyring::Error::NoEntry) => {}
|
||||||
Err(e) => return Err(e.into()),
|
Err(e) => return Err(e.into()),
|
||||||
},
|
},
|
||||||
|
@ -25,32 +53,32 @@ pub fn get_token() -> anyhow::Result<Option<String>> {
|
||||||
Err(e) => return Err(e.into()),
|
Err(e) => return Err(e.into()),
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
Ok(Tokens(BTreeMap::new()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_token(token: Option<&str>) -> anyhow::Result<()> {
|
pub fn set_tokens(tokens: Tokens) -> anyhow::Result<()> {
|
||||||
let entry = match Entry::new("token", env!("CARGO_PKG_NAME")) {
|
let entry = Entry::new("tokens", env!("CARGO_PKG_NAME"))?;
|
||||||
Ok(entry) => entry,
|
let json = serde_json::to_string(&tokens).context("failed to serialize tokens")?;
|
||||||
Err(e) => return Err(e.into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = if let Some(token) = token {
|
match entry.set_password(&json) {
|
||||||
entry.set_password(token)
|
|
||||||
} else {
|
|
||||||
entry.delete_credential()
|
|
||||||
};
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(()) => return Ok(()),
|
Ok(()) => return Ok(()),
|
||||||
Err(keyring::Error::PlatformFailure(_) | keyring::Error::NoEntry) => {}
|
Err(keyring::Error::PlatformFailure(_) | keyring::Error::NoEntry) => {}
|
||||||
Err(e) => return Err(e.into()),
|
Err(e) => return Err(e.into()),
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut config = read_config()?;
|
let mut config = read_config()?;
|
||||||
config.token = token.map(|s| s.to_string());
|
config.tokens = tokens;
|
||||||
write_config(&config)?;
|
write_config(&config).map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
pub fn set_token(repo: &gix::Url, token: Option<&str>) -> anyhow::Result<()> {
|
||||||
|
let mut tokens = get_tokens()?;
|
||||||
|
if let Some(token) = token {
|
||||||
|
tokens.0.insert(repo.clone(), token.to_string());
|
||||||
|
} else {
|
||||||
|
tokens.0.remove(repo);
|
||||||
|
}
|
||||||
|
set_tokens(tokens)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
@ -64,7 +92,7 @@ pub fn get_token_login(
|
||||||
) -> anyhow::Result<String> {
|
) -> anyhow::Result<String> {
|
||||||
let response = reqwest
|
let response = reqwest
|
||||||
.get("https://api.github.com/user")
|
.get("https://api.github.com/user")
|
||||||
.header("Authorization", access_token)
|
.header(AUTHORIZATION, access_token)
|
||||||
.send()
|
.send()
|
||||||
.context("failed to send user request")?
|
.context("failed to send user request")?
|
||||||
.error_for_status()
|
.error_for_status()
|
||||||
|
|
|
@ -5,24 +5,16 @@ use serde::Deserialize;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use pesde::{
|
use pesde::{
|
||||||
errors::ManifestReadError,
|
|
||||||
source::{pesde::PesdePackageSource, traits::PackageSource},
|
source::{pesde::PesdePackageSource, traits::PackageSource},
|
||||||
Project,
|
Project,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::cli::{
|
use crate::cli::auth::{get_token_login, set_token};
|
||||||
auth::{get_token_login, set_token},
|
|
||||||
config::read_config,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
pub struct LoginCommand {
|
pub struct LoginCommand {
|
||||||
/// The index to use. Defaults to `default`, or the configured default index if current directory doesn't have a manifest
|
|
||||||
#[arg(short, long)]
|
|
||||||
index: Option<String>,
|
|
||||||
|
|
||||||
/// The token to use for authentication, skipping login
|
/// The token to use for authentication, skipping login
|
||||||
#[arg(short, long, conflicts_with = "index")]
|
#[arg(short, long)]
|
||||||
token: Option<String>,
|
token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,41 +47,13 @@ enum AccessTokenResponse {
|
||||||
impl LoginCommand {
|
impl LoginCommand {
|
||||||
pub fn authenticate_device_flow(
|
pub fn authenticate_device_flow(
|
||||||
&self,
|
&self,
|
||||||
|
index_url: &gix::Url,
|
||||||
project: &Project,
|
project: &Project,
|
||||||
reqwest: &reqwest::blocking::Client,
|
reqwest: &reqwest::blocking::Client,
|
||||||
) -> anyhow::Result<String> {
|
) -> anyhow::Result<String> {
|
||||||
let manifest = match project.deser_manifest() {
|
println!("logging in into {index_url}");
|
||||||
Ok(manifest) => Some(manifest),
|
|
||||||
Err(e) => match e {
|
|
||||||
ManifestReadError::Io(e) if e.kind() == std::io::ErrorKind::NotFound => None,
|
|
||||||
e => return Err(e.into()),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let index_url = match self.index.as_deref() {
|
let source = PesdePackageSource::new(index_url.clone());
|
||||||
Some(index) => match index.try_into() {
|
|
||||||
Ok(url) => Some(url),
|
|
||||||
Err(_) => None,
|
|
||||||
},
|
|
||||||
None => match manifest {
|
|
||||||
Some(_) => None,
|
|
||||||
None => Some(read_config()?.default_index),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let index_url = match index_url {
|
|
||||||
Some(url) => url,
|
|
||||||
None => {
|
|
||||||
let index_name = self.index.as_deref().unwrap_or("default");
|
|
||||||
|
|
||||||
match manifest.unwrap().indices.get(index_name) {
|
|
||||||
Some(index) => index.clone(),
|
|
||||||
None => anyhow::bail!("Index {index_name} not found"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let source = PesdePackageSource::new(index_url);
|
|
||||||
source.refresh(project).context("failed to refresh index")?;
|
source.refresh(project).context("failed to refresh index")?;
|
||||||
|
|
||||||
let config = source
|
let config = source
|
||||||
|
@ -182,24 +146,32 @@ impl LoginCommand {
|
||||||
anyhow::bail!("code expired, please re-run the login command");
|
anyhow::bail!("code expired, please re-run the login command");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(self, project: Project, reqwest: reqwest::blocking::Client) -> anyhow::Result<()> {
|
pub fn run(
|
||||||
|
self,
|
||||||
|
index_url: gix::Url,
|
||||||
|
project: Project,
|
||||||
|
reqwest: reqwest::blocking::Client,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
let token_given = self.token.is_some();
|
let token_given = self.token.is_some();
|
||||||
let token = match self.token {
|
let token = match self.token {
|
||||||
Some(token) => token,
|
Some(token) => token,
|
||||||
None => self.authenticate_device_flow(&project, &reqwest)?,
|
None => self.authenticate_device_flow(&index_url, &project, &reqwest)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let token = if token_given {
|
let token = if token_given {
|
||||||
println!("set token");
|
println!("set token for {index_url}");
|
||||||
token
|
token
|
||||||
} else {
|
} else {
|
||||||
let token = format!("Bearer {token}");
|
let token = format!("Bearer {token}");
|
||||||
println!("logged in as {}", get_token_login(&reqwest, &token)?.bold());
|
println!(
|
||||||
|
"logged in as {} for {index_url}",
|
||||||
|
get_token_login(&reqwest, &token)?.bold()
|
||||||
|
);
|
||||||
|
|
||||||
token
|
token
|
||||||
};
|
};
|
||||||
|
|
||||||
set_token(Some(&token))?;
|
set_token(&index_url, Some(&token))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,10 @@ use clap::Args;
|
||||||
pub struct LogoutCommand {}
|
pub struct LogoutCommand {}
|
||||||
|
|
||||||
impl LogoutCommand {
|
impl LogoutCommand {
|
||||||
pub fn run(self) -> anyhow::Result<()> {
|
pub fn run(self, index_url: gix::Url) -> anyhow::Result<()> {
|
||||||
set_token(None)?;
|
set_token(&index_url, None)?;
|
||||||
|
|
||||||
println!("logged out");
|
println!("logged out of {index_url}");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,69 @@
|
||||||
use clap::Subcommand;
|
use crate::cli::config::read_config;
|
||||||
use pesde::Project;
|
use clap::{Args, Subcommand};
|
||||||
|
use pesde::{errors::ManifestReadError, Project};
|
||||||
|
|
||||||
mod login;
|
mod login;
|
||||||
mod logout;
|
mod logout;
|
||||||
mod set_token_override;
|
|
||||||
mod whoami;
|
mod whoami;
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct AuthSubcommand {
|
||||||
|
/// The index to use. Defaults to `default`, or the configured default index if current directory doesn't have a manifest
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub index: Option<String>,
|
||||||
|
|
||||||
|
#[clap(subcommand)]
|
||||||
|
pub command: AuthCommands,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub enum AuthCommands {
|
pub enum AuthCommands {
|
||||||
/// Logs in into GitHub, and stores the token
|
/// Sets a token for an index. Optionally gets it from GitHub
|
||||||
Login(login::LoginCommand),
|
Login(login::LoginCommand),
|
||||||
/// Removes the stored token
|
/// Removes the stored token
|
||||||
Logout(logout::LogoutCommand),
|
Logout(logout::LogoutCommand),
|
||||||
/// Prints the username of the currently logged-in user
|
/// Prints the username of the currently logged-in user
|
||||||
#[clap(name = "whoami")]
|
#[clap(name = "whoami")]
|
||||||
WhoAmI(whoami::WhoAmICommand),
|
WhoAmI(whoami::WhoAmICommand),
|
||||||
/// Sets a token override for a specific repository
|
|
||||||
SetTokenOverride(set_token_override::SetTokenOverrideCommand),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuthCommands {
|
impl AuthSubcommand {
|
||||||
pub fn run(self, project: Project, reqwest: reqwest::blocking::Client) -> anyhow::Result<()> {
|
pub fn run(self, project: Project, reqwest: reqwest::blocking::Client) -> anyhow::Result<()> {
|
||||||
match self {
|
let manifest = match project.deser_manifest() {
|
||||||
AuthCommands::Login(login) => login.run(project, reqwest),
|
Ok(manifest) => Some(manifest),
|
||||||
AuthCommands::Logout(logout) => logout.run(),
|
Err(e) => match e {
|
||||||
AuthCommands::WhoAmI(whoami) => whoami.run(reqwest),
|
ManifestReadError::Io(e) if e.kind() == std::io::ErrorKind::NotFound => None,
|
||||||
AuthCommands::SetTokenOverride(set_token_override) => set_token_override.run(),
|
e => return Err(e.into()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let index_url = match self.index.as_deref() {
|
||||||
|
Some(index) => match index.try_into() {
|
||||||
|
Ok(url) => Some(url),
|
||||||
|
Err(_) => None,
|
||||||
|
},
|
||||||
|
None => match manifest {
|
||||||
|
Some(_) => None,
|
||||||
|
None => Some(read_config()?.default_index),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let index_url = match index_url {
|
||||||
|
Some(url) => url,
|
||||||
|
None => {
|
||||||
|
let index_name = self.index.as_deref().unwrap_or("default");
|
||||||
|
|
||||||
|
match manifest.unwrap().indices.get(index_name) {
|
||||||
|
Some(index) => index.clone(),
|
||||||
|
None => anyhow::bail!("index {index_name} not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.command {
|
||||||
|
AuthCommands::Login(login) => login.run(index_url, project, reqwest),
|
||||||
|
AuthCommands::Logout(logout) => logout.run(index_url),
|
||||||
|
AuthCommands::WhoAmI(whoami) => whoami.run(index_url, reqwest),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
use crate::cli::config::{read_config, write_config};
|
|
||||||
use clap::Args;
|
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
|
||||||
pub struct SetTokenOverrideCommand {
|
|
||||||
/// The repository to add the token to
|
|
||||||
#[arg(index = 1, value_parser = crate::cli::parse_gix_url)]
|
|
||||||
repository: gix::Url,
|
|
||||||
|
|
||||||
/// The token to set
|
|
||||||
#[arg(index = 2)]
|
|
||||||
token: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SetTokenOverrideCommand {
|
|
||||||
pub fn run(self) -> anyhow::Result<()> {
|
|
||||||
let mut config = read_config()?;
|
|
||||||
|
|
||||||
if let Some(token) = self.token {
|
|
||||||
println!("set token for {}", self.repository);
|
|
||||||
config.token_overrides.insert(self.repository, token);
|
|
||||||
} else {
|
|
||||||
println!("removed token for {}", self.repository);
|
|
||||||
config.token_overrides.remove(&self.repository);
|
|
||||||
}
|
|
||||||
|
|
||||||
write_config(&config)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::cli::{auth::get_token_login, get_token};
|
use crate::cli::auth::{get_token_login, get_tokens};
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
|
|
||||||
|
@ -6,16 +6,24 @@ use colored::Colorize;
|
||||||
pub struct WhoAmICommand {}
|
pub struct WhoAmICommand {}
|
||||||
|
|
||||||
impl WhoAmICommand {
|
impl WhoAmICommand {
|
||||||
pub fn run(self, reqwest: reqwest::blocking::Client) -> anyhow::Result<()> {
|
pub fn run(
|
||||||
let token = match get_token()? {
|
self,
|
||||||
|
index_url: gix::Url,
|
||||||
|
reqwest: reqwest::blocking::Client,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let tokens = get_tokens()?;
|
||||||
|
let token = match tokens.0.get(&index_url) {
|
||||||
Some(token) => token,
|
Some(token) => token,
|
||||||
None => {
|
None => {
|
||||||
println!("not logged in");
|
println!("not logged in into {index_url}");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("logged in as {}", get_token_login(&reqwest, &token)?.bold());
|
println!(
|
||||||
|
"logged in as {} into {index_url}",
|
||||||
|
get_token_login(&reqwest, token)?.bold()
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,7 @@ mod update;
|
||||||
#[derive(Debug, clap::Subcommand)]
|
#[derive(Debug, clap::Subcommand)]
|
||||||
pub enum Subcommand {
|
pub enum Subcommand {
|
||||||
/// Authentication-related commands
|
/// Authentication-related commands
|
||||||
#[command(subcommand)]
|
Auth(auth::AuthSubcommand),
|
||||||
Auth(auth::AuthCommands),
|
|
||||||
|
|
||||||
/// Configuration-related commands
|
/// Configuration-related commands
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use reqwest::StatusCode;
|
use reqwest::{header::AUTHORIZATION, StatusCode};
|
||||||
use semver::VersionReq;
|
use semver::VersionReq;
|
||||||
use std::{
|
use std::{
|
||||||
io::{Seek, Write},
|
io::{Seek, Write},
|
||||||
|
@ -457,13 +457,11 @@ impl PublishCommand {
|
||||||
.finish()
|
.finish()
|
||||||
.context("failed to get archive bytes")?;
|
.context("failed to get archive bytes")?;
|
||||||
|
|
||||||
let source = PesdePackageSource::new(
|
let index_url = manifest
|
||||||
manifest
|
.indices
|
||||||
.indices
|
.get(DEFAULT_INDEX_NAME)
|
||||||
.get(DEFAULT_INDEX_NAME)
|
.context("missing default index")?;
|
||||||
.context("missing default index")?
|
let source = PesdePackageSource::new(index_url.clone());
|
||||||
.clone(),
|
|
||||||
);
|
|
||||||
source
|
source
|
||||||
.refresh(project)
|
.refresh(project)
|
||||||
.context("failed to refresh source")?;
|
.context("failed to refresh source")?;
|
||||||
|
@ -501,14 +499,19 @@ impl PublishCommand {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = reqwest
|
let mut request = reqwest
|
||||||
.post(format!("{}/v0/packages", config.api()))
|
.post(format!("{}/v0/packages", config.api()))
|
||||||
.multipart(reqwest::blocking::multipart::Form::new().part(
|
.multipart(reqwest::blocking::multipart::Form::new().part(
|
||||||
"tarball",
|
"tarball",
|
||||||
reqwest::blocking::multipart::Part::bytes(archive).file_name("package.tar.gz"),
|
reqwest::blocking::multipart::Part::bytes(archive).file_name("package.tar.gz"),
|
||||||
))
|
));
|
||||||
.send()
|
|
||||||
.context("failed to send request")?;
|
if let Some(token) = project.auth_config().tokens().get(index_url) {
|
||||||
|
log::debug!("using token for {index_url}");
|
||||||
|
request = request.header(AUTHORIZATION, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = request.send().context("failed to send request")?;
|
||||||
|
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
let text = response.text().context("failed to get response text")?;
|
let text = response.text().context("failed to get response text")?;
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
use std::collections::BTreeMap;
|
use crate::cli::{auth::Tokens, home_dir};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::cli::home_dir;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct CliConfig {
|
pub struct CliConfig {
|
||||||
#[serde(
|
#[serde(
|
||||||
|
@ -18,15 +15,7 @@ pub struct CliConfig {
|
||||||
)]
|
)]
|
||||||
pub scripts_repo: gix::Url,
|
pub scripts_repo: gix::Url,
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
pub tokens: Tokens,
|
||||||
pub token: Option<String>,
|
|
||||||
#[serde(
|
|
||||||
default,
|
|
||||||
skip_serializing_if = "BTreeMap::is_empty",
|
|
||||||
serialize_with = "crate::cli::serialize_string_url_map",
|
|
||||||
deserialize_with = "crate::cli::deserialize_string_url_map"
|
|
||||||
)]
|
|
||||||
pub token_overrides: BTreeMap<gix::Url, String>,
|
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub last_checked_updates: Option<(chrono::DateTime<chrono::Utc>, semver::Version)>,
|
pub last_checked_updates: Option<(chrono::DateTime<chrono::Utc>, semver::Version)>,
|
||||||
|
@ -42,8 +31,7 @@ impl Default for CliConfig {
|
||||||
.try_into()
|
.try_into()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
|
||||||
token: None,
|
tokens: Tokens(Default::default()),
|
||||||
token_overrides: Default::default(),
|
|
||||||
|
|
||||||
last_checked_updates: None,
|
last_checked_updates: None,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
use crate::cli::auth::get_token;
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use gix::bstr::BStr;
|
|
||||||
use indicatif::MultiProgress;
|
use indicatif::MultiProgress;
|
||||||
use pesde::{
|
use pesde::{
|
||||||
lockfile::{DependencyGraph, DownloadedGraph, Lockfile},
|
lockfile::{DependencyGraph, DownloadedGraph, Lockfile},
|
||||||
|
@ -10,7 +8,6 @@ use pesde::{
|
||||||
Project,
|
Project,
|
||||||
};
|
};
|
||||||
use relative_path::RelativePathBuf;
|
use relative_path::RelativePathBuf;
|
||||||
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serializer};
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, HashSet},
|
collections::{BTreeMap, HashSet},
|
||||||
fs::create_dir_all,
|
fs::create_dir_all,
|
||||||
|
@ -188,30 +185,6 @@ pub fn parse_gix_url(s: &str) -> Result<gix::Url, gix::url::parse::Error> {
|
||||||
s.try_into()
|
s.try_into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serialize_string_url_map<S: Serializer>(
|
|
||||||
url: &BTreeMap<gix::Url, String>,
|
|
||||||
serializer: S,
|
|
||||||
) -> Result<S::Ok, S::Error> {
|
|
||||||
let mut map = serializer.serialize_map(Some(url.len()))?;
|
|
||||||
for (k, v) in url {
|
|
||||||
map.serialize_entry(&k.to_bstring().to_string(), v)?;
|
|
||||||
}
|
|
||||||
map.end()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deserialize_string_url_map<'de, D: Deserializer<'de>>(
|
|
||||||
deserializer: D,
|
|
||||||
) -> Result<BTreeMap<gix::Url, String>, D::Error> {
|
|
||||||
BTreeMap::<String, String>::deserialize(deserializer)?
|
|
||||||
.into_iter()
|
|
||||||
.map(|(k, v)| {
|
|
||||||
gix::Url::from_bytes(BStr::new(&k))
|
|
||||||
.map(|k| (k, v))
|
|
||||||
.map_err(serde::de::Error::custom)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn download_graph(
|
pub fn download_graph(
|
||||||
project: &Project,
|
project: &Project,
|
||||||
|
|
33
src/lib.rs
33
src/lib.rs
|
@ -44,8 +44,7 @@ pub(crate) const LINK_LIB_NO_FILE_FOUND: &str = "____pesde_no_export_file_found"
|
||||||
/// Struct containing the authentication configuration
|
/// Struct containing the authentication configuration
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct AuthConfig {
|
pub struct AuthConfig {
|
||||||
default_token: Option<String>,
|
tokens: HashMap<gix::Url, String>,
|
||||||
token_overrides: HashMap<gix::Url, String>,
|
|
||||||
git_credentials: Option<Account>,
|
git_credentials: Option<Account>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,18 +54,12 @@ impl AuthConfig {
|
||||||
AuthConfig::default()
|
AuthConfig::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the default token
|
/// Set the tokens
|
||||||
pub fn with_default_token<S: AsRef<str>>(mut self, token: Option<S>) -> Self {
|
pub fn with_tokens<I: IntoIterator<Item = (gix::Url, S)>, S: AsRef<str>>(
|
||||||
self.default_token = token.map(|s| s.as_ref().to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the token overrides
|
|
||||||
pub fn with_token_overrides<I: IntoIterator<Item = (gix::Url, S)>, S: AsRef<str>>(
|
|
||||||
mut self,
|
mut self,
|
||||||
tokens: I,
|
tokens: I,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.token_overrides = tokens
|
self.tokens = tokens
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(url, s)| (url, s.as_ref().to_string()))
|
.map(|(url, s)| (url, s.as_ref().to_string()))
|
||||||
.collect();
|
.collect();
|
||||||
|
@ -79,27 +72,15 @@ impl AuthConfig {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the default token
|
/// Get the tokens
|
||||||
pub fn default_token(&self) -> Option<&str> {
|
pub fn tokens(&self) -> &HashMap<gix::Url, String> {
|
||||||
self.default_token.as_deref()
|
&self.tokens
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the token overrides
|
|
||||||
pub fn token_overrides(&self) -> &HashMap<gix::Url, String> {
|
|
||||||
&self.token_overrides
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the git credentials
|
/// Get the git credentials
|
||||||
pub fn git_credentials(&self) -> Option<&Account> {
|
pub fn git_credentials(&self) -> Option<&Account> {
|
||||||
self.git_credentials.as_ref()
|
self.git_credentials.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_token(&self, url: &gix::Url) -> Option<&str> {
|
|
||||||
self.token_overrides
|
|
||||||
.get(url)
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.or(self.default_token.as_deref())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The main struct of the pesde library, representing a project
|
/// The main struct of the pesde library, representing a project
|
||||||
|
|
18
src/main.rs
18
src/main.rs
|
@ -14,9 +14,7 @@ use std::{
|
||||||
use crate::cli::version::{
|
use crate::cli::version::{
|
||||||
check_for_updates, current_version, get_or_download_version, max_installed_version,
|
check_for_updates, current_version, get_or_download_version, max_installed_version,
|
||||||
};
|
};
|
||||||
use crate::cli::{
|
use crate::cli::{auth::get_tokens, home_dir, repos::update_repo_dependencies, HOME_DIR};
|
||||||
auth::get_token, config::read_config, home_dir, repos::update_repo_dependencies, HOME_DIR,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
@ -187,14 +185,14 @@ fn run() -> anyhow::Result<()> {
|
||||||
let data_dir = home_dir()?.join("data");
|
let data_dir = home_dir()?.join("data");
|
||||||
create_dir_all(&data_dir).expect("failed to create data directory");
|
create_dir_all(&data_dir).expect("failed to create data directory");
|
||||||
|
|
||||||
let token = get_token()?;
|
|
||||||
|
|
||||||
let home_cas_dir = data_dir.join("cas");
|
let home_cas_dir = data_dir.join("cas");
|
||||||
create_dir_all(&home_cas_dir).expect("failed to create cas directory");
|
create_dir_all(&home_cas_dir).expect("failed to create cas directory");
|
||||||
let project_root = get_root(&project_root_dir);
|
let project_root = get_root(&project_root_dir);
|
||||||
let cas_dir = if get_root(&home_cas_dir) == project_root {
|
let cas_dir = if get_root(&home_cas_dir) == project_root {
|
||||||
|
log::debug!("using home cas dir");
|
||||||
home_cas_dir
|
home_cas_dir
|
||||||
} else {
|
} else {
|
||||||
|
log::debug!("using cas dir in {}", project_root.display());
|
||||||
project_root.join(HOME_DIR).join("cas")
|
project_root.join(HOME_DIR).join("cas")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -203,19 +201,11 @@ fn run() -> anyhow::Result<()> {
|
||||||
project_workspace_dir,
|
project_workspace_dir,
|
||||||
data_dir,
|
data_dir,
|
||||||
cas_dir,
|
cas_dir,
|
||||||
AuthConfig::new()
|
AuthConfig::new().with_tokens(get_tokens()?.0),
|
||||||
.with_default_token(token.clone())
|
|
||||||
.with_token_overrides(read_config()?.token_overrides),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let reqwest = {
|
let reqwest = {
|
||||||
let mut headers = reqwest::header::HeaderMap::new();
|
let mut headers = reqwest::header::HeaderMap::new();
|
||||||
if let Some(token) = token {
|
|
||||||
headers.insert(
|
|
||||||
reqwest::header::AUTHORIZATION,
|
|
||||||
token.parse().context("failed to create auth header")?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
headers.insert(
|
headers.insert(
|
||||||
reqwest::header::ACCEPT,
|
reqwest::header::ACCEPT,
|
||||||
|
|
|
@ -7,7 +7,7 @@ use std::{
|
||||||
|
|
||||||
use gix::Url;
|
use gix::Url;
|
||||||
use relative_path::RelativePathBuf;
|
use relative_path::RelativePathBuf;
|
||||||
use reqwest::header::{HeaderMap, ACCEPT, AUTHORIZATION};
|
use reqwest::header::{ACCEPT, AUTHORIZATION};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use pkg_ref::PesdePackageRef;
|
use pkg_ref::PesdePackageRef;
|
||||||
|
@ -273,29 +273,14 @@ impl PackageSource for PesdePackageSource {
|
||||||
.replace("{PACKAGE_VERSION}", &pkg_ref.version.to_string())
|
.replace("{PACKAGE_VERSION}", &pkg_ref.version.to_string())
|
||||||
.replace("{PACKAGE_TARGET}", &pkg_ref.target.to_string());
|
.replace("{PACKAGE_TARGET}", &pkg_ref.target.to_string());
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
let mut request = reqwest.get(&url).header(ACCEPT, "application/octet-stream");
|
||||||
headers.insert(
|
|
||||||
ACCEPT,
|
|
||||||
"application/octet-stream"
|
|
||||||
.parse()
|
|
||||||
.map_err(|e| errors::DownloadError::InvalidHeaderValue("Accept".to_string(), e))?,
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(token) = project.auth_config.get_token(&self.repo_url) {
|
if let Some(token) = project.auth_config.tokens().get(&self.repo_url) {
|
||||||
log::debug!("using token for pesde package download");
|
log::debug!("using token for {}", self.repo_url);
|
||||||
headers.insert(
|
request = request.header(AUTHORIZATION, token);
|
||||||
AUTHORIZATION,
|
|
||||||
token.parse().map_err(|e| {
|
|
||||||
errors::DownloadError::InvalidHeaderValue("Authorization".to_string(), e)
|
|
||||||
})?,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = reqwest
|
let response = request.send()?.error_for_status()?;
|
||||||
.get(url)
|
|
||||||
.headers(headers)
|
|
||||||
.send()?
|
|
||||||
.error_for_status()?;
|
|
||||||
let bytes = response.bytes()?;
|
let bytes = response.bytes()?;
|
||||||
|
|
||||||
let mut decoder = flate2::read::GzDecoder::new(bytes.as_ref());
|
let mut decoder = flate2::read::GzDecoder::new(bytes.as_ref());
|
||||||
|
@ -583,9 +568,5 @@ pub mod errors {
|
||||||
/// Error writing index file
|
/// Error writing index file
|
||||||
#[error("error reading index file")]
|
#[error("error reading index file")]
|
||||||
ReadIndex(#[source] std::io::Error),
|
ReadIndex(#[source] std::io::Error),
|
||||||
|
|
||||||
/// A header value was invalid
|
|
||||||
#[error("invalid header {0} value")]
|
|
||||||
InvalidHeaderValue(String, #[source] reqwest::header::InvalidHeaderValue),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ use std::{
|
||||||
|
|
||||||
use gix::Url;
|
use gix::Url;
|
||||||
use relative_path::RelativePathBuf;
|
use relative_path::RelativePathBuf;
|
||||||
use reqwest::header::{HeaderMap, AUTHORIZATION};
|
use reqwest::header::AUTHORIZATION;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
@ -178,33 +178,19 @@ impl PackageSource for WallyPackageSource {
|
||||||
pkg_ref.version
|
pkg_ref.version
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
let mut request = reqwest.get(&url).header(
|
||||||
headers.insert(
|
|
||||||
"Wally-Version",
|
"Wally-Version",
|
||||||
std::env::var("PESDE_WALLY_VERSION")
|
std::env::var("PESDE_WALLY_VERSION")
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.unwrap_or("0.3.2")
|
.unwrap_or("0.3.2"),
|
||||||
.parse()
|
|
||||||
.map_err(|e| {
|
|
||||||
errors::DownloadError::InvalidHeaderValue("Wally-Version".to_string(), e)
|
|
||||||
})?,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(token) = project.auth_config.get_token(&self.repo_url) {
|
if let Some(token) = project.auth_config.tokens().get(&self.repo_url) {
|
||||||
log::debug!("using token for wally package download");
|
log::debug!("using token for {}", self.repo_url);
|
||||||
headers.insert(
|
request = request.header(AUTHORIZATION, token);
|
||||||
AUTHORIZATION,
|
|
||||||
token.parse().map_err(|e| {
|
|
||||||
errors::DownloadError::InvalidHeaderValue("Authorization".to_string(), e)
|
|
||||||
})?,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = reqwest
|
let response = request.send()?.error_for_status()?;
|
||||||
.get(url)
|
|
||||||
.headers(headers)
|
|
||||||
.send()?
|
|
||||||
.error_for_status()?;
|
|
||||||
let bytes = response.bytes()?;
|
let bytes = response.bytes()?;
|
||||||
|
|
||||||
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(bytes))?;
|
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(bytes))?;
|
||||||
|
@ -355,9 +341,5 @@ pub mod errors {
|
||||||
/// Error writing index file
|
/// Error writing index file
|
||||||
#[error("error writing index file")]
|
#[error("error writing index file")]
|
||||||
WriteIndex(#[source] std::io::Error),
|
WriteIndex(#[source] std::io::Error),
|
||||||
|
|
||||||
/// A header value was invalid
|
|
||||||
#[error("invalid header {0} value")]
|
|
||||||
InvalidHeaderValue(String, #[source] reqwest::header::InvalidHeaderValue),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue