Files
crank/crates/crank-runtime/src/auth.rs
T
2026-04-19 21:25:28 +00:00

246 lines
8.4 KiB
Rust

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<SecretId, Value>,
) -> Result<Self, RuntimeError> {
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());
}
}
}
}
fn extract_secret_string(
secrets: &BTreeMap<SecretId, Value>,
secret_id: &SecretId,
preferred_fields: &[&str],
) -> Result<String, RuntimeError> {
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")
);
}
}