feat: add authentication to cli client
ci/woodpecker/push/woodpecker Pipeline failed Details

main
Tomáš Mládek 2024-06-11 17:28:39 +02:00
parent 72b928067c
commit 9a5a96e6fc
1 changed files with 101 additions and 4 deletions

View File

@ -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<()> {