Skip to content

Commit

Permalink
Refactoring BASIC authorization (#143)
Browse files Browse the repository at this point in the history
- `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.
  • Loading branch information
termoshtt authored May 27, 2024
1 parent 562c5ee commit 0b101b9
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 37 deletions.
30 changes: 19 additions & 11 deletions ocipkg-cli/src/bin/ocipkg.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand Down Expand Up @@ -69,9 +68,9 @@ enum Opt {
/// OCI registry to be login
registry: String,
#[clap(short = 'u', long = "username")]
username: String,
username: Option<String>,
#[clap(short = 'p', long = "password")]
password: String,
password: Option<String>,
},

/// Inspect components in OCI archive
Expand Down Expand Up @@ -156,14 +155,23 @@ fn main() -> Result<()> {
password,
} => {
let url = url::Url::parse(&registry)?;
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()?;
}

Expand Down
62 changes: 47 additions & 15 deletions ocipkg/src/distribution/auth.rs
Original file line number Diff line number Diff line change
@@ -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::*};
Expand Down Expand Up @@ -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 });
}
Expand All @@ -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<Option<String>> {
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::<ErrorResponse>()?;
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)
}

Expand All @@ -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(())
}
Expand All @@ -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<PathBuf> {
directories::ProjectDirs::from("", "", "ocipkg")
.and_then(|dirs| Some(dirs.runtime_dir()?.join("auth.json")))
Expand Down Expand Up @@ -162,6 +177,23 @@ pub struct AuthChallenge {
pub scope: String,
}

impl TryFrom<ureq::Error> for AuthChallenge {
type Error = anyhow::Error;
fn try_from(res: ureq::Error) -> Result<Self> {
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::<ErrorResponse>()?;
Err(err.into())
}
}
ureq::Error::Transport(e) => Err(e.into()),
}
}
}

impl AuthChallenge {
pub fn from_header(header: &str) -> Result<Self> {
let err = || anyhow!("Unsupported WWW-Authenticate header: {}", header);
Expand Down
17 changes: 6 additions & 11 deletions ocipkg/src/distribution/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ureq::Response> {
if let Some(token) = &self.token {
return Ok(req
Expand All @@ -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::<ErrorResponse>()?;
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)
}
Expand Down

0 comments on commit 0b101b9

Please sign in to comment.