246 lines
8.4 KiB
Rust
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")
|
|
);
|
|
}
|
|
}
|