1+ use async_trait:: async_trait;
12use git2:: Repository ;
23use lazy_static:: lazy_static;
34use regex:: Regex ;
5+ use serde:: Deserialize ;
46use serde_json:: Value ;
57use simplelog:: SharedLogger ;
68use std:: collections:: BTreeMap ;
79use std:: { env, fs} ;
810
911use crate :: prelude:: * ;
12+ use crate :: request_client:: OIDC_CLIENT ;
1013use crate :: run:: run_environment:: { RunEnvironment , RunPart } ;
1114use 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
3541impl GitHubActionsProvider {
@@ -42,6 +48,11 @@ impl GitHubActionsProvider {
4248 }
4349}
4450
51+ #[ derive( Deserialize ) ]
52+ struct OIDCResponse {
53+ value : Option < String > ,
54+ }
55+
4556lazy_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 ) ]
132145impl 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 ( ) ;
0 commit comments