chore: publish clean community baseline
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
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")
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user