Skip to content

Commit 2085652

Browse files
momosonMorian Sonnet
andauthored
New feature: Allow authentication by personal tokens (#53)
* Correct usage of cargo-get in .gitlab-ci snippet * Add note reg. incompatibility of cargo and thrussh * README: Add .ssh/config for personal token usage * README: Newer cargo versions do not allow URL-embedded passwords * Refactor: Add function to build client with token * Allow authentication with personal token * Keep personal token in User * Optionally use personal token for reading packages * Optionally use personal token for DL urls * README: Add usage of personal token authentication * Consider only packages with type generic --------- Co-authored-by: Morian Sonnet <morian.sonnet@isea.rwth-aachen.de>
1 parent 45a2cb5 commit 2085652

File tree

4 files changed

+97
-35
lines changed

4 files changed

+97
-35
lines changed

README.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,28 @@ Say goodbye to your Git dependencies, `gitlab-cargo-shim` is a stateless SSH ser
66

77
Access controls work like they do in GitLab, builds are scoped to users - if they don't have permission to the dependency they can't build it, it's that simple.
88

9-
Users are identified by their SSH keys from GitLab when connecting to the server and an [impersonation token][imp-token] will be generated for that run in order to pull available versions. Builds will insert their token as a username to the SSH server and the shim will use that to call the GitLab API.
9+
Users are either identified by their SSH keys from GitLab when connecting to the server or by an Gitlab personal-token. If no token is given, an [impersonation token][imp-token] will be generated for that run in order to pull available versions. Doing so requires ad admin personal token.
1010

11-
To publish run `cargo package` and push the resulting `.crate` file to the GitLab package repository with a semver-compatible version string, to consume the package configure your `.cargo/config.toml` and `Cargo.toml` accordingly.
11+
To publish run `cargo package` and push the resulting `.crate` file to the GitLab package repository with a semver-compatible version string, to consume the package configure your `.cargo/config.toml`, `Cargo.toml` and, optionally, `.ssh/config` accordingly.
12+
13+
At time of writing, `libssh2`, which `cargo` implicitly uses for communicating with the registry by SSH, is incompatible with rust's `thrussh`, due to non-overlapping ciphers. Hence, activating `net.git-fetch-with-cli` is necessary.
1214

1315
```toml
1416
# .cargo/config.toml
1517
[registries]
16-
my-gitlab-project = { index = "ssh://gitlab-cargo-shim.local/my-gitlab-group/my-gitlab-project" }
18+
my-gitlab-project = { index = "ssh://gitlab-cargo-shim.local/my-gitlab-group/my-gitlab-project/" }
19+
[net]
20+
git-fetch-with-cli = true
1721

1822
# Cargo.toml
1923
[dependencies]
2024
my-crate = { version = "0.1", registry = "my-gitlab-project" }
2125
```
26+
```ssh-config
27+
# .ssh/config (only if authentication by personal token is requires)
28+
Host gitlab-cargo-shim.local
29+
User personal-token:<your-personal-token>
30+
```
2231

2332
In your CI build, setup a `before_script` step to replace the connection string with one containing the CI token:
2433

@@ -34,13 +43,13 @@ To release your package from CI, add a new pipeline step:
3443
3544
```yaml
3645
release-crate:
37-
image: rust:1.62
46+
image: rust:latest
3847
stage: deploy
3948
only: # release when a tag is pushed
4049
- tags
4150
before_script:
4251
- cargo install cargo-get
43-
- export CRATE_NAME=$(cargo get --name) CRATE_VERSION=$(cargo get version)
52+
- export CRATE_NAME=$(cargo-get package.name) CRATE_VERSION=$(cargo-get package.version)
4453
- export CRATE_FILE=${CRATE_NAME}-${CRATE_VERSION}.crate
4554
script:
4655
- cargo package
@@ -54,4 +63,4 @@ It's that easy. Go forth and enjoy your newfound quality of life improvements, R
5463
[gitlab-package-registry]: https://docs.gitlab.com/ee/user/packages/package_registry/index.html
5564
[imp-token]: https://docs.gitlab.com/ee/api/index.html#impersonation-tokens
5665
[envvar]: https://doc.rust-lang.org/cargo/reference/registries.html#using-an-alternate-registry
57-
[example-configuration]: https://github.com/w4/gitlab-cargo-shim/blob/main/config.toml
66+
[example-configuration]: https://github.com/w4/gitlab-cargo-shim/blob/main/config.toml

src/main.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ impl<U: UserProvider + PackageProvider + Send + Sync + 'static> Handler<U> {
247247

248248
// fetch metadata from the provider
249249
let metadata = Arc::clone(&self.gitlab)
250-
.fetch_metadata_for_release(path, crate_version)
250+
.fetch_metadata_for_release(path, crate_version, self.user()?)
251251
.await?;
252252

253253
// transform the `cargo metadata` output to the cargo index
@@ -291,7 +291,10 @@ impl<U: UserProvider + PackageProvider + Send + Sync + 'static> Handler<U> {
291291

292292
// fetch the impersonation token for the user we'll embed
293293
// the `dl` string.
294-
let token = self.gitlab.fetch_token_for_user(self.user()?).await?;
294+
let token = match &self.user()?.token {
295+
None => self.gitlab.fetch_token_for_user(self.user()?).await?,
296+
Some(token) => token.clone(),
297+
};
295298

296299
// generate the config for the user, containing the download
297300
// url template from gitlab and the impersonation token embedded
@@ -408,7 +411,7 @@ impl<U: UserProvider + PackageProvider + Send + Sync + 'static> thrussh::server:
408411
info!(
409412
"Successfully authenticated for GitLab user `{}` by {}",
410413
&user.username,
411-
if by_ssh_key { "SSH Key" } else { "Build Token" },
414+
if by_ssh_key { "SSH Key" } else { "Build or Personal Token" },
412415
);
413416
self.user = Some(Arc::new(user));
414417
self.finished_auth(Auth::Accept).await

src/providers/gitlab.rs

Lines changed: 73 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use std::{borrow::Cow, sync::Arc};
1111
use time::{Duration, OffsetDateTime};
1212
use tracing::{info_span, instrument, Instrument};
1313
use url::Url;
14+
use std::str::FromStr;
1415

1516
pub struct Gitlab {
1617
client: reqwest::Client,
@@ -46,6 +47,19 @@ impl Gitlab {
4647
ssl_cert,
4748
})
4849
}
50+
51+
pub fn build_client_with_token(&self, token_field: &str, token: &str) -> anyhow::Result<reqwest::Client> {
52+
let mut headers = header::HeaderMap::new();
53+
headers.insert(
54+
header::HeaderName::from_str(token_field)?,
55+
header::HeaderValue::from_str(token)?,
56+
);
57+
let mut client_builder = reqwest::ClientBuilder::new().default_headers(headers);
58+
if let Some(cert) = &self.ssl_cert {
59+
client_builder = client_builder.add_root_certificate(cert.clone());
60+
}
61+
Ok(client_builder.build()?)
62+
}
4963
}
5064

5165
#[async_trait]
@@ -60,29 +74,43 @@ impl super::UserProvider for Gitlab {
6074
return Ok(None);
6175
};
6276

63-
if username == "gitlab-ci-token" {
77+
if username == "gitlab-ci-token" || username == "personal-token" {
6478
// we're purposely not using `self.client` here as we don't
6579
// want to use our admin token for this request but still want to use any ssl cert provided.
66-
let mut client_builder = reqwest::Client::builder();
67-
if let Some(cert) = &self.ssl_cert {
68-
client_builder = client_builder.add_root_certificate(cert.clone());
80+
let client = self.build_client_with_token(if username == "gitlab-ci-token" { "JOB-TOKEN" } else { "PRIVATE-TOKEN" }, password);
81+
if username == "gitlab-ci-token" {
82+
let res: GitlabJobResponse = handle_error(
83+
client?
84+
.get(self.base_url.join("job/")?)
85+
.send()
86+
.await?,
87+
)
88+
.await?
89+
.json()
90+
.await?;
91+
92+
Ok(Some(User {
93+
id: res.user.id,
94+
username: res.user.username,
95+
..Default::default()
96+
}))
97+
} else {
98+
let res: GitlabUserResponse = handle_error(
99+
client?
100+
.get(self.base_url.join("user/")?)
101+
.send()
102+
.await?,
103+
)
104+
.await?
105+
.json()
106+
.await?;
107+
108+
Ok(Some(User {
109+
id: res.id,
110+
username: res.username,
111+
token: Some(password.to_string()),
112+
}))
69113
}
70-
let client = client_builder.build();
71-
let res: GitlabJobResponse = handle_error(
72-
client?
73-
.get(self.base_url.join("job/")?)
74-
.header("JOB-TOKEN", password)
75-
.send()
76-
.await?,
77-
)
78-
.await?
79-
.json()
80-
.await?;
81-
82-
Ok(Some(User {
83-
id: res.user.id,
84-
username: res.user.username,
85-
}))
86114
} else {
87115
Ok(None)
88116
}
@@ -101,6 +129,7 @@ impl super::UserProvider for Gitlab {
101129
Ok(res.user.map(|u| User {
102130
id: u.id,
103131
username: u.username,
132+
..Default::default()
104133
}))
105134
}
106135

@@ -149,15 +178,23 @@ impl super::PackageProvider for Gitlab {
149178
query.append_pair("per_page", itoa::Buffer::new().format(100u16));
150179
query.append_pair("pagination", "keyset");
151180
query.append_pair("sort", "asc");
152-
query.append_pair("sudo", itoa::Buffer::new().format(do_as.id));
181+
if do_as.token.is_none() {
182+
query.append_pair("sudo", itoa::Buffer::new().format(do_as.id));
183+
}
153184
}
154185
uri
155186
});
156187

157188
let futures = FuturesUnordered::new();
158189

190+
let client = match &do_as.token {
191+
None => self.client.clone(),
192+
Some(token) => self.build_client_with_token("PRIVATE-TOKEN", token)?
193+
};
194+
let client = Arc::new(client);
195+
159196
while let Some(uri) = next_uri.take() {
160-
let res = handle_error(self.client.get(uri).send().await?).await?;
197+
let res = handle_error(client.get(uri).send().await?).await?;
161198

162199
if let Some(link_header) = res.headers().get(header::LINK) {
163200
let mut link_header = parse_link_header::parse_with_rel(link_header.to_str()?)?;
@@ -167,10 +204,15 @@ impl super::PackageProvider for Gitlab {
167204
}
168205
}
169206

170-
let res: Vec<GitlabPackageResponse> = res.json().await?;
207+
let res: Vec<GitlabPackageResponse> = res.json::<Vec<GitlabPackageResponse>>()
208+
.await?
209+
.into_iter()
210+
.filter(|release| release.package_type == "generic")
211+
.collect();
171212

172213
for release in res {
173214
let this = Arc::clone(&self);
215+
let client = Arc::clone(&client);
174216

175217
futures.push(tokio::spawn(
176218
async move {
@@ -189,7 +231,7 @@ impl super::PackageProvider for Gitlab {
189231
});
190232

191233
let package_files: Vec<GitlabPackageFilesResponse> = handle_error(
192-
this.client
234+
client
193235
.get(format!(
194236
"{}/projects/{}/packages/{}/package_files",
195237
this.base_url,
@@ -239,10 +281,15 @@ impl super::PackageProvider for Gitlab {
239281
&self,
240282
path: &Self::CratePath,
241283
version: &str,
284+
do_as: &User,
242285
) -> anyhow::Result<cargo_metadata::Metadata> {
243286
let uri = self.base_url.join(&path.metadata_uri(version))?;
287+
let client = match &do_as.token {
288+
None => self.client.clone(),
289+
Some(token) => self.build_client_with_token("PRIVATE-TOKEN", token)?
290+
};
244291

245-
Ok(handle_error(self.client.get(uri).send().await?)
292+
Ok(handle_error(client.get(uri).send().await?)
246293
.await?
247294
.json()
248295
.await?)
@@ -315,6 +362,7 @@ pub struct GitlabPackageResponse {
315362
pub id: u64,
316363
pub name: String,
317364
pub version: String,
365+
pub package_type: String,
318366
#[serde(rename = "_links")]
319367
pub links: GitlabPackageLinksResponse,
320368
}

src/providers/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,17 @@ pub trait PackageProvider {
3131
&self,
3232
path: &Self::CratePath,
3333
version: &str,
34+
do_as: &User,
3435
) -> anyhow::Result<cargo_metadata::Metadata>;
3536

3637
fn cargo_dl_uri(&self, project: &str, token: &str) -> anyhow::Result<String>;
3738
}
3839

39-
#[derive(Debug, Clone)]
40+
#[derive(Debug, Clone, Default)]
4041
pub struct User {
4142
pub id: u64,
4243
pub username: String,
44+
pub token: Option<String>,
4345
}
4446

4547
pub type ReleaseName = Arc<str>;

0 commit comments

Comments
 (0)