From 0b101b960634b39d081849e0932813e898d39c34 Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Mon, 27 May 2024 21:48:29 +0900 Subject: [PATCH] Refactoring BASIC authorization (#143) - `Client::add_basic_auth` to add additional auth info for using `ocipkg` as a library. - Add `StoredAuth::add` which append new `username:password` pair, instead `#[deprecate]` `StoredAuth::insert` which takes base64-encoded string of the pair. - Validate the contents of `auth.json` when loading. --- ocipkg-cli/src/bin/ocipkg.rs | 30 +++++++++------ ocipkg/src/distribution/auth.rs | 62 +++++++++++++++++++++++-------- ocipkg/src/distribution/client.rs | 17 +++------ 3 files changed, 72 insertions(+), 37 deletions(-) diff --git a/ocipkg-cli/src/bin/ocipkg.rs b/ocipkg-cli/src/bin/ocipkg.rs index 94c5d14d..73eb400c 100644 --- a/ocipkg-cli/src/bin/ocipkg.rs +++ b/ocipkg-cli/src/bin/ocipkg.rs @@ -1,5 +1,4 @@ -use anyhow::Result; -use base64::{engine::general_purpose::STANDARD, Engine}; +use anyhow::{bail, Context, Result}; use clap::Parser; use ocipkg::image::{Artifact, Image}; use std::path::*; @@ -69,9 +68,9 @@ enum Opt { /// OCI registry to be login registry: String, #[clap(short = 'u', long = "username")] - username: String, + username: Option, #[clap(short = 'p', long = "password")] - password: String, + password: Option, }, /// Inspect components in OCI archive @@ -156,14 +155,23 @@ fn main() -> Result<()> { password, } => { let url = url::Url::parse(®istry)?; - let octet = STANDARD.encode(format!("{}:{}", username, password,)); - let mut new_auth = ocipkg::distribution::StoredAuth::default(); - new_auth.insert(url.domain().unwrap(), octet); - let _token = new_auth.get_token(&url)?; - println!("Login succeed"); - let mut auth = ocipkg::distribution::StoredAuth::load()?; - auth.append(new_auth)?; + match (username, password) { + (Some(username), Some(password)) => { + auth.add( + url.domain().context("URL does not contain domain name")?, + &username, + &password, + ); + } + (None, None) => {} + _ => { + bail!("Both username and password must be set"); + } + } + + let _token = auth.get_token(&url)?; + log::info!("Login succeed"); auth.save()?; } diff --git a/ocipkg/src/distribution/auth.rs b/ocipkg/src/distribution/auth.rs index 8d911fdc..49e6f87d 100644 --- a/ocipkg/src/distribution/auth.rs +++ b/ocipkg/src/distribution/auth.rs @@ -1,4 +1,5 @@ use anyhow::{anyhow, Context, Result}; +use base64::engine::{general_purpose::STANDARD, Engine}; use oci_spec::distribution::ErrorResponse; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fs, io, path::*}; @@ -41,6 +42,12 @@ impl StoredAuth { Ok(auth) } + pub fn add(&mut self, domain: &str, username: &str, password: &str) { + self.auths + .insert(domain.to_string(), Auth::new(username, password)); + } + + #[deprecated(note = "Use `add` instead")] pub fn insert(&mut self, domain: &str, octet: String) { self.auths.insert(domain.to_string(), Auth { auth: octet }); } @@ -49,32 +56,22 @@ impl StoredAuth { let path = auth_path().context("No valid runtime directory")?; let parent = path.parent().unwrap(); if !parent.exists() { + log::info!("Creating directory: {}", parent.display()); fs::create_dir_all(parent)?; } + log::info!("Saving auth info to: {}", path.display()); let f = fs::File::create(&path)?; serde_json::to_writer_pretty(f, self)?; Ok(()) } /// Get token by trying to access API root `/v2/` - /// - /// Returns `None` if no authentication is required. pub fn get_token(&self, url: &url::Url) -> Result> { let test_url = url.join("/v2/").unwrap(); - let www_auth = match ureq::get(test_url.as_str()).call() { + let challenge = match ureq::get(test_url.as_str()).call() { Ok(_) => return Ok(None), - Err(ureq::Error::Status(status, res)) => { - if status == 401 { - res.header("www-authenticate").unwrap().to_string() - } else { - let err = res.into_json::()?; - return Err(err.into()); - } - } - Err(ureq::Error::Transport(e)) => return Err(e.into()), + Err(e) => AuthChallenge::try_from(e)?, }; - - let challenge = AuthChallenge::from_header(&www_auth)?; self.challenge(&challenge).map(Some) } @@ -99,7 +96,9 @@ impl StoredAuth { pub fn append(&mut self, other: Self) -> Result<()> { for (key, value) in other.auths.into_iter() { - self.auths.insert(key, value); + if value.is_valid() { + self.auths.insert(key, value); + } } Ok(()) } @@ -116,9 +115,25 @@ impl StoredAuth { #[derive(Debug, Clone, Serialize, Deserialize)] struct Auth { + // base64 encoded username:password auth: String, } +impl Auth { + fn new(username: &str, password: &str) -> Self { + let auth = format!("{}:{}", username, password); + let auth = STANDARD.encode(auth.as_bytes()); + Self { auth } + } + + fn is_valid(&self) -> bool { + let Ok(decoded) = STANDARD.decode(&self.auth) else { + return false; + }; + decoded.split(|b| *b == b':').count() == 2 + } +} + fn auth_path() -> Option { directories::ProjectDirs::from("", "", "ocipkg") .and_then(|dirs| Some(dirs.runtime_dir()?.join("auth.json"))) @@ -162,6 +177,23 @@ pub struct AuthChallenge { pub scope: String, } +impl TryFrom for AuthChallenge { + type Error = anyhow::Error; + fn try_from(res: ureq::Error) -> Result { + match res { + ureq::Error::Status(status, res) => { + if status == 401 && res.has("www-authenticate") { + Self::from_header(res.header("www-authenticate").unwrap()) + } else { + let err = res.into_json::()?; + Err(err.into()) + } + } + ureq::Error::Transport(e) => Err(e.into()), + } + } +} + impl AuthChallenge { pub fn from_header(header: &str) -> Result { let err = || anyhow!("Unsupported WWW-Authenticate header: {}", header); diff --git a/ocipkg/src/distribution/client.rs b/ocipkg/src/distribution/client.rs index a2b2f267..d9c2b2fc 100644 --- a/ocipkg/src/distribution/client.rs +++ b/ocipkg/src/distribution/client.rs @@ -32,6 +32,10 @@ impl Client { Self::new(image.registry_url()?, image.name.clone()) } + pub fn add_basic_auth(&mut self, domain: &str, username: &str, password: &str) { + self.auth.add(domain, username, password); + } + fn call(&mut self, req: ureq::Request) -> Result { if let Some(token) = &self.token { return Ok(req @@ -41,19 +45,10 @@ impl Client { // Try get token let try_req = req.clone(); - let www_auth = match try_req.call() { + let challenge = match try_req.call() { Ok(res) => return Ok(res), - Err(ureq::Error::Status(status, res)) => { - if status == 401 && res.has("www-authenticate") { - res.header("www-authenticate").unwrap().to_string() - } else { - let err = res.into_json::()?; - return Err(err.into()); - } - } - Err(ureq::Error::Transport(e)) => return Err(e.into()), + Err(e) => AuthChallenge::try_from(e)?, }; - let challenge = AuthChallenge::from_header(&www_auth)?; self.token = Some(self.auth.challenge(&challenge)?); self.call(req) }