Skip to content

Commit 300edb5

Browse files
committed
feat(provider): add support for oidc token authentication
1 parent 2359e53 commit 300edb5

File tree

7 files changed

+166
-5
lines changed

7 files changed

+166
-5
lines changed

src/request_client.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ use reqwest_middleware::{ClientBuilder as ClientWithMiddlewareBuilder, ClientWit
44
use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff};
55

66
const UPLOAD_RETRY_COUNT: u32 = 3;
7+
const OIDC_RETRY_COUNT: u32 = 10;
8+
const USER_AGENT: &str = "codspeed-runner";
79

810
lazy_static! {
911
pub static ref REQUEST_CLIENT: ClientWithMiddleware = ClientWithMiddlewareBuilder::new(
1012
ClientBuilder::new()
11-
.user_agent("codspeed-runner")
13+
.user_agent(USER_AGENT)
1214
.build()
1315
.unwrap()
1416
)
@@ -19,7 +21,19 @@ lazy_static! {
1921

2022
// Client without retry middleware for streaming uploads (can't be cloned)
2123
pub static ref STREAMING_CLIENT: reqwest::Client = ClientBuilder::new()
22-
.user_agent("codspeed-runner")
24+
.user_agent(USER_AGENT)
2325
.build()
2426
.unwrap();
27+
28+
// Client with retry middleware for OIDC token requests
29+
pub static ref OIDC_CLIENT: ClientWithMiddleware = ClientWithMiddlewareBuilder::new(
30+
ClientBuilder::new()
31+
.user_agent(USER_AGENT)
32+
.build()
33+
.unwrap()
34+
)
35+
.with(RetryTransientMiddleware::new_with_policy(
36+
ExponentialBackoff::builder().build_with_max_retries(OIDC_RETRY_COUNT)
37+
))
38+
.build();
2539
}

src/run/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,9 @@ pub async fn run(
195195
}
196196
debug!("Using the token from the CodSpeed configuration file");
197197
config.set_token(codspeed_config.auth.token.clone());
198+
} else {
199+
// If relevant, set the OIDC token for authentication
200+
provider.set_oidc_token(&mut config).await;
198201
}
199202

200203
let system_info = SystemInfo::new()?;

src/run/run_environment/buildkite/provider.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::env;
22

3+
use async_trait::async_trait;
34
use simplelog::SharedLogger;
45

56
use crate::prelude::*;
@@ -119,6 +120,7 @@ impl RunEnvironmentDetector for BuildkiteProvider {
119120
}
120121
}
121122

123+
#[async_trait(?Send)]
122124
impl RunEnvironmentProvider for BuildkiteProvider {
123125
fn get_repository_provider(&self) -> RepositoryProvider {
124126
RepositoryProvider::GitHub
@@ -151,6 +153,15 @@ impl RunEnvironmentProvider for BuildkiteProvider {
151153
fn get_run_provider_run_part(&self) -> Option<RunPart> {
152154
None
153155
}
156+
157+
/// For now, we do not support OIDC tokens for Buildkite
158+
///
159+
/// If we want to in the future, we can implement it using the Buildkite Agent CLI.
160+
///
161+
/// Docs:
162+
/// - https://buildkite.com/docs/agent/v3/cli-oidc
163+
/// - https://buildkite.com/docs/pipelines/security/oidc
164+
async fn set_oidc_token(&self, _config: &mut Config) {}
154165
}
155166

156167
#[cfg(test)]

src/run/run_environment/github_actions/provider.rs

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
use async_trait::async_trait;
12
use git2::Repository;
23
use lazy_static::lazy_static;
34
use regex::Regex;
5+
use serde::Deserialize;
46
use serde_json::Value;
57
use simplelog::SharedLogger;
68
use std::collections::BTreeMap;
79
use std::{env, fs};
810

911
use crate::prelude::*;
12+
use crate::request_client::OIDC_CLIENT;
1013
use crate::run::run_environment::{RunEnvironment, RunPart};
1114
use crate::run::{
1215
config::Config,
@@ -30,6 +33,9 @@ pub struct GitHubActionsProvider {
3033
pub gh_data: GhData,
3134
pub event: RunEvent,
3235
pub repository_root_path: String,
36+
37+
/// Indicates whether the head repository is a fork of the base repository.
38+
is_head_repo_fork: bool,
3339
}
3440

3541
impl GitHubActionsProvider {
@@ -42,6 +48,11 @@ impl GitHubActionsProvider {
4248
}
4349
}
4450

51+
#[derive(Deserialize)]
52+
struct OIDCResponse {
53+
value: Option<String>,
54+
}
55+
4556
lazy_static! {
4657
static ref PR_REF_REGEX: Regex = Regex::new(r"^refs/pull/(?P<pr_number>\d+)/merge$").unwrap();
4758
}
@@ -55,7 +66,7 @@ impl TryFrom<&Config> for GitHubActionsProvider {
5566
let (owner, repository) = Self::get_owner_and_repository()?;
5667
let ref_ = get_env_variable("GITHUB_REF")?;
5768
let is_pr = PR_REF_REGEX.is_match(&ref_);
58-
let head_ref = if is_pr {
69+
let (head_ref, is_head_repo_fork) = if is_pr {
5970
let github_event_path = get_env_variable("GITHUB_EVENT_PATH")?;
6071
let github_event = fs::read_to_string(github_event_path)?;
6172
let github_event: Value = serde_json::from_str(&github_event)
@@ -76,9 +87,9 @@ impl TryFrom<&Config> for GitHubActionsProvider {
7687
} else {
7788
pull_request["head"]["ref"].as_str().unwrap().to_owned()
7889
};
79-
Some(head_ref)
90+
(Some(head_ref), is_head_repo_fork)
8091
} else {
81-
None
92+
(None, false)
8293
};
8394

8495
let github_event_name = get_env_variable("GITHUB_EVENT_NAME")?;
@@ -118,6 +129,7 @@ impl TryFrom<&Config> for GitHubActionsProvider {
118129
}),
119130
base_ref: get_env_variable("GITHUB_BASE_REF").ok(),
120131
repository_root_path,
132+
is_head_repo_fork,
121133
})
122134
}
123135
}
@@ -129,6 +141,7 @@ impl RunEnvironmentDetector for GitHubActionsProvider {
129141
}
130142
}
131143

144+
#[async_trait(?Send)]
132145
impl RunEnvironmentProvider for GitHubActionsProvider {
133146
fn get_repository_provider(&self) -> RepositoryProvider {
134147
RepositoryProvider::GitHub
@@ -236,6 +249,80 @@ impl RunEnvironmentProvider for GitHubActionsProvider {
236249
.to_string();
237250
Ok(commit_hash)
238251
}
252+
253+
/// Set the OIDC token for GitHub Actions if necessary
254+
///
255+
/// ## Logic
256+
/// - If the user has explicitly set a token in the configuration (i.e. "static token"), do not override it, but display an info message.
257+
/// - Otherwise, check if the necessary environment variables are set to use OIDC.
258+
/// - Then attempt to request an OIDC token.
259+
///
260+
/// If environment variables are not set, this could be because:
261+
/// - The user has misconfigured the workflow (missing `id-token` permission)
262+
/// - The run is from a public fork, in which case GitHub Actions does not provide these environment variables for security reasons.
263+
///
264+
///
265+
/// ## Notes
266+
/// Retrieving the token requires that the workflow has the `id-token` permission enabled.
267+
///
268+
/// Docs:
269+
/// - https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-with-reusable-workflows
270+
/// - https://docs.github.com/en/actions/concepts/security/openid-connect
271+
/// - https://docs.github.com/en/actions/reference/security/oidc#methods-for-requesting-the-oidc-token
272+
async fn set_oidc_token(&self, config: &mut Config) {
273+
// Check if a static token is already set
274+
if config.token.is_some() {
275+
info!(
276+
"CodSpeed now supports OIDC tokens for authentication.\n
277+
Add the `id-token: write` permission to your workflow, remove the token from your configuration, and benefit from enhanced security.\n
278+
Learn more at https://codspeed.io/docs/integrations/ci/github-actions#openid-connect-oidc-authentication"
279+
);
280+
281+
return;
282+
}
283+
284+
// The `ACTIONS_ID_TOKEN_REQUEST_TOKEN` environment variable is set when the `id-token` permission is granted, which is necessary to authenticate with OIDC.
285+
let request_token = get_env_variable("ACTIONS_ID_TOKEN_REQUEST_TOKEN").ok();
286+
let request_url = get_env_variable("ACTIONS_ID_TOKEN_REQUEST_URL").ok();
287+
288+
if request_token.is_none() || request_url.is_none() {
289+
// Only display a warning if the run is not from a fork
290+
if !self.is_head_repo_fork {
291+
warn!(
292+
"CodSpeed was unable to request an OIDC token for this job.\n
293+
Add the `id-token: write` permission to your workflow, and benefit from enhanced security and faster processing times.\n
294+
Learn more at https://codspeed.io/docs/integrations/ci/github-actions#openid-connect-oidc-authentication"
295+
);
296+
}
297+
298+
return;
299+
}
300+
301+
let request_url = request_url.unwrap();
302+
let request_url = format!("{request_url}&audience={}", self.get_oidc_audience());
303+
let request_token = request_token.unwrap();
304+
305+
let token = match OIDC_CLIENT
306+
.get(request_url)
307+
.header("Accept", "application/json")
308+
.header("Authorization", format!("Bearer {request_token}"))
309+
.send()
310+
.await
311+
{
312+
Ok(response) => match response.json::<OIDCResponse>().await {
313+
Ok(oidc_response) => oidc_response.value,
314+
Err(_) => None,
315+
},
316+
Err(_) => None,
317+
};
318+
319+
if token.is_some() {
320+
debug!("Successfully retrieved OIDC token for authentication.");
321+
config.set_token(token);
322+
} else {
323+
warn!("Failed to retrieve OIDC token for authentication.");
324+
}
325+
}
239326
}
240327

241328
#[cfg(test)]
@@ -298,6 +385,7 @@ mod tests {
298385
github_actions_provider.sender.as_ref().unwrap().id,
299386
"1234567890"
300387
);
388+
assert!(!github_actions_provider.is_head_repo_fork);
301389
},
302390
)
303391
}
@@ -338,6 +426,8 @@ mod tests {
338426
..Config::test()
339427
};
340428
let github_actions_provider = GitHubActionsProvider::try_from(&config).unwrap();
429+
assert!(!github_actions_provider.is_head_repo_fork);
430+
341431
let run_environment_metadata = github_actions_provider
342432
.get_run_environment_metadata()
343433
.unwrap();
@@ -391,6 +481,8 @@ mod tests {
391481
..Config::test()
392482
};
393483
let github_actions_provider = GitHubActionsProvider::try_from(&config).unwrap();
484+
assert!(github_actions_provider.is_head_repo_fork);
485+
394486
let run_environment_metadata = github_actions_provider
395487
.get_run_environment_metadata()
396488
.unwrap();
@@ -471,6 +563,8 @@ mod tests {
471563
..Config::test()
472564
};
473565
let github_actions_provider = GitHubActionsProvider::try_from(&config).unwrap();
566+
assert!(!github_actions_provider.is_head_repo_fork);
567+
474568
let run_environment_metadata = github_actions_provider
475569
.get_run_environment_metadata()
476570
.unwrap();
@@ -503,6 +597,7 @@ mod tests {
503597
},
504598
event: RunEvent::Push,
505599
repository_root_path: "/home/work/my-repo".into(),
600+
is_head_repo_fork: false,
506601
};
507602

508603
let run_part = github_actions_provider.get_run_provider_run_part().unwrap();
@@ -545,6 +640,7 @@ mod tests {
545640
},
546641
event: RunEvent::Push,
547642
repository_root_path: "/home/work/my-repo".into(),
643+
is_head_repo_fork: false,
548644
};
549645

550646
let run_part = github_actions_provider.get_run_provider_run_part().unwrap();
@@ -596,6 +692,7 @@ mod tests {
596692
},
597693
event: RunEvent::Push,
598694
repository_root_path: "/home/work/my-repo".into(),
695+
is_head_repo_fork: false,
599696
};
600697

601698
let run_part = github_actions_provider.get_run_provider_run_part().unwrap();
@@ -645,6 +742,7 @@ mod tests {
645742
},
646743
event: RunEvent::Push,
647744
repository_root_path: "/home/work/my-repo".into(),
745+
is_head_repo_fork: false,
648746
};
649747

650748
let run_part = github_actions_provider.get_run_provider_run_part().unwrap();

src/run/run_environment/gitlab_ci/provider.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use async_trait::async_trait;
12
use simplelog::SharedLogger;
23
use std::collections::BTreeMap;
34
use std::env;
@@ -139,6 +140,7 @@ impl RunEnvironmentDetector for GitLabCIProvider {
139140
}
140141
}
141142

143+
#[async_trait(?Send)]
142144
impl RunEnvironmentProvider for GitLabCIProvider {
143145
fn get_logger(&self) -> Box<dyn SharedLogger> {
144146
Box::new(GitLabCILogger::new())
@@ -175,6 +177,13 @@ impl RunEnvironmentProvider for GitLabCIProvider {
175177
metadata: BTreeMap::new(),
176178
})
177179
}
180+
181+
/// For GitLab CI, OIDC tokens must be passed via env variable.
182+
///
183+
/// See:
184+
/// - https://docs.gitlab.com/integration/openid_connect_provider/
185+
/// - https://docs.gitlab.com/ci/secrets/id_token_authentication/
186+
async fn set_oidc_token(&self, _config: &mut Config) {}
178187
}
179188

180189
#[cfg(test)]

src/run/run_environment/local/provider.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use async_trait::async_trait;
12
use git2::Repository;
23
use simplelog::SharedLogger;
34

@@ -117,6 +118,7 @@ impl RunEnvironmentDetector for LocalProvider {
117118
}
118119
}
119120

121+
#[async_trait(?Send)]
120122
impl RunEnvironmentProvider for LocalProvider {
121123
fn get_repository_provider(&self) -> RepositoryProvider {
122124
self.repository_provider.clone()

src/run/run_environment/provider.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use async_trait::async_trait;
12
use git2::Repository;
23
use simplelog::SharedLogger;
34

@@ -16,8 +17,16 @@ pub trait RunEnvironmentDetector {
1617
fn detect() -> bool;
1718
}
1819

20+
/// Audience to be used when requesting OIDC tokens.
21+
///
22+
/// It will be validated when the token is used to authenticate with CodSpeed.
23+
///
24+
/// This value must match the audience configured in CodSpeed backend.
25+
static OIDC_AUDIENCE: &str = "codspeed.io";
26+
1927
/// `RunEnvironmentProvider` is a trait that defines the necessary methods
2028
/// for a continuous integration provider.
29+
#[async_trait(?Send)]
2130
pub trait RunEnvironmentProvider {
2231
/// Returns the logger for the RunEnvironment.
2332
fn get_logger(&self) -> Box<dyn SharedLogger>;
@@ -34,6 +43,21 @@ pub trait RunEnvironmentProvider {
3443
/// Return the metadata necessary to identify the `RunPart`
3544
fn get_run_provider_run_part(&self) -> Option<RunPart>;
3645

46+
/// Get the OIDC audience that must be used when requesting OIDC tokens.
47+
///
48+
/// It will be validated when the token is used to authenticate with CodSpeed.
49+
fn get_oidc_audience(&self) -> &str {
50+
OIDC_AUDIENCE
51+
}
52+
53+
/// Handle an OIDC token for the current run environment, if supported.
54+
///
55+
/// Updates the config if necessary.
56+
///
57+
/// Depending on the provider, this may involve requesting the token,
58+
/// warning the user about potential misconfigurations, or other necessary steps.
59+
async fn set_oidc_token(&self, _config: &mut Config) {}
60+
3761
/// Returns the metadata necessary for uploading results to CodSpeed.
3862
///
3963
/// # Arguments

0 commit comments

Comments
 (0)