Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/Skidbladnir

src/federation/aws.rs

//! **AWS CodeArtifact** harbour.
//!
//! CodeArtifact exposes each repository through a package-manager-compatible HTTPS
//! endpoint:
//! `https://{domain}-{owner}.d.codeartifact.{region}.amazonaws.com/{format}/{repo}/…`
//! (the value `aws codeartifact get-repository-endpoint` returns). Those endpoints
//! authenticate with a short-lived **bearer authorization token** — the exact
//! token `aws codeartifact get-authorization-token` mints and that npm/pip/cargo
//! all consume. We speak the endpoint directly with that bearer token via the
//! shared sync client.
//!
//! We deliberately do NOT vendor `aws-sdk-codeartifact` + `aws-config`: their
//! default TLS provider is `aws-lc-rs`, which pulls a **C/cmake/nasm** build
//! toolchain and so violates the nordisk pure-Rust charter (and would risk the
//! green build). The one thing the SDK is genuinely needed for — SigV4-signing the
//! `GetAuthorizationToken` STS call — is done out-of-band by the operator's `aws`
//! CLI (or their own SDK) and the resulting token is provisioned via config/env,
//! exactly as every AWS package-manager integration does. No AWS crypto is
//! hand-rolled here; the harbour is a faithful CodeArtifact HTTP client.

use anyhow::Result;

use super::{ArtifactCoord, ArtifactMeta, RegistryFormat, UpstreamAuth, UpstreamRegistry, verify_sha256};
#[cfg(feature = "federation")]
use super::{get_bytes, head_meta};

/// The CodeArtifact URL path segment for a repository format.
fn format_segment(fmt: RegistryFormat) -> &'static str {
    match fmt {
        RegistryFormat::Npm => "npm",
        RegistryFormat::Maven => "maven",
        RegistryFormat::Pip => "pypi",
        RegistryFormat::Nuget => "nuget",
        RegistryFormat::Rust => "cargo",
        RegistryFormat::Gem => "ruby",
        // AWS CodeArtifact has no OCI repository format (OCI images are served by the
        // OciDistributionRegistry harbour, not CodeArtifact) → the generic segment.
        RegistryFormat::Oci => "generic",
        RegistryFormat::Raw => "generic",
    }
}

/// A read-only harbour proxying one AWS CodeArtifact repository.
pub struct AwsCodeArtifactRegistry {
    name: String,
    /// The repository endpoint base, e.g.
    /// `https://my-domain-111122223333.d.codeartifact.us-west-2.amazonaws.com/npm/my-repo`
    /// (no trailing slash).
    endpoint: String,
    auth: UpstreamAuth,
    format: RegistryFormat,
}

impl AwsCodeArtifactRegistry {
    /// Build from a full repository endpoint (what `get-repository-endpoint`
    /// returns) plus the bearer authorization token.
    pub fn new(
        name: impl Into<String>,
        endpoint: impl Into<String>,
        auth_token: impl Into<String>,
        format: RegistryFormat,
    ) -> Self {
        Self {
            name: name.into(),
            endpoint: endpoint.into().trim_end_matches('/').to_string(),
            auth: UpstreamAuth::Bearer {
                token: auth_token.into(),
            },
            format,
        }
    }

    /// Build the endpoint from its parts (domain, 12-digit account owner, region,
    /// repository) — the canonical CodeArtifact host shape.
    pub fn from_parts(
        name: impl Into<String>,
        domain: &str,
        domain_owner: &str,
        region: &str,
        repository: &str,
        format: RegistryFormat,
        auth_token: impl Into<String>,
    ) -> Self {
        let endpoint = format!(
            "https://{domain}-{domain_owner}.d.codeartifact.{region}.amazonaws.com/{}/{repository}",
            format_segment(format)
        );
        Self::new(name, endpoint, auth_token, format)
    }

    /// Build from parts against an arbitrary host (tests / VPC endpoint). `host`
    /// is the scheme+host base, e.g. `http://127.0.0.1:1234`.
    pub fn with_host(
        name: impl Into<String>,
        host: &str,
        repository: &str,
        format: RegistryFormat,
        auth_token: impl Into<String>,
    ) -> Self {
        let endpoint = format!(
            "{}/{}/{repository}",
            host.trim_end_matches('/'),
            format_segment(format)
        );
        Self::new(name, endpoint, auth_token, format)
    }

    /// Full content URL for one artifact, per the format's CodeArtifact asset path.
    pub fn content_url(&self, id: &ArtifactCoord) -> String {
        let name = &id.name;
        let ver = &id.version;
        let tail = match self.format {
            RegistryFormat::Npm => format!("{name}/-/{name}-{ver}.tgz"),
            RegistryFormat::Maven => {
                let g = id.namespace.as_deref().unwrap_or("").replace('.', "/");
                format!("{g}/{name}/{ver}/{name}-{ver}.jar")
            }
            RegistryFormat::Pip => format!("simple/{name}/{name}-{ver}.tar.gz"),
            RegistryFormat::Nuget => {
                let n = name.to_lowercase();
                format!("v3/content/{n}/{ver}/{n}.{ver}.nupkg")
            }
            RegistryFormat::Rust => format!("api/v1/crates/{name}/{ver}/download"),
            RegistryFormat::Gem => format!("gems/{name}-{ver}.gem"),
            // OCI images are served by the OciDistributionRegistry harbour, not
            // CodeArtifact → the opaque fallback path.
            RegistryFormat::Oci => format!("{name}/{ver}"),
            RegistryFormat::Raw => format!("{name}/{ver}"),
        };
        format!("{}/{}", self.endpoint, tail)
    }
}

#[cfg(feature = "federation")]
impl UpstreamRegistry for AwsCodeArtifactRegistry {
    fn name(&self) -> &str {
        &self.name
    }
    fn base_url(&self) -> &str {
        &self.endpoint
    }
    fn auth(&self) -> &UpstreamAuth {
        &self.auth
    }
    fn format(&self) -> RegistryFormat {
        self.format
    }

    fn fetch(&self, id: &ArtifactCoord) -> Result<Option<Vec<u8>>> {
        let (bytes, sha256) = match get_bytes(&self.endpoint, &self.content_url(id), &self.auth)? {
            Some(b) => b,
            None => return Ok(None),
        };
        if let Some(expected) = sha256 {
            verify_sha256(&bytes, &expected)?;
        }
        Ok(Some(bytes))
    }

    fn metadata(&self, id: &ArtifactCoord) -> Result<Option<ArtifactMeta>> {
        head_meta(&self.endpoint, &self.content_url(id), &self.auth)
    }
}

#[cfg(test)]
mod tests {
    use super::super::testutil::MockServer;
    use super::*;

    fn id(name: &str, version: &str) -> ArtifactCoord {
        ArtifactCoord::new(name, version)
    }

    #[test]
    fn endpoint_and_asset_url_for_npm() {
        let up = AwsCodeArtifactRegistry::from_parts(
            "ca",
            "my-domain",
            "111122223333",
            "us-west-2",
            "my-repo",
            RegistryFormat::Npm,
            "AUTHTOK",
        );
        assert_eq!(
            up.content_url(&id("left-pad", "1.3.0")),
            "https://my-domain-111122223333.d.codeartifact.us-west-2.amazonaws.com/npm/my-repo/left-pad/-/left-pad-1.3.0.tgz"
        );
    }

    #[test]
    fn cargo_asset_url_uses_codeartifact_download_path() {
        let up = AwsCodeArtifactRegistry::from_parts(
            "ca",
            "d",
            "111122223333",
            "eu-north-1",
            "crates",
            RegistryFormat::Rust,
            "T",
        );
        assert!(
            up.content_url(&id("serde", "1.0.0"))
                .ends_with("/cargo/crates/api/v1/crates/serde/1.0.0/download")
        );
    }

    #[test]
    fn bearer_token_attached_and_fetch_against_mock() {
        let server = MockServer::start(|method, path| {
            if method == "GET" && path.ends_with("/left-pad/-/left-pad-1.3.0.tgz") {
                (200, b"tgz-bytes".to_vec())
            } else {
                (404, b"no".to_vec())
            }
        });
        let up = AwsCodeArtifactRegistry::with_host(
            "ca",
            &server.base_url,
            "my-repo",
            RegistryFormat::Npm,
            "AUTHTOK",
        );
        let got = up.fetch(&id("left-pad", "1.3.0")).unwrap();
        assert_eq!(got.as_deref(), Some(b"tgz-bytes".as_ref()));
        assert_eq!(
            server.requests()[0].header("authorization"),
            Some("Bearer AUTHTOK")
        );
        crate::functional_status(
            "skidbladnir/federation-aws",
            "codeartifact_bearer_fetch",
            got.as_deref() == Some(b"tgz-bytes".as_ref())
                && server.requests()[0].header("authorization") == Some("Bearer AUTHTOK"),
            "CodeArtifact fetch with bearer authorization token",
        );
    }

    #[test]
    fn missing_is_clean_none() {
        let server = MockServer::start(|_m, _p| (404, b"no".to_vec()));
        let up = AwsCodeArtifactRegistry::with_host(
            "ca",
            &server.base_url,
            "my-repo",
            RegistryFormat::Npm,
            "T",
        );
        assert!(up.fetch(&id("ghost", "0.0.0")).unwrap().is_none());
    }
}