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
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ clap = { version = "4.4.18", features = ["derive"] }
configparser = { version = "3.1.0", features = ["indexmap"] }
const_format = "0.2.33"
derive_more = "0.99.19"
docker_credential = { version = "1.0.1", optional = true }
duct = "0.13.7"
enumflags2 = { version = "0.7", features = ["serde"] }
env_logger = "0.11.5"
Expand Down Expand Up @@ -84,6 +85,7 @@ sha2 = "0.10.8"
dangerous-options = [
"trident_api/dangerous-options",
"setsail/dangerous-options",
"docker_credential",
]
sysupdate = ["trident_api/sysupdate"]
functional-test = [
Expand Down
3 changes: 3 additions & 0 deletions selinux-policy-trident/trident.te
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ require {
type cloud_init_exec_t;
type cloud_init_state_t;
type cloud_init_t;
type container_conf_home_t;
type container_unit_t;
type container_var_lib_t;
type crack_db_t;
Expand Down Expand Up @@ -299,6 +300,8 @@ allow trident_t cloud_init_exec_t:file getattr;
allow trident_t cloud_init_state_t:dir { list_dir_perms relabelto };
allow trident_t cloud_init_state_t:lnk_file read_lnk_file_perms;
allow trident_t cloud_init_state_t:file getattr;
allow trident_t container_conf_home_t:dir search; # Allow access to ~/.docker directory
allow trident_t container_conf_home_t:file { open read }; # Allow access to ~/.docker/config.json
allow trident_t container_unit_t:file getattr;
allow trident_t container_var_lib_t:file getattr;
allow trident_t crack_db_t:dir { getattr open search read };
Expand Down
74 changes: 61 additions & 13 deletions src/osimage/cosi/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@ use std::{
time::{Duration, Instant},
};

#[cfg(feature = "dangerous-options")]
use std::{env, io::BufReader};

use anyhow::{bail, ensure, Context, Error};
use log::{debug, trace, warn};
use oci_client::{secrets::RegistryAuth, Client as OciClient, Reference};
use reqwest::blocking::{Client, Response};
use tokio::runtime::Runtime;
use url::Url;

#[cfg(feature = "dangerous-options")]
use docker_credential::{self, DockerCredential};

pub(super) trait ReadSeek: Read + Seek {}

impl ReadSeek for HttpFile {}
Expand All @@ -23,6 +29,9 @@ impl ReadSeek for File {}
#[cfg(test)]
impl ReadSeek for Cursor<Vec<u8>> {}

#[cfg(feature = "dangerous-options")]
const DOCKER_CONFIG_FILE_PATH: &str = ".docker/config.json";

/// An abstraction over a COSI file reader that can be either a local file or an
/// HTTP request.
///
Expand Down Expand Up @@ -140,9 +149,10 @@ impl HttpFile {
})?)
.with_context(|| format!("Failed to parse URL '{url}'"))?;

let oci_client = OciClient::default();
let rt = Runtime::new().context("Failed to create Tokio runtime")?;
let token = Self::retrieve_access_token(&img_ref, &rt)?;
let digest = Self::retrieve_artifact_digest(&img_ref, &rt)?;
let token = Self::retrieve_access_token(&img_ref, &rt, &oci_client)?;
let digest = Self::retrieve_artifact_digest(&img_ref, &rt, &oci_client)?;
trace!("Retrieved artifact digest: {digest}");

// Create HTTP URL
Expand Down Expand Up @@ -239,18 +249,18 @@ impl HttpFile {

/// Retrieve bearer token to access container registry. Even registries allowing anonymous
/// access may require a token.
fn retrieve_access_token(img_ref: &Reference, runtime: &Runtime) -> Result<String, Error> {
fn retrieve_access_token(
img_ref: &Reference,
runtime: &Runtime,
client: &OciClient,
) -> Result<String, Error> {
trace!(
"Retrieving access token for OCI registry '{}'",
img_ref.registry()
);
let client = OciClient::default();
let auth = Self::get_auth(img_ref);
runtime
.block_on(client.auth(
img_ref,
&RegistryAuth::Anonymous,
oci_client::RegistryOperation::Pull,
))
.block_on(client.auth(img_ref, &auth, oci_client::RegistryOperation::Pull))
.with_context(|| {
format!(
"Registry '{}' is not accessible or does not exist",
Expand All @@ -260,16 +270,52 @@ impl HttpFile {
.context("Failed to retrieve authorization token")
}

/// Get authentication credentials for accessing registry. Unless "dangerous-options" flag is
/// enabled, will default to anonymous access.
fn get_auth(_img_ref: &Reference) -> RegistryAuth {
#[cfg(feature = "dangerous-options")]
if let Ok(docker_config) = File::open(
env::home_dir()
.unwrap_or_default()
.join(DOCKER_CONFIG_FILE_PATH),
) {
let registry = _img_ref
.resolve_registry()
.strip_suffix('/')
.unwrap_or_else(|| _img_ref.resolve_registry());
match docker_credential::get_credential_from_reader(
BufReader::new(docker_config),
registry,
) {
Ok(DockerCredential::UsernamePassword(username, password)) => {
debug!("Found username and password docker credential");
return RegistryAuth::Basic(username, password);
}
Ok(DockerCredential::IdentityToken(_)) => {
debug!("Found identity token docker credential")
}
Err(_) => debug!("Failed to find docker credentials"),
}
};

debug!("Proceeding with anonymous access");
RegistryAuth::Anonymous
}

/// Retrieve artifact digest, which is necessary to send HTTP request to container registry.
fn retrieve_artifact_digest(img_ref: &Reference, runtime: &Runtime) -> Result<String, Error> {
fn retrieve_artifact_digest(
img_ref: &Reference,
runtime: &Runtime,
client: &OciClient,
) -> Result<String, Error> {
trace!("Retrieving artifact digest");
Ok(match img_ref.digest() {
Some(digest) => digest.to_string(),
None => {
let tag = img_ref.tag().with_context(|| {
format!("Failed to retrieve tag from OCI URL '{}'", img_ref.whole())
})?;
// Attempt to retrieve digest from manifest
let client = OciClient::default();
let manifest = client.pull_image_manifest(img_ref, &RegistryAuth::Anonymous);
let (oci_image_manifest, _) = runtime.block_on(manifest).with_context(||
format!(
Expand Down Expand Up @@ -504,17 +550,19 @@ mod tests {

#[test]
fn test_retrieve_access_token() {
let client = OciClient::default();
let rt = Runtime::new().unwrap();
let url = "oci://docker.io/library/hello-world:latest".to_string();
let img_ref = url
.strip_prefix("oci://")
.and_then(|url| url.parse::<Reference>().ok())
.unwrap();
HttpFile::retrieve_access_token(&img_ref, &rt).unwrap();
HttpFile::retrieve_access_token(&img_ref, &rt, &client).unwrap();
}

#[test]
fn test_retrieve_artifact_digest() {
let client = OciClient::default();
let rt = Runtime::new().unwrap();
// TODO(12732): Fix this test to use test COSI file instead of hello-world image
let url = "oci://docker.io/library/hello-world@sha256:940c619fbd418f9b2b1b63e25d8861f9cc1b46e3fc8b018ccfe8b78f19b8cc4f".to_string();
Expand All @@ -523,7 +571,7 @@ mod tests {
.and_then(|url| url.parse::<Reference>().ok())
.unwrap();
assert_eq!(
HttpFile::retrieve_artifact_digest(&img_ref, &rt).unwrap(),
HttpFile::retrieve_artifact_digest(&img_ref, &rt, &client).unwrap(),
"sha256:940c619fbd418f9b2b1b63e25d8861f9cc1b46e3fc8b018ccfe8b78f19b8cc4f"
);
}
Expand Down