Skip to content

Commit

Permalink
feat. add login api/config
Browse files Browse the repository at this point in the history
  • Loading branch information
juliusl committed Jun 15, 2023
1 parent 6e48f07 commit ad25f4d
Show file tree
Hide file tree
Showing 18 changed files with 499 additions and 48 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ serde_urlencoded = "0.7.1"
async-trait = "0.1.64"
base64-url = "1.4.13"
toml_edit = { version = "0.19.2", features = ["serde"] }
url = "2.4.0"

[[bin]]
name = "acr-dev"
Expand Down
37 changes: 37 additions & 0 deletions docs/auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Auth

This document has information on configuring authentication w/ the mirror.

## Using file as an access provider

Create a file at `/opt/acr/bin/.world/azurecr.io/token_cache` with the following content:

```json
{
"refresh_token": "<refresh-token>",
"claims": {
"exp": 1686613092
}
}
```

## Using username/password for registry

You can login w/ registry credentials by calling the '/login' api w/ the mirror.

Example curl request,

```sh

read -r -d '' LOGIN <<- EOM
{
"host": "host.registry.io",
"username": "username",
"password": "password"
}
EOM

curl -X PUT 'localhost:8578/login' \
-d $LOGIN
```

8 changes: 6 additions & 2 deletions src/access_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ pub trait AccessProvider {
/// Returns the default access provider,
///
pub fn default_access_provider(
access_token_path: Option<PathBuf>,
token_cache: Option<PathBuf>,
) -> Arc<dyn AccessProvider + Send + Sync + 'static> {
if let Some(aks_config) = AzureAKSConfig::try_load().ok() {
info!("AKS config detected, using AKS as the access provider");
Arc::new(aks_config)
} else if let Some(path) = access_token_path {
} else if let Some(path) = token_cache {
info!(
"File access_token provided, using {:?} as the access provider",
path
Expand Down Expand Up @@ -87,6 +87,10 @@ mod tests {

let test_file_path = PathBuf::from(".test/test_access_provider");

if test_file_path.exists() {
std::fs::remove_file(&test_file_path).unwrap();
}

std::fs::create_dir_all(".test").expect("should be able to create test dir");

std::fs::write(&test_file_path, test_file).expect("should be able to write");
Expand Down
118 changes: 118 additions & 0 deletions src/config/login_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use std::path::PathBuf;

use toml_edit::{Document, Table};

use crate::Error;

/// Struct that reads a more traditional docker login config,
///
/// **Note** Should be placed in `/etc/acr-mirror/login.toml`
///
/// Example:
///
/// ```toml
/// [auth."<host>"]
/// username = <username>
/// password = <password>
///
/// ```
#[derive(Default)]
pub struct LoginConfig {
/// Auth table
///
doc: toml_edit::Document,
/// Root config dir,
///
root: PathBuf,
}

/// Default directory to use for config,
///
const DEFAULT_ROOT_CONFIG_PATH: &'static str = "/etc/acr-mirror/";

/// Config file name,
///
const CONFIG_NAME: &'static str = "login.toml";

impl LoginConfig {
/// Creates a new login config, or loads an existing one
///
pub fn load(root: Option<PathBuf>) -> Result<Self, Error> {
let root = root.unwrap_or(PathBuf::from(DEFAULT_ROOT_CONFIG_PATH));
let mut config = Self { doc: toml_edit::Document::new(), root };
std::fs::create_dir_all(&config.root)?;

let path = config.root.join(CONFIG_NAME);

if path.exists() {
if let Ok(doc) = std::fs::read_to_string(path)?.parse::<Document>() {
config.doc = doc;
}
}

if !config.doc.get_mut("auth").map(|t| t.is_table()).unwrap_or_default() {
config.doc["auth"] = toml_edit::table();
}

config.doc["auth"].as_table_mut().map(|t| t.set_implicit(true));

Ok(config)
}

/// Adds a new login to config and writes to file,
///
pub fn login(&mut self, host: impl AsRef<str>, username: impl Into<String>, password: impl Into<String>) -> Result<bool, Error> {
let mut login = Table::new();
login.set_implicit(true);
login["username"] = toml_edit::value(username.into());
login["password"] = toml_edit::value(password.into());

let existed = self.doc["auth"].as_table().map(|t| t.contains_table(host.as_ref())).unwrap_or_default();
// This will clear any existing login for this host
self.doc["auth"].as_table_mut().map(|t| t.insert(host.as_ref(), toml_edit::Item::Table(login)));

self.save_to_disk()?;

Ok(existed)
}

/// Authorizes a host,
///
pub fn authorize(&self, host: impl AsRef<str>) -> Option<(&str, &str)> {
self.doc["auth"].as_table().and_then(|t| t.get(host.as_ref()).and_then(|v| v.as_table()).and_then(|t| {
if let (Some(u), Some(p)) = (t["username"].as_str(), t["password"].as_str()) {
Some((u, p))
} else {
None
}
}))
}

/// Saves login to disk,
///
pub fn save_to_disk(&self) -> Result<(), Error> {
let path = self.root.join(CONFIG_NAME);

std::fs::write(&path, format!("{}", self.doc))?;
Ok(())
}
}

#[allow(unused_imports)]
mod tests {
use super::LoginConfig;

#[test]
fn test_login_config() {
let mut config = LoginConfig::load(Some(".test_login".into())).unwrap();

let overwritten = config.login("test.endpoint.io", "username", "password").unwrap();
assert!(!overwritten);

let (u, p) = config.authorize("test.endpoint.io").unwrap();
assert_eq!("username", u);
assert_eq!("password", p);

std::fs::remove_dir_all(".test_login").unwrap();
}
}
5 changes: 4 additions & 1 deletion src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ pub use hosts_config::Host;

mod containerd_config;
pub use containerd_config::ContainerdConfig;
pub use containerd_config::enable_containerd_config;
pub use containerd_config::enable_containerd_config;

mod login_config;
pub use login_config::LoginConfig;
1 change: 1 addition & 0 deletions src/content/contents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use super::{Upstream, Local};
/// System data for content storage,
///
#[derive(SystemData)]
#[allow(dead_code)]
pub struct Contents<'a> {
descriptors: WriteStorage<'a, Descriptor>,
platforms: WriteStorage<'a, Platform>,
Expand Down
4 changes: 4 additions & 0 deletions src/content/manifests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ use crate::{ArtifactManifest, Descriptor, ImageIndex, ImageManifest};
/// Enumeration of possible manifest types,
///
#[derive(Debug, Clone)]

#[allow(dead_code)]
pub enum Manifests {
Image(Descriptor, ImageManifest),
Artifact(Descriptor, ArtifactManifest),
Index(Descriptor, ImageIndex),
}


#[allow(dead_code)]
impl Manifests {
/// Copies manifest to context for later processing,
///
Expand Down
43 changes: 34 additions & 9 deletions src/content/registry.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use std::sync::Arc;

use hyper::{Body, StatusCode, Uri};
use lifec::engine::NodeCommand;
use lifec::prelude::{SpecialAttribute, ThunkContext};
use lifec::state::AttributeIndex;
use lifec_poem::RoutePlugin;
use poem::{Request, Response};
use tracing::{debug, event, Level, error, info};
use tokio::sync::RwLock;
use tracing::{debug, error, event, info, Level};

use crate::config::LoginConfig;
use crate::hosts_config::MirrorHost;

pub mod consts {
Expand Down Expand Up @@ -43,6 +47,7 @@ impl Registry {
namespace: impl Into<String>,
repo: impl Into<String>,
reference: Option<impl Into<String>>,
login_config: Arc<RwLock<LoginConfig>>,
) -> Response
where
P: RoutePlugin + SpecialAttribute,
Expand All @@ -61,10 +66,11 @@ impl Registry {

// Check if the request uri ends with the suffix value of the header, if not then return 503 Service Unavailable
let accept_if_header = request.header(consts::ACCEPT_IF_SUFFIX_HEADER);
if accept_if_header.is_some() && !accept_if_header
.filter(|f| f.len() < 256)
.map(|suffix| namespace.ends_with(suffix))
.unwrap_or_default()
if accept_if_header.is_some()
&& !accept_if_header
.filter(|f| f.len() < 256)
.map(|suffix| namespace.ends_with(suffix))
.unwrap_or_default()
{
debug!("Rejecting host {:?}", namespace);
return Self::soft_fail();
Expand All @@ -85,7 +91,10 @@ impl Registry {
);

if let Err(err) = mirror_hosts_config.install(None::<String>) {
error!("Unable to enable mirror host config for, {}, {:?}", namespace, err);
error!(
"Unable to enable mirror host config for, {}, {:?}",
namespace, err
);
} else {
debug!("Enabled mirror host config for {}", namespace);
}
Expand Down Expand Up @@ -118,8 +127,16 @@ impl Registry {
workspace.tag()
);

let context =
self.prepare_registry_context::<P>(request, namespace, repo, reference, context);
let context = self
.prepare_registry_context::<P>(
request,
namespace,
repo,
reference,
context,
login_config.clone(),
)
.await;

if let Some(yielding) = context.dispatch_node_command(NodeCommand::Spawn(*operation)) {
match yielding.await {
Expand Down Expand Up @@ -173,13 +190,14 @@ impl Registry {

/// Returns a context prepared with registry context,
///
pub fn prepare_registry_context<S>(
pub async fn prepare_registry_context<S>(
&self,
request: &Request,
namespace: impl Into<String>,
repo: impl Into<String>,
reference: Option<impl Into<String>>,
context: &ThunkContext,
login_config: Arc<RwLock<LoginConfig>>,
) -> ThunkContext
where
S: SpecialAttribute,
Expand Down Expand Up @@ -224,6 +242,13 @@ impl Registry {
format!("https://{namespace}/v2/{repo}/{resource}/{reference}"),
);

// If login credentials exist for namespace, then login
if let Some((u, p)) = login_config.read().await.authorize(namespace) {
context
.with_symbol("REGISTRY_USER", u)
.with_symbol("REGISTRY_PASSWORD", p);
}

let headers = request.headers();
for (name, value) in headers
.iter()
Expand Down
7 changes: 7 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,13 @@ impl From<std::time::SystemTimeError> for Error {
}
}

impl From<url::ParseError> for Error {
fn from(value: url::ParseError) -> Self {
error!("Could not parse url, {value}");
Self::data_format()
}
}

impl From<Error> for lifec::error::Error {
fn from(value: Error) -> lifec::error::Error {
match &value.category {
Expand Down
Loading

0 comments on commit ad25f4d

Please sign in to comment.