Skip to content

Commit ff17e84

Browse files
committed
feat(provider): add support for oidc token authentication
1 parent a4ac960 commit ff17e84

File tree

10 files changed

+239
-9
lines changed

10 files changed

+239
-9
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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ pub struct RunArgs {
6868
pub upload_url: Option<String>,
6969

7070
/// The token to use for uploading the results,
71+
///
72+
/// It can be either a CodSpeed token retrieved from the repository setting
73+
/// or an OIDC token issued by the identity provider.
7174
#[arg(long, env = "CODSPEED_TOKEN")]
7275
pub token: Option<String>,
7376

@@ -205,6 +208,9 @@ pub async fn run(
205208
}
206209
debug!("Using the token from the CodSpeed configuration file");
207210
config.set_token(codspeed_config.auth.token.clone());
211+
} else {
212+
// If relevant, set the OIDC token for authentication
213+
provider.set_oidc_token(&mut config).await?;
208214
}
209215

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

src/run/run_environment/buildkite/provider.rs

Lines changed: 13 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,17 @@ 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) -> Result<()> {
165+
Ok(())
166+
}
154167
}
155168

156169
#[cfg(test)]

src/run/run_environment/github_actions/provider.rs

Lines changed: 151 additions & 7 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,12 @@ 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,
39+
40+
/// Indicates whether the repository is private.
41+
is_repository_private: bool,
3342
}
3443

3544
impl GitHubActionsProvider {
@@ -42,6 +51,11 @@ impl GitHubActionsProvider {
4251
}
4352
}
4453

54+
#[derive(Deserialize)]
55+
struct OIDCResponse {
56+
value: Option<String>,
57+
}
58+
4559
lazy_static! {
4660
static ref PR_REF_REGEX: Regex = Regex::new(r"^refs/pull/(?P<pr_number>\d+)/merge$").unwrap();
4761
}
@@ -53,13 +67,18 @@ impl TryFrom<&Config> for GitHubActionsProvider {
5367
bail!("Specifying owner and repository from CLI is not supported for Github Actions");
5468
}
5569
let (owner, repository) = Self::get_owner_and_repository()?;
70+
71+
let github_event_path = get_env_variable("GITHUB_EVENT_PATH")?;
72+
let github_event = fs::read_to_string(github_event_path)?;
73+
let github_event: Value =
74+
serde_json::from_str(&github_event).expect("GITHUB_EVENT_PATH file could not be read");
75+
5676
let ref_ = get_env_variable("GITHUB_REF")?;
5777
let is_pr = PR_REF_REGEX.is_match(&ref_);
58-
let head_ref = if is_pr {
59-
let github_event_path = get_env_variable("GITHUB_EVENT_PATH")?;
60-
let github_event = fs::read_to_string(github_event_path)?;
61-
let github_event: Value = serde_json::from_str(&github_event)
62-
.expect("GITHUB_EVENT_PATH file could not be read");
78+
79+
let is_repository_private = github_event["repository"]["private"].as_bool().unwrap();
80+
81+
let (head_ref, is_head_repo_fork) = if is_pr {
6382
let pull_request = github_event["pull_request"].as_object().unwrap();
6483

6584
let head_repo = pull_request["head"]["repo"].as_object().unwrap();
@@ -76,9 +95,9 @@ impl TryFrom<&Config> for GitHubActionsProvider {
7695
} else {
7796
pull_request["head"]["ref"].as_str().unwrap().to_owned()
7897
};
79-
Some(head_ref)
98+
(Some(head_ref), is_head_repo_fork)
8099
} else {
81-
None
100+
(None, false)
82101
};
83102

84103
let github_event_name = get_env_variable("GITHUB_EVENT_NAME")?;
@@ -118,6 +137,8 @@ impl TryFrom<&Config> for GitHubActionsProvider {
118137
}),
119138
base_ref: get_env_variable("GITHUB_BASE_REF").ok(),
120139
repository_root_path,
140+
is_head_repo_fork,
141+
is_repository_private,
121142
})
122143
}
123144
}
@@ -129,6 +150,7 @@ impl RunEnvironmentDetector for GitHubActionsProvider {
129150
}
130151
}
131152

153+
#[async_trait(?Send)]
132154
impl RunEnvironmentProvider for GitHubActionsProvider {
133155
fn get_repository_provider(&self) -> RepositoryProvider {
134156
RepositoryProvider::GitHub
@@ -236,6 +258,99 @@ impl RunEnvironmentProvider for GitHubActionsProvider {
236258
.to_string();
237259
Ok(commit_hash)
238260
}
261+
262+
/// Set the OIDC token for GitHub Actions if necessary
263+
///
264+
/// ## Logic
265+
/// - If the user has explicitly set a token in the configuration (i.e. "static token"), do not override it, but display an info message.
266+
/// - Otherwise, check if the necessary environment variables are set to use OIDC.
267+
/// - Then attempt to request an OIDC token.
268+
///
269+
/// If environment variables are not set, this could be because:
270+
/// - The user has misconfigured the workflow (missing `id-token` permission)
271+
/// - The run is from a public fork, in which case GitHub Actions does not provide these environment variables for security reasons.
272+
///
273+
///
274+
/// ## Notes
275+
/// Retrieving the token requires that the workflow has the `id-token` permission enabled.
276+
///
277+
/// Docs:
278+
/// - https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-with-reusable-workflows
279+
/// - https://docs.github.com/en/actions/concepts/security/openid-connect
280+
/// - https://docs.github.com/en/actions/reference/security/oidc#methods-for-requesting-the-oidc-token
281+
async fn set_oidc_token(&self, config: &mut Config) -> Result<()> {
282+
// Check if a static token is already set
283+
if config.token.is_some() {
284+
info!(
285+
"CodSpeed now supports OIDC tokens for authentication.\n
286+
Benefit from enhanced security by adding the `id-token: write` permission to your workflow and removing the static token from your configuration.\n
287+
Learn more at https://codspeed.io/docs/integrations/ci/github-actions#openid-connect-oidc-authentication"
288+
);
289+
290+
return Ok(());
291+
}
292+
293+
// The `ACTIONS_ID_TOKEN_REQUEST_TOKEN` environment variable is set when the `id-token` permission is granted, which is necessary to authenticate with OIDC.
294+
let request_token = get_env_variable("ACTIONS_ID_TOKEN_REQUEST_TOKEN").ok();
295+
let request_url = get_env_variable("ACTIONS_ID_TOKEN_REQUEST_URL").ok();
296+
297+
if request_token.is_none() || request_url.is_none() {
298+
// If the run is from a fork, it is expected that these environment variables are not set.
299+
// We will fall back to tokenless authentication in this case.
300+
if self.is_head_repo_fork {
301+
return Ok(());
302+
}
303+
304+
if self.is_repository_private {
305+
bail!(
306+
"Unable to retrieve OIDC token for authentication. \n
307+
Make sure your workflow has the `id-token: write` permission set. \n
308+
See https://codspeed.io/docs/integrations/ci/github-actions#openid-connect-oidc-authentication"
309+
)
310+
}
311+
312+
info!(
313+
"CodSpeed now supports OIDC tokens for authentication.\n
314+
Benefit from enhanced security and faster processing times by adding the `id-token: write` permission to your workflow.\n
315+
Learn more at https://codspeed.io/docs/integrations/ci/github-actions#openid-connect-oidc-authentication"
316+
);
317+
318+
return Ok(());
319+
}
320+
321+
let request_url = request_url.unwrap();
322+
let request_url = format!("{request_url}&audience={}", self.get_oidc_audience());
323+
let request_token = request_token.unwrap();
324+
325+
let token = match OIDC_CLIENT
326+
.get(request_url)
327+
.header("Accept", "application/json")
328+
.header("Authorization", format!("Bearer {request_token}"))
329+
.send()
330+
.await
331+
{
332+
Ok(response) => match response.json::<OIDCResponse>().await {
333+
Ok(oidc_response) => oidc_response.value,
334+
Err(_) => None,
335+
},
336+
Err(_) => None,
337+
};
338+
339+
if token.is_some() {
340+
debug!("Successfully retrieved OIDC token for authentication.");
341+
config.set_token(token);
342+
} else if self.is_repository_private {
343+
bail!(
344+
"Unable to retrieve OIDC token for authentication. \n
345+
Make sure your workflow has the `id-token: write` permission set. \n
346+
See https://codspeed.io/docs/integrations/ci/github-actions#openid-connect-oidc-authentication"
347+
)
348+
} else {
349+
warn!("Failed to retrieve OIDC token for authentication.");
350+
}
351+
352+
Ok(())
353+
}
239354
}
240355

241356
#[cfg(test)]
@@ -271,6 +386,16 @@ mod tests {
271386
("GITHUB_ACTOR", Some("actor")),
272387
("GITHUB_BASE_REF", Some("main")),
273388
("GITHUB_EVENT_NAME", Some("push")),
389+
(
390+
"GITHUB_EVENT_PATH",
391+
Some(
392+
format!(
393+
"{}/src/run/run_environment/github_actions/samples/push-event.json",
394+
env!("CARGO_MANIFEST_DIR")
395+
)
396+
.as_str(),
397+
),
398+
),
274399
("GITHUB_JOB", Some("job")),
275400
("GITHUB_REF", Some("refs/heads/main")),
276401
("GITHUB_REPOSITORY", Some("owner/repository")),
@@ -298,6 +423,8 @@ mod tests {
298423
github_actions_provider.sender.as_ref().unwrap().id,
299424
"1234567890"
300425
);
426+
assert!(!github_actions_provider.is_head_repo_fork);
427+
assert!(!github_actions_provider.is_repository_private);
301428
},
302429
)
303430
}
@@ -338,6 +465,9 @@ mod tests {
338465
..Config::test()
339466
};
340467
let github_actions_provider = GitHubActionsProvider::try_from(&config).unwrap();
468+
assert!(!github_actions_provider.is_head_repo_fork);
469+
assert!(github_actions_provider.is_repository_private);
470+
341471
let run_environment_metadata = github_actions_provider
342472
.get_run_environment_metadata()
343473
.unwrap();
@@ -391,6 +521,9 @@ mod tests {
391521
..Config::test()
392522
};
393523
let github_actions_provider = GitHubActionsProvider::try_from(&config).unwrap();
524+
assert!(github_actions_provider.is_head_repo_fork);
525+
assert!(!github_actions_provider.is_repository_private);
526+
394527
let run_environment_metadata = github_actions_provider
395528
.get_run_environment_metadata()
396529
.unwrap();
@@ -471,6 +604,9 @@ mod tests {
471604
..Config::test()
472605
};
473606
let github_actions_provider = GitHubActionsProvider::try_from(&config).unwrap();
607+
assert!(!github_actions_provider.is_head_repo_fork);
608+
assert!(github_actions_provider.is_repository_private);
609+
474610
let run_environment_metadata = github_actions_provider
475611
.get_run_environment_metadata()
476612
.unwrap();
@@ -503,6 +639,8 @@ mod tests {
503639
},
504640
event: RunEvent::Push,
505641
repository_root_path: "/home/work/my-repo".into(),
642+
is_head_repo_fork: false,
643+
is_repository_private: false,
506644
};
507645

508646
let run_part = github_actions_provider.get_run_provider_run_part().unwrap();
@@ -545,6 +683,8 @@ mod tests {
545683
},
546684
event: RunEvent::Push,
547685
repository_root_path: "/home/work/my-repo".into(),
686+
is_head_repo_fork: false,
687+
is_repository_private: false,
548688
};
549689

550690
let run_part = github_actions_provider.get_run_provider_run_part().unwrap();
@@ -596,6 +736,8 @@ mod tests {
596736
},
597737
event: RunEvent::Push,
598738
repository_root_path: "/home/work/my-repo".into(),
739+
is_head_repo_fork: false,
740+
is_repository_private: false,
599741
};
600742

601743
let run_part = github_actions_provider.get_run_provider_run_part().unwrap();
@@ -645,6 +787,8 @@ mod tests {
645787
},
646788
event: RunEvent::Push,
647789
repository_root_path: "/home/work/my-repo".into(),
790+
is_head_repo_fork: false,
791+
is_repository_private: false,
648792
};
649793

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

src/run/run_environment/github_actions/samples/fork-pr-event.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,8 @@
1515
},
1616
"sha": "24809d9fca9ad0808a777bcbd807ecd5ec8a9100"
1717
}
18+
},
19+
"repository": {
20+
"private": false
1821
}
1922
}

src/run/run_environment/github_actions/samples/pr-event.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,8 @@
1212
},
1313
"sha": "24809d9fca9ad0808a777bcbd807ecd5ec8a9100"
1414
}
15+
},
16+
"repository": {
17+
"private": true
1518
}
1619
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"repository": {
3+
"private": false
4+
}
5+
}

0 commit comments

Comments
 (0)