🏡 index : ~doyle/gitlab-cargo-shim.git

author Jordan Doyle <jordan@doyle.la> 2022-03-12 17:10:11.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2022-03-12 17:10:11.0 +00:00:00
commit
e57bc3cc9788eec12c300acb7cb86ae158d448cd [patch]
tree
087d9e026868bd4a07ef2249048dcd5f409eb282
parent
8da0ec266cb4b05dd7180c074ff3aa63f7f6e8e6
download
e57bc3cc9788eec12c300acb7cb86ae158d448cd.tar.gz

Ensure group exists before continuing the handshake with Git



Diff

 src/git_command_handlers/fetch.rs |  4 +--
 src/main.rs                       | 30 ++++++++++----
 src/metadata.rs                   |  2 +-
 src/providers/gitlab.rs           | 84 +++++++++++++++++++++++++++++++++-------
 src/providers/mod.rs              | 12 +++++-
 5 files changed, 103 insertions(+), 29 deletions(-)

diff --git a/src/git_command_handlers/fetch.rs b/src/git_command_handlers/fetch.rs
index 0034e9f..d157996 100644
--- a/src/git_command_handlers/fetch.rs
+++ b/src/git_command_handlers/fetch.rs
@@ -32,10 +32,6 @@ pub fn handle<U: UserProvider + PackageProvider + Send + Sync + 'static>(
    // magic header
    handle.write(PktLine::Data(b"packfile\n"))?;

    // send a welcome message
    // handle.write(PktLine::SidebandMsg(b"Hello from gitlab-cargo-shim!\n"))?;
    // handle.flush(session, channel);

    // send the complete packfile
    let packfile = PackFile::new(packfile_entries);
    handle.write(PktLine::SidebandData(packfile))?;
diff --git a/src/main.rs b/src/main.rs
index ade64fc..66472ff 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -27,6 +27,7 @@ use thrussh::{
use thrussh_keys::key::PublicKey;
use tokio_util::{codec::Decoder, codec::Encoder as CodecEncoder};
use tracing::error;
use crate::providers::Group;

const AGENT: &str = concat!(
    "agent=",
@@ -94,7 +95,7 @@ pub struct Handler<U: UserProvider + PackageProvider + Send + Sync + 'static> {
    codec: GitCodec,
    gitlab: Arc<U>,
    user: Option<User>,
    group: Option<String>,
    group: Option<Group>,
    // fetcher_future: Option<JoinHandle<anyhow::Result<Vec<Release>>>>,
    input_bytes: BytesMut,
    output_bytes: BytesMut,
@@ -108,8 +109,8 @@ impl<U: UserProvider + PackageProvider + Send + Sync + 'static> Handler<U> {
        self.user.as_ref().ok_or(anyhow::anyhow!("no user set"))
    }

    fn group(&self) -> anyhow::Result<&str> {
        self.group.as_deref().ok_or(anyhow::anyhow!("no group set"))
    fn group(&self) -> anyhow::Result<&Group> {
        self.group.as_ref().ok_or(anyhow::anyhow!("no group set"))
    }

    fn write(&mut self, packet: PktLine<'_>) -> Result<(), anyhow::Error> {
@@ -125,9 +126,9 @@ impl<U: UserProvider + PackageProvider + Send + Sync + 'static> Handler<U> {

    async fn fetch_releases_by_crate(
        &self,
        group: &str,
    ) -> anyhow::Result<HashMap<(U::CratePath, String), Vec<Release>>> {
        let user = self.user()?;
        let group = self.group()?;

        let mut res = HashMap::new();

@@ -208,7 +209,7 @@ impl<U: UserProvider + PackageProvider + Send + Sync + 'static> Handler<U> {
        packfile.insert(vec![], "config.json".to_string(), config_json)?;

        // fetch the releases for every project within the given group
        let releases_by_crate = self.fetch_releases_by_crate(group).await?;
        let releases_by_crate = self.fetch_releases_by_crate().await?;

        let mut buffer = BytesMut::new();

@@ -414,11 +415,24 @@ impl<'a, U: UserProvider + PackageProvider + Send + Sync + 'static> thrussh::ser
            // parse the requested group from the given path (the argument
            // given to `git-upload-pack`)
            if let Some(group) = args.next().filter(|v| v.as_str() != "/") {
                let user = self.user()?;
                let group = group
                    .trim_start_matches('/')
                    .trim_end_matches('/')
                    .to_string();
                self.group = Some(group);
                    .trim_end_matches('/');

                match self.gitlab.clone().fetch_group(group, user).await {
                    Ok(v) => self.group = Some(v),
                    Err(e) => {
                        session.extended_data(channel, 1, CryptoVec::from_slice(format!(indoc::indoc! {"
                            \r\nGitLab returned an error when attempting to query for group `{}` as `{}`:

                                {}

                            The group might not exist or you may not have permission to view it.\r\n
                        "}, group, user.username, e).as_bytes()));
                        session.close(channel);
                    }
                }
            } else {
                session.extended_data(channel, 1, CryptoVec::from_slice(indoc::indoc! {b"
                    \r\nNo group was given in the path part of the SSH URI. A GitLab group should be defined in your .cargo/config.toml as follows:
diff --git a/src/metadata.rs b/src/metadata.rs
index 2a75669..9b35137 100644
--- a/src/metadata.rs
+++ b/src/metadata.rs
@@ -1,4 +1,4 @@
use cargo_metadata::{Package};
use cargo_metadata::Package;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

diff --git a/src/providers/gitlab.rs b/src/providers/gitlab.rs
index 3d39ab8..0019dbe 100644
--- a/src/providers/gitlab.rs
+++ b/src/providers/gitlab.rs
@@ -1,9 +1,10 @@
use crate::providers::{Release, User};
use crate::providers::{Group, Release, User};
use async_trait::async_trait;
use futures::{stream::FuturesUnordered, StreamExt, TryStreamExt};
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use reqwest::header;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::sync::Arc;

const GITLAB_API_ENDPOINT: &str = "http://127.0.0.1:3000";
@@ -43,14 +44,16 @@ impl super::UserProvider for Gitlab {
        };

        if username == "gitlab-ci-token" {
            let res: GitlabJobResponse = self
                .client
                .get(format!("{}/job", self.base_url))
                .header("JOB-TOKEN", password)
                .send()
                .await?
                .json()
                .await?;
            let res: GitlabJobResponse = handle_error(
                self.client
                    .get(format!("{}/job", self.base_url))
                    .header("JOB-TOKEN", password)
                    .send()
                    .await?,
            )
            .await?
            .json()
            .await?;

            Ok(Some(User {
                id: res.user.id,
@@ -103,15 +106,32 @@ impl super::UserProvider for Gitlab {
impl super::PackageProvider for Gitlab {
    type CratePath = Arc<GitlabCratePath>;

    async fn fetch_group(self: Arc<Self>, group: &str, do_as: &User) -> anyhow::Result<Group> {
        let uri = format!(
            "{}/groups/{}?sudo={}",
            self.base_url,
            utf8_percent_encode(group, NON_ALPHANUMERIC),
            do_as.id
        );

        let req = handle_error(self.client.get(uri).send().await?)
            .await?
            .json::<GitlabGroupResponse>()
            .await?
            .into();

        Ok(req)
    }

    async fn fetch_releases_for_group(
        self: Arc<Self>,
        group: &str,
        group: &Group,
        do_as: &User,
    ) -> anyhow::Result<Vec<(Self::CratePath, Release)>> {
        let mut next_uri = Some(format!(
            "{}/groups/{}/packages?per_page=100&pagination=keyset&sort=asc&sudo={}",
            self.base_url,
            utf8_percent_encode(group, NON_ALPHANUMERIC),
            utf8_percent_encode(&group.name, NON_ALPHANUMERIC),
            do_as.id
        ));

@@ -223,14 +243,50 @@ impl super::PackageProvider for Gitlab {
        Ok(self.client.get(uri).send().await?.json().await?)
    }

    fn cargo_dl_uri(&self, group: &str, token: &str) -> String {
    fn cargo_dl_uri(&self, group: &Group, token: &str) -> String {
        format!(
            "{}/groups/{group}/packages/generic/{{sha256-checksum}}/{{crate}}-{{version}}.crate?private_token={token}",
            self.base_url
            "{}/groups/{}/packages/generic/{{sha256-checksum}}/{{crate}}-{{version}}.crate?private_token={token}",
            self.base_url,
            group.id,
        )
    }
}

async fn handle_error(resp: reqwest::Response) -> Result<reqwest::Response, anyhow::Error> {
    if resp.status().is_success() {
        Ok(resp)
    } else {
        let resp: GitlabErrorResponse = resp.json().await?;
        Err(anyhow::Error::msg(
            resp.message
                .or(resp.error)
                .map(Cow::Owned)
                .unwrap_or_else(|| Cow::Borrowed("unknown error")),
        ))
    }
}

#[derive(Deserialize)]
pub struct GitlabErrorResponse {
    message: Option<String>,
    error: Option<String>,
}

#[derive(Deserialize)]
pub struct GitlabGroupResponse {
    id: u64,
    name: String,
}

impl From<GitlabGroupResponse> for Group {
    fn from(v: GitlabGroupResponse) -> Self {
        Self {
            id: v.id,
            name: v.name,
        }
    }
}

#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct GitlabCratePath {
    project: String,
diff --git a/src/providers/mod.rs b/src/providers/mod.rs
index ffa2bed..4aebf12 100644
--- a/src/providers/mod.rs
+++ b/src/providers/mod.rs
@@ -21,9 +21,11 @@ pub trait PackageProvider {
    /// figure out the path of a package.
    type CratePath: std::fmt::Debug + Send + std::hash::Hash + Clone + Eq + PartialEq + Send + Sync;

    async fn fetch_group(self: Arc<Self>, group: &str, do_as: &User) -> anyhow::Result<Group>;

    async fn fetch_releases_for_group(
        self: Arc<Self>,
        group: &str,
        group: &Group,
        do_as: &User,
    ) -> anyhow::Result<Vec<(Self::CratePath, Release)>>;

@@ -33,7 +35,7 @@ pub trait PackageProvider {
        version: &str,
    ) -> anyhow::Result<cargo_metadata::Metadata>;

    fn cargo_dl_uri(&self, group: &str, token: &str) -> String;
    fn cargo_dl_uri(&self, group: &Group, token: &str) -> String;
}

#[derive(Debug, Clone)]
@@ -42,6 +44,12 @@ pub struct User {
    pub username: String,
}

#[derive(Debug, Clone)]
pub struct Group {
    pub id: u64,
    pub name: String,
}

#[derive(Debug)]
pub struct Release {
    pub name: String,