Skip to content

Commit

Permalink
refactor(rust): extract token handling to a agama_lib::auth (#1287)
Browse files Browse the repository at this point in the history
This PR aims to bring the token related code to a single place. It
introduces an `AuthToken` struct
that offers a simplistic API to token management.

Additionally, it introduces a behaviour change: if the /run/agama/token
is available and readable,
the CLI uses such a token.
  • Loading branch information
imobachgs authored Jun 5, 2024
2 parents f7bf531 + e8441d7 commit 8bebd79
Show file tree
Hide file tree
Showing 13 changed files with 265 additions and 203 deletions.
8 changes: 5 additions & 3 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion rust/agama-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ zbus = { version = "3", default-features = false, features = ["tokio"] }
tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread"] }
async-trait = "0.1.77"
reqwest = { version = "0.11", features = ["json"] }
home = "0.5.9"
rpassword = "7.3.1"
url = "2.5.0"

Expand Down
114 changes: 7 additions & 107 deletions rust/agama-cli/src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
use agama_lib::auth::AuthToken;
use clap::Subcommand;

use crate::error::CliError;
use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
use std::fs;
use std::fs::File;
use std::io::{self, IsTerminal};
use std::io::{BufRead, BufReader};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};

use crate::error::CliError;

const DEFAULT_JWT_FILE: &str = ".agama/agama-jwt";
const DEFAULT_AGAMA_TOKEN_FILE: &str = "/run/agama/token";
const DEFAULT_AUTH_URL: &str = "http://localhost/api/auth";
const DEFAULT_FILE_MODE: u32 = 0o600;

#[derive(Subcommand, Debug)]
pub enum AuthCommands {
Expand All @@ -38,28 +30,6 @@ pub async fn run(subcommand: AuthCommands) -> anyhow::Result<()> {
}
}

/// Returns the stored Agama token.
pub fn agama_token() -> anyhow::Result<String> {
if let Some(file) = agama_token_file() {
if let Ok(token) = read_line_from_file(file.as_path()) {
return Ok(token);
}
}

Err(anyhow::anyhow!("Authentication token not available"))
}

/// Reads stored token and returns it
pub fn jwt() -> anyhow::Result<String> {
if let Some(file) = jwt_file() {
if let Ok(token) = read_line_from_file(file.as_path()) {
return Ok(token);
}
}

Err(anyhow::anyhow!("Authentication token not available"))
}

/// Reads the password
///
/// It reads the password from stdin if available; otherwise, it asks the
Expand All @@ -76,53 +46,6 @@ fn read_password() -> Result<String, CliError> {
Ok(password)
}

/// Path to file where JWT is stored
fn jwt_file() -> Option<PathBuf> {
Some(home::home_dir()?.join(DEFAULT_JWT_FILE))
}
/// Path to agama-live token file.
fn agama_token_file() -> Option<PathBuf> {
home::home_dir().map(|p| p.join(DEFAULT_AGAMA_TOKEN_FILE))
}

/// Reads first line from given file
fn read_line_from_file(path: &Path) -> io::Result<String> {
if !path.exists() {
return Err(io::Error::new(
io::ErrorKind::Other,
"Cannot find the file containing the credentials.",
));
}

if let Ok(file) = File::open(path) {
// cares only of first line, take everything. No comments
// or something like that supported
let raw = BufReader::new(file).lines().next();

if let Some(line) = raw {
return line;
}
}

Err(io::Error::new(
io::ErrorKind::Other,
"Failed to open the file",
))
}

/// Sets the archive owner to root:root. Also sets the file permissions to read/write for the
/// owner only.
fn set_file_permissions(file: &Path) -> io::Result<()> {
let attr = fs::metadata(file)?;
let mut permissions = attr.permissions();

// set the file file permissions to -rw-------
permissions.set_mode(DEFAULT_FILE_MODE);
fs::set_permissions(file, permissions)?;

Ok(())
}

/// Necessary http request header for authenticate
fn authenticate_headers() -> HeaderMap {
let mut headers = HeaderMap::new();
Expand Down Expand Up @@ -157,44 +80,21 @@ async fn get_jwt(url: String, password: String) -> anyhow::Result<String> {
async fn login(password: String) -> anyhow::Result<()> {
// 1) ask web server for JWT
let res = get_jwt(DEFAULT_AUTH_URL.to_string(), password).await?;

// 2) if successful store the JWT for later use
if let Some(path) = jwt_file() {
if let Some(dir) = path.parent() {
fs::create_dir_all(dir)?;
} else {
return Err(anyhow::anyhow!("Cannot store the authentication token"));
}

fs::write(path.as_path(), res)?;
set_file_permissions(path.as_path())?;
}

Ok(())
let token = AuthToken::new(&res);
Ok(token.write_user_token()?)
}

/// Releases JWT
fn logout() -> anyhow::Result<()> {
let path = jwt_file();

if !&path.clone().is_some_and(|p| p.exists()) {
// mask if the file with the JWT doesn't exist (most probably no login before logout)
return Ok(());
}

// panicking is right thing to do if expect fails, becase it was already checked twice that
// the path exists
let file = path.expect("Cannot locate stored JWT");

Ok(fs::remove_file(file)?)
Ok(AuthToken::remove_user_token()?)
}

/// Shows stored JWT on stdout
fn show() -> anyhow::Result<()> {
// we do not care if jwt() fails or not. If there is something to print, show it otherwise
// stay silent
if let Ok(token) = jwt() {
println!("{}", token);
if let Some(token) = AuthToken::find() {
println!("{}", token.as_str());
}

Ok(())
Expand Down
10 changes: 3 additions & 7 deletions rust/agama-cli/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::{
auth,
error::CliError,
printers::{print, Format},
};
use agama_lib::{
auth::AuthToken,
connection,
install_settings::{InstallSettings, Scope},
Store as SettingsStore,
Expand Down Expand Up @@ -53,17 +53,13 @@ pub enum ConfigAction {
Load(String),
}

fn token() -> Option<String> {
auth::jwt().or_else(|_| auth::agama_token()).ok()
}

pub async fn run(subcommand: ConfigCommands, format: Format) -> anyhow::Result<()> {
let Some(token) = token() else {
let Some(token) = AuthToken::find() else {
println!("You need to login for generating a valid token");
return Ok(());
};

let client = agama_lib::http_client(token)?;
let client = agama_lib::http_client(token.as_str())?;
let store = SettingsStore::new(connection().await?, client).await?;

let command = parse_config_command(subcommand)?;
Expand Down
3 changes: 3 additions & 0 deletions rust/agama-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ utoipa = "4.2.0"
zbus = { version = "3", default-features = false, features = ["tokio"] }
# Needed to define curl error in profile errors
curl = { version = "0.4.44", features = ["protocol-ftp"] }
jsonwebtoken = "9.3.0"
chrono = { version = "0.4.38", default-features = false, features = ["now", "std", "alloc", "clock"] }
home = "0.5.9"
Loading

0 comments on commit 8bebd79

Please sign in to comment.