Skip to content
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
108 changes: 26 additions & 82 deletions rust/agama-cli/src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,37 @@
use clap::{arg, Args, Subcommand};
use clap::Subcommand;

use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
use std::fs;
use std::fs::File;
use std::io;
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 {
/// Login with defined server. Result is JWT stored locally and made available to
/// further use. Password can be provided by commandline option, from a file or it fallbacks
/// into an interactive prompt.
Login(LoginArgs),
/// Release currently stored JWT
/// Authenticate with Agama's server and store the credentials
///
/// It reads the password from the standard input. If it is not available,
/// it asks the user.
Login,
/// Deauthenticate by removing the credentials
Logout,
/// Prints currently stored JWT to stdout
/// Prints currently stored credentials to the standard output
Show,
}

/// Main entry point called from agama CLI main loop
pub async fn run(subcommand: AuthCommands) -> anyhow::Result<()> {
match subcommand {
AuthCommands::Login(options) => login(LoginArgs::proceed(options).password()?).await,
AuthCommands::Login => login(read_password()?).await,
AuthCommands::Logout => logout(),
AuthCommands::Show => show(),
}
Expand Down Expand Up @@ -56,65 +59,20 @@ pub fn jwt() -> anyhow::Result<String> {
Err(anyhow::anyhow!("Authentication token not available"))
}

/// Stores user provided configuration for login command
#[derive(Args, Debug)]
pub struct LoginArgs {
#[arg(long, short = 'p')]
password: Option<String>,
#[arg(long, short = 'f')]
file: Option<PathBuf>,
}

impl LoginArgs {
/// Transforms user provided options into internal representation
/// See Credentials trait
fn proceed(options: LoginArgs) -> Box<dyn Credentials> {
if let Some(password) = options.password {
Box::new(KnownCredentials { password })
} else if let Some(path) = options.file {
Box::new(FileCredentials { path })
} else {
Box::new(MissingCredentials {})
}
}
}

/// Placeholder for no configuration provided by user
struct MissingCredentials;

/// Stores whatever is needed for reading credentials from a file
struct FileCredentials {
path: PathBuf,
}

/// Stores credentials as provided by the user directly
struct KnownCredentials {
password: String,
}

/// Transforms credentials from user's input into format used internaly
trait Credentials {
fn password(&self) -> io::Result<String>;
}

impl Credentials for KnownCredentials {
fn password(&self) -> io::Result<String> {
Ok(self.password.clone())
}
}

impl Credentials for FileCredentials {
fn password(&self) -> io::Result<String> {
read_line_from_file(self.path.as_path())
}
}

impl Credentials for MissingCredentials {
fn password(&self) -> io::Result<String> {
let password = read_credential("Password".to_string())?;

Ok(password)
}
/// Reads the password
///
/// It reads the password from stdin if available; otherwise, it asks the
/// user.
fn read_password() -> Result<String, CliError> {
let stdin = io::stdin();
let password = if stdin.is_terminal() {
rpassword::prompt_password("Please, introduce the root password: ")?
} else {
let mut buffer = String::new();
stdin.read_line(&mut buffer)?;
buffer
};
Ok(password)
}

/// Path to file where JWT is stored
Expand Down Expand Up @@ -151,20 +109,6 @@ fn read_line_from_file(path: &Path) -> io::Result<String> {
))
}

/// Asks user to provide a line of input. Displays a prompt.
fn read_credential(caption: String) -> io::Result<String> {
let caption = format!("{}: ", caption);
let cred = rpassword::prompt_password(caption.clone()).unwrap();
if cred.is_empty() {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("Failed to read {}", caption),
));
}

Ok(cred)
}

/// 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<()> {
Expand Down
2 changes: 2 additions & 0 deletions rust/agama-cli/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ pub enum CliError {
InstallationError,
#[error("Missing the '=' separator in '{0}'")]
MissingSeparator(String),
#[error("Could not read the password: {0}")]
MissingPassword(#[from] std::io::Error),
}
6 changes: 6 additions & 0 deletions rust/package/agama.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
-------------------------------------------------------------------
Mon May 27 14:11:55 UTC 2024 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com>

- The "agama auth" command reads the password from the standard
input (gh#openSUSE/agama#1265).

-------------------------------------------------------------------
Mon May 27 05:49:46 UTC 2024 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com>

Expand Down