#![allow(dead_code)]
use crate::core::api::ApiError;
use crate::models::auth::{
CredentialSource, CredentialStatus, SchemeType, SecurityRequirement, SelectedAuth,
SelectionReason, ValidationStatus,
};
use anyhow::{Context, Result};
use std::collections::{HashMap, HashSet};
use std::env;
use std::path::PathBuf;
pub struct AuthSelector {
available_credentials: HashMap<String, CredentialStatus>,
operation_requirements: Option<SecurityRequirement>,
global_requirements: Option<SecurityRequirement>,
preferred_scheme: Option<String>,
}
impl AuthSelector {
pub fn new() -> Self {
Self {
available_credentials: HashMap::new(),
operation_requirements: None,
global_requirements: None,
preferred_scheme: None,
}
}
pub fn with_operation_requirements(mut self, req: SecurityRequirement) -> Self {
self.operation_requirements = Some(req);
self
}
pub fn with_global_requirements(mut self, req: SecurityRequirement) -> Self {
self.global_requirements = Some(req);
self
}
pub fn with_preferred_scheme(mut self, scheme: String) -> Self {
self.preferred_scheme = Some(scheme);
self
}
pub fn discover_credentials(
&mut self,
schemes: &HashMap<String, crate::models::auth::SecuritySchemeDetails>,
) -> Result<()> {
for (name, details) in schemes {
let status = self.check_credential_status(name, details)?;
self.available_credentials.insert(name.clone(), status);
}
Ok(())
}
fn check_credential_status(
&self,
name: &str,
details: &crate::models::auth::SecuritySchemeDetails,
) -> Result<CredentialStatus> {
let (is_configured, source) = match details.scheme_type {
SchemeType::ApiKey => self.check_api_key(name),
SchemeType::Http => self.check_http_auth(name, details),
SchemeType::OAuth2 => self.check_oauth2(name),
SchemeType::OpenIdConnect => self.check_oidc(name),
SchemeType::MutualTls => self.check_mtls(name),
};
Ok(CredentialStatus {
scheme_name: name.to_string(),
is_configured,
source,
validation_status: ValidationStatus::NotValidated,
last_used: None,
})
}
fn check_api_key(&self, name: &str) -> (bool, CredentialSource) {
let env_vars = vec![
format!("{}_API_KEY", name.to_uppercase()),
format!("API_KEY"),
format!("{}_KEY", name.to_uppercase()),
];
for var in env_vars {
if env::var(&var).is_ok() {
return (true, CredentialSource::Environment(var));
}
}
let config_path = self.get_auth_config_path(name);
if config_path.exists() {
return (
true,
CredentialSource::ConfigFile(config_path.to_string_lossy().to_string()),
);
}
(false, CredentialSource::NotConfigured)
}
fn check_http_auth(
&self,
name: &str,
details: &crate::models::auth::SecuritySchemeDetails,
) -> (bool, CredentialSource) {
let is_bearer = details.bearer_format.is_some();
let env_vars = if is_bearer {
vec![
format!("{}_TOKEN", name.to_uppercase()),
format!("BEARER_TOKEN"),
format!("ACCESS_TOKEN"),
format!("AUTH_TOKEN"),
]
} else {
vec![
format!("{}_USERNAME", name.to_uppercase()),
format!("{}_PASSWORD", name.to_uppercase()),
format!("BASIC_AUTH"),
]
};
for var in &env_vars {
if env::var(var).is_ok() {
return (true, CredentialSource::Environment(var.clone()));
}
}
let profile_path = self.get_auth_profile_path(name);
if profile_path.exists() {
return (true, CredentialSource::Profile(name.to_string()));
}
(false, CredentialSource::NotConfigured)
}
fn check_oauth2(&self, name: &str) -> (bool, CredentialSource) {
let token_path = dirs::home_dir()
.map(|d| {
d.join(".mrapids")
.join("auth")
.join("tokens")
.join(format!("{}.json", name))
})
.unwrap_or_default();
if token_path.exists() {
return (true, CredentialSource::Profile(name.to_string()));
}
let client_id = format!("{}_CLIENT_ID", name.to_uppercase());
let client_secret = format!("{}_CLIENT_SECRET", name.to_uppercase());
if env::var(&client_id).is_ok() && env::var(&client_secret).is_ok() {
return (
true,
CredentialSource::Environment(format!("{}, {}", client_id, client_secret)),
);
}
(false, CredentialSource::NotConfigured)
}
fn check_oidc(&self, name: &str) -> (bool, CredentialSource) {
self.check_oauth2(name)
}
fn check_mtls(&self, name: &str) -> (bool, CredentialSource) {
let cert_vars = vec![
format!("{}_CLIENT_CERT", name.to_uppercase()),
format!("{}_CLIENT_KEY", name.to_uppercase()),
format!("CLIENT_CERT_PATH"),
format!("CLIENT_KEY_PATH"),
];
for var in &cert_vars {
if let Ok(path) = env::var(var) {
if PathBuf::from(&path).exists() {
return (true, CredentialSource::Environment(var.clone()));
}
}
}
(false, CredentialSource::NotConfigured)
}
pub fn select_best_scheme(&self) -> Result<SelectedAuth> {
if let Some(preferred) = &self.preferred_scheme {
if let Some(cred) = self.available_credentials.get(preferred) {
if cred.is_configured {
return self
.create_selected_auth(preferred, SelectionReason::ExplicitSelection);
} else {
return Err(ApiError::AuthError(format!(
"Preferred auth scheme '{}' is not configured",
preferred
))
.into());
}
} else {
return Err(
ApiError::AuthError(format!("Unknown auth scheme: {}", preferred)).into(),
);
}
}
let requirements = self
.operation_requirements
.as_ref()
.or(self.global_requirements.as_ref());
if let Some(req) = requirements {
if req.is_optional() {
return self.create_selected_auth("none", SelectionReason::OnlyOption);
}
if let Some(option) = req.simplest_option() {
let mut all_available = true;
let mut first_scheme = None;
for scheme_req in &option.schemes {
if first_scheme.is_none() {
first_scheme = Some(&scheme_req.name);
}
if let Some(cred) = self.available_credentials.get(&scheme_req.name) {
if !cred.is_configured {
all_available = false;
break;
}
} else {
all_available = false;
break;
}
}
if all_available {
if let Some(scheme_name) = first_scheme {
return self.create_selected_auth(
scheme_name,
SelectionReason::BestAvailable("Simplest valid option".to_string()),
);
}
}
}
for option in &req.options {
let mut all_available = true;
let mut schemes_needed = Vec::new();
for scheme_req in &option.schemes {
schemes_needed.push(scheme_req.name.clone());
if let Some(cred) = self.available_credentials.get(&scheme_req.name) {
if !cred.is_configured {
all_available = false;
break;
}
} else {
all_available = false;
break;
}
}
if all_available && !schemes_needed.is_empty() {
return self.create_selected_auth(
&schemes_needed[0],
SelectionReason::BestAvailable(format!(
"First available option with {} scheme(s)",
schemes_needed.len()
)),
);
}
}
return Err(ApiError::AuthError(
"No valid authentication credentials available for this operation".to_string(),
)
.into());
}
for (name, cred) in &self.available_credentials {
if cred.is_configured {
return self.create_selected_auth(name, SelectionReason::Default);
}
}
self.create_selected_auth("none", SelectionReason::OnlyOption)
}
fn create_selected_auth(
&self,
scheme_name: &str,
reason: SelectionReason,
) -> Result<SelectedAuth> {
let cred = self
.available_credentials
.get(scheme_name)
.context("Auth scheme not found")?;
let credentials = HashMap::new();
Ok(SelectedAuth {
scheme_name: scheme_name.to_string(),
scheme_type: SchemeType::Http, credentials,
source: cred.source.clone(),
reason,
})
}
pub fn validate_selection(&self, scheme: &str) -> Result<()> {
let requirements = self
.operation_requirements
.as_ref()
.or(self.global_requirements.as_ref());
if let Some(req) = requirements {
for option in &req.options {
let schemes_in_option: HashSet<String> =
option.schemes.iter().map(|s| s.name.clone()).collect();
if schemes_in_option.contains(scheme) {
let mut all_available = true;
for required_scheme in &schemes_in_option {
if let Some(cred) = self.available_credentials.get(required_scheme) {
if !cred.is_configured {
all_available = false;
break;
}
} else {
all_available = false;
break;
}
}
if all_available {
return Ok(());
} else {
return Err(ApiError::AuthError(format!(
"Auth scheme '{}' requires additional schemes to be configured",
scheme
))
.into());
}
}
}
return Err(ApiError::AuthError(format!(
"Auth scheme '{}' does not satisfy the security requirements",
scheme
))
.into());
}
if let Some(cred) = self.available_credentials.get(scheme) {
if cred.is_configured {
Ok(())
} else {
Err(
ApiError::AuthError(format!("Auth scheme '{}' is not configured", scheme))
.into(),
)
}
} else {
Err(ApiError::AuthError(format!("Unknown auth scheme: {}", scheme)).into())
}
}
fn get_auth_config_path(&self, name: &str) -> PathBuf {
dirs::home_dir()
.map(|d| {
d.join(".mrapids")
.join("auth")
.join(format!("{}.yaml", name))
})
.unwrap_or_else(|| {
PathBuf::from(".mrapids")
.join("auth")
.join(format!("{}.yaml", name))
})
}
fn get_auth_profile_path(&self, name: &str) -> PathBuf {
PathBuf::from(".mrapids")
.join("auth")
.join(format!("{}.toml", name))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::auth::{RequirementOption, SchemeRequirement};
#[test]
fn test_select_with_preference() {
let mut selector = AuthSelector::new().with_preferred_scheme("api_key".to_string());
selector.available_credentials.insert(
"api_key".to_string(),
CredentialStatus {
scheme_name: "api_key".to_string(),
is_configured: true,
source: CredentialSource::Environment("API_KEY".to_string()),
validation_status: ValidationStatus::NotValidated,
last_used: None,
},
);
let selected = selector.select_best_scheme().unwrap();
assert_eq!(selected.scheme_name, "api_key");
matches!(selected.reason, SelectionReason::ExplicitSelection);
}
#[test]
fn test_select_simplest_option() {
let mut selector = AuthSelector::new();
let mut req = SecurityRequirement::default();
req.options.push(RequirementOption {
schemes: vec![SchemeRequirement {
name: "api_key".to_string(),
scopes: vec![],
}],
});
req.options.push(RequirementOption {
schemes: vec![
SchemeRequirement {
name: "oauth2".to_string(),
scopes: vec!["read".to_string()],
},
SchemeRequirement {
name: "basic".to_string(),
scopes: vec![],
},
],
});
selector = selector.with_operation_requirements(req);
selector.available_credentials.insert(
"api_key".to_string(),
CredentialStatus {
scheme_name: "api_key".to_string(),
is_configured: true,
source: CredentialSource::Environment("API_KEY".to_string()),
validation_status: ValidationStatus::NotValidated,
last_used: None,
},
);
let selected = selector.select_best_scheme().unwrap();
assert_eq!(selected.scheme_name, "api_key");
}
}