Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(rust): extract token handling to a agama_lib::auth #1287

Merged
merged 1 commit into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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