Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 69 additions & 32 deletions src/api_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::fmt::Display;

use crate::executor::ExecutorName;
use crate::prelude::*;
use crate::run_environment::RepositoryProvider;
use crate::run_environment::{RepositoryProvider, RunEnvironment};
use console::style;
use gql_client::{Client as GQLClient, ClientConfig};
use nestify::nest;
Expand All @@ -12,50 +12,87 @@ pub struct CodSpeedAPIClient {
gql_client: GQLClient,
unauthenticated_gql_client: GQLClient,
api_url: String,
/// The token this client authenticates with. Exposed so downstream
/// consumers (the uploader's `Authorization` header, the executor's
/// `CODSPEED_OAUTH_TOKEN` env injection) don't have to thread the
/// token separately from the client.
token: Option<String>,
authentication: Authentication,
}

/// How the runner authenticates the uploads of a run, and the token it does so
/// with.
///
/// The runner cannot always tell which kind of token it was handed: GitLab CI
/// passes the OIDC token it issues through `CODSPEED_TOKEN`, exactly where a
/// static CodSpeed token would be, and only the API can tell the two apart. So
/// these variants name where the token came from, not what it turned out to be.
///
/// Deliberately not `Debug`: every variant but one holds a credential.
#[derive(Clone)]
pub enum Authentication {
/// An OIDC token the runner minted from the run environment. Replaced before
/// every upload, since a token expires an hour after it is minted.
Oidc(String),
/// The token the run was given through `--token` / `CODSPEED_TOKEN`.
RunToken(String),
/// A token obtained through `codspeed auth login`: either the one persisted
/// for the selected profile, or one passed through `--oauth-token` /
/// `CODSPEED_OAUTH_TOKEN`.
CliLogin(String),
/// No token at all. CodSpeed matches the upload to the job by looking for the
/// run hash the runner prints, which only works for public repositories.
Tokenless,
}

impl Authentication {
pub fn token(&self) -> Option<&str> {
match self {
Authentication::Oidc(token)
| Authentication::RunToken(token)
| Authentication::CliLogin(token) => Some(token),
Authentication::Tokenless => None,
}
}

/// How to name it in the runner output.
pub fn label(&self, run_environment: &RunEnvironment) -> String {
match self {
Authentication::Oidc(_) => format!("OIDC token minted by {run_environment}"),
Authentication::RunToken(_) => "token from `CODSPEED_TOKEN`".to_owned(),
Authentication::CliLogin(_) => "token from `codspeed auth login`".to_owned(),
Comment thread
fargito marked this conversation as resolved.
Authentication::Tokenless => {
"tokenless, supported for public repositories only".to_owned()
}
}
}
}

impl CodSpeedAPIClient {
/// Build a client authenticated with `token` (when `Some`).
///
/// The CLI resolves the effective token at construction time, so
/// callers downstream (the uploader, the executor's env injection,
/// every GraphQL caller) just consume it from the client through
/// [`Self::token`] and don't have to thread the token separately.
pub fn new(token: Option<String>, api_url: String) -> Self {
pub fn new(authentication: Authentication, api_url: String) -> Self {
Self {
gql_client: build_gql_api_client(token.as_deref(), api_url.clone()),
gql_client: build_gql_api_client(authentication.token(), api_url.clone()),
unauthenticated_gql_client: build_gql_api_client(None, api_url.clone()),
api_url,
token,
authentication,
}
}

/// Returns a client that uses `token` for authentication, regardless of
/// the token this client was built with.
pub fn with_token(&self, token: String) -> Self {
Self::new(Some(token), self.api_url.clone())
/// A copy of this client, authenticating differently.
pub fn with_authentication(&self, authentication: Authentication) -> Self {
Self::new(authentication, self.api_url.clone())
}

/// The token this client currently authenticates with, if any.
///
/// Note: this is not necessarily the token the client was built with —
/// in CI with OIDC, [`Self::set_token`] is called before each upload to
/// rotate the credentials. See [`crate::run_environment::RunEnvironmentProvider::refresh_token`].
pub fn token(&self) -> Option<&str> {
self.token.as_deref()
self.authentication.token()
}

/// How this client authenticates. Not necessarily how it was built: with
/// OIDC the token is rotated before every upload, in
/// [`crate::run_environment::RunEnvironmentProvider::set_oidc_token`].
pub fn authentication(&self) -> &Authentication {
&self.authentication
}

/// Replace the token this client uses for authenticated GraphQL
/// requests and that the uploader pulls for its `Authorization`
/// header. The single mutation point for the credentials.
pub fn set_token(&mut self, token: Option<String>) {
self.gql_client = build_gql_api_client(token.as_deref(), self.api_url.clone());
self.token = token;
/// The single mutation point for the credentials.
pub fn set_authentication(&mut self, authentication: Authentication) {
self.gql_client = build_gql_api_client(authentication.token(), self.api_url.clone());
self.authentication = authentication;
}
}

Expand Down Expand Up @@ -560,6 +597,6 @@ impl CodSpeedAPIClient {
/// Create a test API client with a custom URL for use in tests
#[cfg(test)]
pub fn create_test_client_with_url(api_url: String) -> Self {
Self::new(None, api_url)
Self::new(Authentication::Tokenless, api_url)
}
}
8 changes: 5 additions & 3 deletions src/cli/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ use std::io::Read;
use std::time::Duration;

use crate::api_client::{
CodSpeedAPIClient, RepositoryOverviewPayload, SessionAndRepositoryOverviewError,
SessionAndRepositoryOverviewVars, SessionError, SessionPayload,
Authentication, CodSpeedAPIClient, RepositoryOverviewPayload,
SessionAndRepositoryOverviewError, SessionAndRepositoryOverviewVars, SessionError,
SessionPayload,
};
use crate::cli::run::helpers::{
ParsedRepository, find_repository_root, parse_repository_from_remote,
Expand Down Expand Up @@ -111,7 +112,8 @@ async fn login(
};

// Validate the token before persisting
let api_client_with_token = api_client.with_token(token.clone());
let api_client_with_token =
api_client.with_authentication(Authentication::CliLogin(token.clone()));
api_client_with_token
.session()
.await
Expand Down
16 changes: 10 additions & 6 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub(crate) use shared::*;
use std::path::PathBuf;

use crate::{
api_client::CodSpeedAPIClient,
api_client::{Authentication, CodSpeedAPIClient},
config::{CodSpeedConfig, ConfigOverrides},
executor::helpers::command::CommandBuilder,
local_logger::{CODSPEED_U8_COLOR_CODE, init_local_logger},
Expand Down Expand Up @@ -254,14 +254,18 @@ fn load_config(cli: &Cli) -> Result<CodSpeedConfig> {
/// 2. `--oauth-token` / `CODSPEED_OAUTH_TOKEN` and the persisted CLI
/// token from the selected profile.
fn build_api_client(cli: &Cli, config: &CodSpeedConfig) -> CodSpeedAPIClient {
let explicit = match &cli.command {
let run_token = match &cli.command {
Commands::Run(args) => args.shared.token.clone(),
Commands::Exec(args) => args.shared.token.clone(),
_ => None,
};
let token = match explicit {
Some(token) => Some(token),
None => config.auth.token.clone(),
let authentication = match run_token {
Some(token) => Authentication::RunToken(token),
None => config
.auth
.token
.clone()
.map_or(Authentication::Tokenless, Authentication::CliLogin),
};
CodSpeedAPIClient::new(token, config.api_url.clone())
CodSpeedAPIClient::new(authentication, config.api_url.clone())
}
17 changes: 17 additions & 0 deletions src/executor/orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,18 @@ impl Orchestrator {
Ok(())
}

fn log_authentication(&self, api_client: &CodSpeedAPIClient) {
let run_environment = self.provider.get_run_environment();

info!(
"Authentication: {}",
api_client.authentication().label(&run_environment)
);
if let Some(url) = run_environment.authentication_docs_url() {
info!("Learn more at {url}");
}
}

/// Resolve the profile folder for a given run part.
///
/// - Single run part + user-specified folder: use as-is
Expand Down Expand Up @@ -257,6 +269,11 @@ impl Orchestrator {
// OIDC tokens can expire quickly, so refresh just before each upload
self.provider.set_oidc_token(api_client).await?;

if run_part_index == 0 {
// After the mint, so this names the token the upload actually uses
self.log_authentication(api_client);
}

if total_runs > 1 {
info!("Uploading results {}/{total_runs}", run_part_index + 1);
}
Expand Down
23 changes: 14 additions & 9 deletions src/run_environment/circleci/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ use async_trait::async_trait;
use serde_json::Value;
use simplelog::SharedLogger;

use crate::api_client::CodSpeedAPIClient;
use crate::api_client::{Authentication, CodSpeedAPIClient};
use crate::cli::run::helpers::{
GitRemote, find_repository_root, get_env_variable, parse_git_remote,
};
use crate::executor::config::OrchestratorConfig;
use crate::prelude::*;
use crate::run_environment::interfaces::{RepositoryProvider, RunEnvironmentMetadata, RunEvent};
use crate::run_environment::interfaces::{
CIRCLECI_AUTHENTICATION_DOCS_URL, CIRCLECI_OIDC_DOCS_URL, RepositoryProvider,
RunEnvironmentMetadata, RunEvent,
};
use crate::run_environment::provider::{RunEnvironmentDetector, RunEnvironmentProvider};
use crate::run_environment::{RunEnvironment, RunPart};

Expand Down Expand Up @@ -222,11 +225,11 @@ impl RunEnvironmentProvider for CircleCIProvider {
fn check_oidc_configuration(&mut self, api_client: &CodSpeedAPIClient) -> Result<()> {
if api_client.token().is_some() {
if !self.is_forked_pull_request {
announcement!(
announcement!(format!(
"You can now authenticate your CircleCI jobs using OpenID Connect (OIDC) tokens instead of `CODSPEED_TOKEN` secrets.\n\
This makes integrating and authenticating jobs safer and simpler.\n\
Learn more at https://codspeed.io/docs/integrations/ci/circleci/configuration#oidc-recommended\n"
);
Learn more at {CIRCLECI_OIDC_DOCS_URL}\n"
));
}

return Ok(());
Expand All @@ -236,7 +239,7 @@ impl RunEnvironmentProvider for CircleCIProvider {
bail!(
"Pull requests opened from a fork cannot authenticate with OIDC on CircleCI.\n\
Set `CODSPEED_TOKEN` for this job instead.\n\
See https://codspeed.io/docs/integrations/ci/circleci/configuration#authentication"
See {CIRCLECI_AUTHENTICATION_DOCS_URL}"
);
}

Expand All @@ -245,7 +248,7 @@ impl RunEnvironmentProvider for CircleCIProvider {
"{error}\n\
Unable to mint an OIDC token for authentication. \
Set `CODSPEED_TOKEN` for this job instead.\n\
See https://codspeed.io/docs/integrations/ci/circleci/configuration#oidc-recommended"
See {CIRCLECI_OIDC_DOCS_URL}"
);
}

Expand All @@ -266,7 +269,7 @@ impl RunEnvironmentProvider for CircleCIProvider {
let token = oidc::mint_token(self.get_oidc_audience())?;

debug!("Minted an OIDC token to authenticate the upload");
api_client.set_token(Some(token));
api_client.set_authentication(Authentication::Oidc(token));

Ok(())
}
Expand Down Expand Up @@ -498,7 +501,9 @@ mod tests {

fn api_client(token: Option<&str>) -> CodSpeedAPIClient {
CodSpeedAPIClient::new(
token.map(str::to_string),
token.map_or(Authentication::Tokenless, |token| {
Authentication::RunToken(token.to_string())
}),
"https://gql.codspeed.io/".to_string(),
)
}
Expand Down
24 changes: 12 additions & 12 deletions src/run_environment/github_actions/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ use simplelog::SharedLogger;
use std::collections::BTreeMap;
use std::{env, fs};

use crate::api_client::CodSpeedAPIClient;
use crate::api_client::{Authentication, CodSpeedAPIClient};
use crate::cli::run::helpers::{find_repository_root, get_env_variable};
use crate::executor::config::OrchestratorConfig;
use crate::prelude::*;
use crate::request_client::OIDC_CLIENT;
use crate::run_environment::interfaces::{
RepositoryProvider, RunEnvironmentMetadata, RunEvent, Sender,
GITHUB_ACTIONS_OIDC_DOCS_URL, RepositoryProvider, RunEnvironmentMetadata, RunEvent, Sender,
};
use crate::run_environment::provider::{RunEnvironmentDetector, RunEnvironmentProvider};
use crate::run_environment::{RunEnvironment, RunPart};
Expand Down Expand Up @@ -289,11 +289,11 @@ impl RunEnvironmentProvider for GitHubActionsProvider {
fn check_oidc_configuration(&mut self, api_client: &CodSpeedAPIClient) -> Result<()> {
// Check if a static token is already set
if api_client.token().is_some() {
announcement!(
announcement!(format!(
"You can now authenticate your CI workflows using OpenID Connect (OIDC) tokens instead of `CODSPEED_TOKEN` secrets.\n\
This makes integrating and authenticating jobs safer and simpler.\n\
Learn more at https://codspeed.io/docs/integrations/ci/github-actions/configuration#oidc-recommended\n"
);
Learn more at {GITHUB_ACTIONS_OIDC_DOCS_URL}\n"
));

return Ok(());
}
Expand All @@ -313,15 +313,15 @@ impl RunEnvironmentProvider for GitHubActionsProvider {
bail!(
"Unable to retrieve OIDC token for authentication.\n\
Make sure your workflow has the `id-token: write` permission set.\n\
See https://codspeed.io/docs/integrations/ci/github-actions/configuration#oidc-recommended"
See {GITHUB_ACTIONS_OIDC_DOCS_URL}"
)
}

announcement!(
announcement!(format!(
"You can now authenticate your CI workflows using OpenID Connect (OIDC).\n\
This makes integrating and authenticating jobs safer and simpler.\n\
Learn more at https://codspeed.io/docs/integrations/ci/github-actions/configuration#oidc-recommended\n"
);
Learn more at {GITHUB_ACTIONS_OIDC_DOCS_URL}\n"
));

return Ok(());
}
Expand Down Expand Up @@ -366,14 +366,14 @@ impl RunEnvironmentProvider for GitHubActionsProvider {
Err(_) => None,
};

if token.is_some() {
if let Some(token) = token {
debug!("Successfully retrieved OIDC token for authentication.");
api_client.set_token(token);
api_client.set_authentication(Authentication::Oidc(token));
} else if self.is_repository_private {
bail!(
"Unable to retrieve OIDC token for authentication. \n\
Make sure your workflow has the `id-token: write` permission set. \n\
See https://codspeed.io/docs/integrations/ci/github-actions/configuration#oidc-recommended"
See {GITHUB_ACTIONS_OIDC_DOCS_URL}"
)
} else {
warn!("Failed to retrieve OIDC token for authentication.");
Expand Down
Loading