feat: add authentication to cli client
ci/woodpecker/push/woodpecker Pipeline failed
Details
ci/woodpecker/push/woodpecker Pipeline failed
Details
parent
72b928067c
commit
9a5a96e6fc
105
cli/src/main.rs
105
cli/src/main.rs
|
@ -4,18 +4,19 @@ extern crate upend_db;
|
||||||
use crate::common::{REQWEST_ASYNC_CLIENT, WEBUI_PATH};
|
use crate::common::{REQWEST_ASYNC_CLIENT, WEBUI_PATH};
|
||||||
use crate::config::UpEndConfig;
|
use crate::config::UpEndConfig;
|
||||||
use actix_web::HttpServer;
|
use actix_web::HttpServer;
|
||||||
use anyhow::Result;
|
use anyhow::{anyhow, Result};
|
||||||
use clap::{Args, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
|
use clap::{Args, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
|
||||||
use filebuffer::FileBuffer;
|
use filebuffer::FileBuffer;
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
use regex::Captures;
|
use regex::Captures;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use serde_json::json;
|
use serde_json::{json, Value};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
@ -58,6 +59,9 @@ enum Commands {
|
||||||
/// Output format
|
/// Output format
|
||||||
#[arg(short, long, default_value = "tsv")]
|
#[arg(short, long, default_value = "tsv")]
|
||||||
format: OutputFormat,
|
format: OutputFormat,
|
||||||
|
/// Credentials
|
||||||
|
#[arg(short, long)]
|
||||||
|
credentials: Credentials,
|
||||||
},
|
},
|
||||||
Get {
|
Get {
|
||||||
/// URL of the UpEnd instance to query.
|
/// URL of the UpEnd instance to query.
|
||||||
|
@ -70,6 +74,9 @@ enum Commands {
|
||||||
/// Output format
|
/// Output format
|
||||||
#[arg(short, long, default_value = "tsv")]
|
#[arg(short, long, default_value = "tsv")]
|
||||||
format: OutputFormat,
|
format: OutputFormat,
|
||||||
|
/// Credentials
|
||||||
|
#[arg(short, long)]
|
||||||
|
credentials: Credentials,
|
||||||
},
|
},
|
||||||
/// Insert an entry into an UpEnd server instance.
|
/// Insert an entry into an UpEnd server instance.
|
||||||
Insert {
|
Insert {
|
||||||
|
@ -85,6 +92,17 @@ enum Commands {
|
||||||
/// Output format
|
/// Output format
|
||||||
#[arg(short, long, default_value = "tsv")]
|
#[arg(short, long, default_value = "tsv")]
|
||||||
format: OutputFormat,
|
format: OutputFormat,
|
||||||
|
/// Credentials
|
||||||
|
#[arg(short, long)]
|
||||||
|
credentials: Credentials,
|
||||||
|
},
|
||||||
|
/// Get authorization token from an UpEnd server instance.
|
||||||
|
Authenticate {
|
||||||
|
/// URL of the UpEnd instance to query.
|
||||||
|
#[arg(short, long, default_value = "http://localhost:8093")]
|
||||||
|
url: Url,
|
||||||
|
/// Credentials
|
||||||
|
credentials: Credentials,
|
||||||
},
|
},
|
||||||
/// Get the address of a file, attribute, or URL.
|
/// Get the address of a file, attribute, or URL.
|
||||||
Address {
|
Address {
|
||||||
|
@ -100,6 +118,30 @@ enum Commands {
|
||||||
Serve(ServeArgs),
|
Serve(ServeArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum Credentials {
|
||||||
|
/// Use an API token to authenticate.
|
||||||
|
Token(String),
|
||||||
|
/// Use a username and password to authenticate.
|
||||||
|
Password(String, String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Credentials {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
let parts: Vec<&str> = s.split(':').collect();
|
||||||
|
match parts.len() {
|
||||||
|
1 => Ok(Credentials::Token(parts[0].to_string())),
|
||||||
|
2 => Ok(Credentials::Password(
|
||||||
|
parts[0].to_string(),
|
||||||
|
parts[1].to_string(),
|
||||||
|
)),
|
||||||
|
_ => Err(anyhow::anyhow!("Invalid credentials format.")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
|
#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
|
||||||
enum OutputFormat {
|
enum OutputFormat {
|
||||||
/// JSON
|
/// JSON
|
||||||
|
@ -191,7 +233,13 @@ async fn main() -> Result<()> {
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
Commands::Query { url, query, format } => {
|
Commands::Query {
|
||||||
|
url,
|
||||||
|
query,
|
||||||
|
format,
|
||||||
|
credentials,
|
||||||
|
} => {
|
||||||
|
let api_token = get_api_token(&url, credentials).await?;
|
||||||
let re = Regex::new(r#"@(="([^"]+)"|=([^ ]+))"#).unwrap();
|
let re = Regex::new(r#"@(="([^"]+)"|=([^ ]+))"#).unwrap();
|
||||||
|
|
||||||
let query = re
|
let query = re
|
||||||
|
@ -210,6 +258,7 @@ async fn main() -> Result<()> {
|
||||||
debug!("Querying \"{}\": {}", api_url, query);
|
debug!("Querying \"{}\": {}", api_url, query);
|
||||||
let response = REQWEST_ASYNC_CLIENT
|
let response = REQWEST_ASYNC_CLIENT
|
||||||
.post(api_url)
|
.post(api_url)
|
||||||
|
.header("Authorization", format!("Bearer {}", api_token))
|
||||||
.body(query)
|
.body(query)
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -225,8 +274,10 @@ async fn main() -> Result<()> {
|
||||||
entity,
|
entity,
|
||||||
attribute,
|
attribute,
|
||||||
format,
|
format,
|
||||||
|
credentials,
|
||||||
} => {
|
} => {
|
||||||
let response = if let Some(attribute) = attribute {
|
let response = if let Some(attribute) = attribute {
|
||||||
|
let api_token = get_api_token(&url, credentials).await?;
|
||||||
let api_url = url.join("/api/query")?;
|
let api_url = url.join("/api/query")?;
|
||||||
|
|
||||||
let entity = match entity {
|
let entity = match entity {
|
||||||
|
@ -242,6 +293,7 @@ async fn main() -> Result<()> {
|
||||||
debug!("Querying \"{}\": {}", api_url, query);
|
debug!("Querying \"{}\": {}", api_url, query);
|
||||||
REQWEST_ASYNC_CLIENT
|
REQWEST_ASYNC_CLIENT
|
||||||
.post(api_url)
|
.post(api_url)
|
||||||
|
.header("Authorization", format!("Bearer {}", api_token))
|
||||||
.body(query)
|
.body(query)
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
|
@ -268,7 +320,9 @@ async fn main() -> Result<()> {
|
||||||
attribute,
|
attribute,
|
||||||
value,
|
value,
|
||||||
format: _,
|
format: _,
|
||||||
|
credentials,
|
||||||
} => {
|
} => {
|
||||||
|
let api_token = get_api_token(&url, credentials).await?;
|
||||||
let api_url = url.join("/api/obj")?;
|
let api_url = url.join("/api/obj")?;
|
||||||
|
|
||||||
let entity = match entity {
|
let entity = match entity {
|
||||||
|
@ -286,7 +340,12 @@ async fn main() -> Result<()> {
|
||||||
});
|
});
|
||||||
|
|
||||||
debug!("Inserting {:?} at \"{}\"", body, api_url);
|
debug!("Inserting {:?} at \"{}\"", body, api_url);
|
||||||
let response = REQWEST_ASYNC_CLIENT.put(api_url).json(&body).send().await?;
|
let response = REQWEST_ASYNC_CLIENT
|
||||||
|
.put(api_url)
|
||||||
|
.header("Authorization", format!("Bearer {}", api_token))
|
||||||
|
.json(&body)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
match response.error_for_status_ref() {
|
match response.error_for_status_ref() {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
@ -299,6 +358,16 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Commands::Authenticate { url, credentials } => match credentials {
|
||||||
|
Credentials::Token(_) => {
|
||||||
|
Err(anyhow!("Please specify a username and password for authentication in the format `username:password`."))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let api_token = get_api_token(&url, credentials).await?;
|
||||||
|
println!("{}", api_token);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
},
|
||||||
Commands::Address {
|
Commands::Address {
|
||||||
_type,
|
_type,
|
||||||
input,
|
input,
|
||||||
|
@ -496,6 +565,34 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_api_token(url: &Url, credentials: Credentials) -> Result<String> {
|
||||||
|
match credentials {
|
||||||
|
Credentials::Token(token) => Ok(token),
|
||||||
|
Credentials::Password(username, password) => {
|
||||||
|
debug!("Logging in as {}...", username);
|
||||||
|
let api_url = url.join("/api/auth/login?via=token").unwrap();
|
||||||
|
let body = json!({
|
||||||
|
"username": username,
|
||||||
|
"password": password
|
||||||
|
});
|
||||||
|
|
||||||
|
let response = REQWEST_ASYNC_CLIENT
|
||||||
|
.post(api_url)
|
||||||
|
.json(&body)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
response.error_for_status_ref()?;
|
||||||
|
|
||||||
|
let data: Value = response.json().await?;
|
||||||
|
data.get("key")
|
||||||
|
.and_then(|key| key.as_str())
|
||||||
|
.map(|key| key.to_string())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("No API token in response."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Entries = HashMap<String, serde_json::Value>;
|
type Entries = HashMap<String, serde_json::Value>;
|
||||||
|
|
||||||
async fn print_response_entries(response: reqwest::Response, format: OutputFormat) -> Result<()> {
|
async fn print_response_entries(response: reqwest::Response, format: OutputFormat) -> Result<()> {
|
||||||
|
|
Loading…
Reference in New Issue