use std::collections::BTreeMap; use base64::{Engine as _, engine::general_purpose::STANDARD}; use crank_core::{AuthConfig, AuthProfile, SecretId}; use serde_json::Value; use crate::{PreparedRequest, RuntimeError}; #[derive(Clone, Debug, PartialEq, Eq)] pub enum ResolvedAuth { Bearer { header_name: String, token: String }, Basic { username: String, password: String }, ApiKeyHeader { header_name: String, value: String }, ApiKeyQuery { param_name: String, value: String }, } impl ResolvedAuth { pub fn from_profile( profile: &AuthProfile, secrets: &BTreeMap, ) -> Result { match &profile.config { AuthConfig::Bearer(config) => Ok(Self::Bearer { header_name: config.header_name.clone(), token: extract_secret_string(secrets, &config.secret_id, &["token", "value"])?, }), AuthConfig::Basic(config) => Ok(Self::Basic { username: extract_secret_string( secrets, &config.username_secret_id, &["username", "value"], )?, password: extract_secret_string( secrets, &config.password_secret_id, &["password", "value"], )?, }), AuthConfig::ApiKeyHeader(config) => Ok(Self::ApiKeyHeader { header_name: config.header_name.clone(), value: extract_secret_string(secrets, &config.secret_id, &["token", "value"])?, }), AuthConfig::ApiKeyQuery(config) => Ok(Self::ApiKeyQuery { param_name: config.param_name.clone(), value: extract_secret_string(secrets, &config.secret_id, &["token", "value"])?, }), } } pub fn apply(&self, prepared_request: &mut PreparedRequest) { match self { Self::Bearer { header_name, token } => { prepared_request .headers .insert(header_name.clone(), format!("Bearer {token}")); } Self::Basic { username, password } => { let credentials = STANDARD.encode(format!("{username}:{password}")); prepared_request .headers .insert("Authorization".to_owned(), format!("Basic {credentials}")); } Self::ApiKeyHeader { header_name, value } => { prepared_request .headers .insert(header_name.clone(), value.clone()); } Self::ApiKeyQuery { param_name, value } => { prepared_request .query_params .insert(param_name.clone(), value.clone()); } } } } pub(crate) fn apply_resolved_auth( mut prepared_request: PreparedRequest, resolved_auth: Option<&ResolvedAuth>, ) -> PreparedRequest { if let Some(resolved_auth) = resolved_auth { resolved_auth.apply(&mut prepared_request); } prepared_request } fn extract_secret_string( secrets: &BTreeMap, secret_id: &SecretId, preferred_fields: &[&str], ) -> Result { let Some(value) = secrets.get(secret_id) else { return Err(RuntimeError::MissingSecret { secret_id: secret_id.as_str().to_owned(), }); }; if let Some(text) = value.as_str() { return Ok(text.to_owned()); } let Some(object) = value.as_object() else { return Err(RuntimeError::InvalidAuthSecretValue { secret_id: secret_id.as_str().to_owned(), reason: "secret payload must be a string or object".to_owned(), }); }; for field in preferred_fields { if let Some(text) = object.get(*field).and_then(Value::as_str) { return Ok(text.to_owned()); } } Err(RuntimeError::InvalidAuthSecretValue { secret_id: secret_id.as_str().to_owned(), reason: format!( "secret payload must contain one of: {}", preferred_fields.join(", ") ), }) } #[cfg(test)] mod tests { use std::collections::BTreeMap; use crank_core::{ ApiKeyHeaderAuthConfig, ApiKeyQueryAuthConfig, AuthConfig, AuthKind, AuthProfile, BasicAuthConfig, BearerAuthConfig, SecretId, WorkspaceId, }; use serde_json::json; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use crate::{PreparedRequest, auth::ResolvedAuth}; fn timestamp(value: &str) -> OffsetDateTime { OffsetDateTime::parse(value, &Rfc3339).unwrap() } #[test] fn applies_bearer_auth_to_headers() { let auth = ResolvedAuth::from_profile( &AuthProfile { id: "auth_01".into(), workspace_id: WorkspaceId::new("ws_default"), name: "bearer".to_owned(), kind: AuthKind::Bearer, config: AuthConfig::Bearer(BearerAuthConfig { header_name: "Authorization".to_owned(), secret_id: SecretId::new("secret_token"), }), created_at: timestamp("2026-04-07T00:00:00Z"), updated_at: timestamp("2026-04-07T00:00:00Z"), }, &BTreeMap::from([(SecretId::new("secret_token"), json!({"token":"abc"}))]), ) .unwrap(); let mut request = PreparedRequest::default(); auth.apply(&mut request); assert_eq!( request.headers.get("Authorization").map(String::as_str), Some("Bearer abc") ); } #[test] fn applies_basic_auth_to_headers() { let auth = ResolvedAuth::from_profile( &AuthProfile { id: "auth_01".into(), workspace_id: WorkspaceId::new("ws_default"), name: "basic".to_owned(), kind: AuthKind::Basic, config: AuthConfig::Basic(BasicAuthConfig { username_secret_id: SecretId::new("secret_user"), password_secret_id: SecretId::new("secret_pass"), }), created_at: timestamp("2026-04-07T00:00:00Z"), updated_at: timestamp("2026-04-07T00:00:00Z"), }, &BTreeMap::from([ (SecretId::new("secret_user"), json!({"username":"demo"})), (SecretId::new("secret_pass"), json!({"password":"s3cr3t"})), ]), ) .unwrap(); let mut request = PreparedRequest::default(); auth.apply(&mut request); assert_eq!( request.headers.get("Authorization").map(String::as_str), Some("Basic ZGVtbzpzM2NyM3Q=") ); } #[test] fn applies_api_key_header_auth() { let auth = ResolvedAuth::from_profile( &AuthProfile { id: "auth_01".into(), workspace_id: WorkspaceId::new("ws_default"), name: "api-header".to_owned(), kind: AuthKind::ApiKeyHeader, config: AuthConfig::ApiKeyHeader(ApiKeyHeaderAuthConfig { header_name: "X-Api-Key".to_owned(), secret_id: SecretId::new("secret_api_key"), }), created_at: timestamp("2026-04-07T00:00:00Z"), updated_at: timestamp("2026-04-07T00:00:00Z"), }, &BTreeMap::from([(SecretId::new("secret_api_key"), json!("key-123"))]), ) .unwrap(); let mut request = PreparedRequest::default(); auth.apply(&mut request); assert_eq!( request.headers.get("X-Api-Key").map(String::as_str), Some("key-123") ); } #[test] fn applies_api_key_query_auth() { let auth = ResolvedAuth::from_profile( &AuthProfile { id: "auth_01".into(), workspace_id: WorkspaceId::new("ws_default"), name: "api-query".to_owned(), kind: AuthKind::ApiKeyQuery, config: AuthConfig::ApiKeyQuery(ApiKeyQueryAuthConfig { param_name: "api_key".to_owned(), secret_id: SecretId::new("secret_api_key"), }), created_at: timestamp("2026-04-07T00:00:00Z"), updated_at: timestamp("2026-04-07T00:00:00Z"), }, &BTreeMap::from([(SecretId::new("secret_api_key"), json!({"value":"key-123"}))]), ) .unwrap(); let mut request = PreparedRequest::default(); auth.apply(&mut request); assert_eq!( request.query_params.get("api_key").map(String::as_str), Some("key-123") ); } }