use crate::store::traits::{AccessKeyStore, UserStore};
use crate::wami::identity::root_user::ROOT_USER_NAME;
use crate::wami::identity::User;
use std::sync::Arc;
use tokio::sync::RwLock;
use wami_core::arn::TenantPath;
use wami_core::context::WamiContext;
use wami_core::error::{AmiError, Result};
pub struct AuthenticationService<S>
where
S: AccessKeyStore + UserStore + Send + Sync,
{
store: Arc<RwLock<S>>,
}
impl<S> AuthenticationService<S>
where
S: AccessKeyStore + UserStore + Send + Sync,
{
pub fn new(store: Arc<RwLock<S>>) -> Self {
Self { store }
}
pub async fn authenticate(
&self,
access_key_id: &str,
secret_access_key: &str,
) -> Result<WamiContext> {
let user = self
.validate_access_key(access_key_id, secret_access_key)
.await?;
self.create_context_from_user(&user).await
}
async fn validate_access_key(
&self,
access_key_id: &str,
secret_access_key: &str,
) -> Result<User> {
let store = self.store.read().await;
let access_key = store.get_access_key(access_key_id).await?.ok_or_else(|| {
AmiError::InvalidParameter {
message: "Invalid access key ID or secret".to_string(),
}
})?;
if access_key.status.to_lowercase() != "active" {
return Err(AmiError::InvalidParameter {
message: "Access key is not active".to_string(),
});
}
let secret_matches = if let Some(stored_secret) = &access_key.secret_access_key {
if stored_secret.starts_with("$2") {
bcrypt::verify(secret_access_key, stored_secret).unwrap_or(false)
} else {
constant_time_compare(secret_access_key.as_bytes(), stored_secret.as_bytes())
}
} else {
false
};
if !secret_matches {
return Err(AmiError::InvalidParameter {
message: "Invalid access key ID or secret".to_string(),
});
}
let user = store
.get_user(&access_key.user_name)
.await?
.ok_or_else(|| AmiError::ResourceNotFound {
resource: format!("User {}", access_key.user_name),
})?;
Ok(user)
}
async fn create_context_from_user(&self, user: &User) -> Result<WamiContext> {
let arn = &user.wami_arn;
WamiContext::builder().caller_arn(arn.clone()).build()
}
pub async fn authenticate_root(
&self,
instance_id: &str,
access_key_id: &str,
secret_access_key: &str,
) -> Result<WamiContext> {
let user = self
.validate_access_key(access_key_id, secret_access_key)
.await?;
if user.user_name != ROOT_USER_NAME {
return Err(AmiError::AccessDenied {
message: "Not a root user".to_string(),
});
}
if user.wami_arn.wami_instance_id != instance_id {
return Err(AmiError::AccessDenied {
message: "Instance ID mismatch".to_string(),
});
}
WamiContext::builder()
.instance_id(instance_id)
.tenant_path(TenantPath::single(
crate::wami::identity::root_user::ROOT_TENANT_ID,
))
.caller_arn(user.wami_arn.clone())
.is_root(true)
.build()
}
}
fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut result = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
result |= x ^ y;
}
result == 0
}
pub fn hash_secret(secret: &str) -> Result<String> {
bcrypt::hash(secret, bcrypt::DEFAULT_COST)
.map_err(|e| AmiError::StoreError(format!("Failed to hash secret: {}", e)))
}
pub fn verify_secret(secret: &str, hash: &str) -> Result<bool> {
bcrypt::verify(secret, hash)
.map_err(|e| AmiError::StoreError(format!("Failed to verify secret: {}", e)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_constant_time_compare() {
assert!(constant_time_compare(b"hello", b"hello"));
assert!(!constant_time_compare(b"hello", b"world"));
assert!(!constant_time_compare(b"hello", b"hello!"));
}
#[test]
fn test_hash_and_verify_secret() {
let secret = "my-super-secret-key";
let hash = hash_secret(secret).unwrap();
assert!(hash.starts_with("$2")); assert!(verify_secret(secret, &hash).unwrap());
assert!(!verify_secret("wrong-secret", &hash).unwrap());
}
}