pub mod arn_builder;
pub mod aws;
pub mod azure;
pub mod custom;
pub mod gcp;
pub mod provider_info;
use crate::error::Result;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProviderConfig {
pub provider_name: String,
pub account_id: String,
pub native_arn: String,
pub synced_at: chrono::DateTime<chrono::Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tenant_id: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResourceType {
User,
Group,
Role,
Policy,
AccessKey,
ServerCertificate,
ServiceCredential,
ServiceLinkedRole,
MfaDevice,
SigningCertificate,
SamlProvider,
OidcProvider,
StsAssumedRole,
StsFederatedUser,
StsSession,
Tenant,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceLimits {
pub max_access_keys_per_user: usize,
pub max_signing_certificates_per_user: usize,
pub max_service_credentials_per_user_per_service: usize,
pub max_tags_per_resource: usize,
pub max_mfa_devices_per_user: usize,
pub session_duration_min: i32,
pub session_duration_max: i32,
}
impl Default for ResourceLimits {
fn default() -> Self {
Self {
max_access_keys_per_user: 2,
max_signing_certificates_per_user: 2,
max_service_credentials_per_user_per_service: 2,
max_tags_per_resource: 50,
max_mfa_devices_per_user: 8,
session_duration_min: 3600, session_duration_max: 43200, }
}
}
pub trait CloudProvider: Send + Sync + std::fmt::Debug {
fn name(&self) -> &str;
fn generate_resource_identifier(
&self,
resource_type: ResourceType,
account_id: &str,
path: &str,
name: &str,
) -> String;
fn generate_resource_id(&self, resource_type: ResourceType) -> String;
fn resource_limits(&self) -> &ResourceLimits;
#[allow(clippy::result_large_err)]
fn validate_service_name(&self, service: &str) -> Result<()>;
#[allow(clippy::result_large_err)]
fn validate_path(&self, path: &str) -> Result<()>;
#[allow(clippy::result_large_err)]
fn validate_session_duration(&self, duration: i32) -> Result<()> {
let limits = self.resource_limits();
if duration < limits.session_duration_min || duration > limits.session_duration_max {
return Err(crate::error::AmiError::InvalidParameter {
message: format!(
"Session duration must be between {} and {} seconds",
limits.session_duration_min, limits.session_duration_max
),
});
}
Ok(())
}
fn generate_service_linked_role_name(
&self,
service_name: &str,
custom_suffix: Option<&str>,
) -> String;
fn generate_service_linked_role_path(&self, service_name: &str) -> String;
fn generate_wami_arn(
&self,
resource_type: ResourceType,
account_id: &str,
path: &str,
name: &str,
) -> String {
let service = match resource_type {
ResourceType::User
| ResourceType::Group
| ResourceType::Role
| ResourceType::Policy
| ResourceType::AccessKey
| ResourceType::MfaDevice
| ResourceType::ServiceLinkedRole
| ResourceType::ServiceCredential
| ResourceType::SigningCertificate
| ResourceType::ServerCertificate
| ResourceType::SamlProvider
| ResourceType::OidcProvider => "iam",
ResourceType::StsAssumedRole
| ResourceType::StsFederatedUser
| ResourceType::StsSession => "sts",
ResourceType::Tenant => "organizations",
};
let resource_prefix = match resource_type {
ResourceType::User => "user",
ResourceType::Group => "group",
ResourceType::Role => "role",
ResourceType::Policy => "policy",
ResourceType::ServerCertificate => "server-certificate",
ResourceType::AccessKey => "access-key",
ResourceType::ServiceCredential => "service-credential",
ResourceType::ServiceLinkedRole => "role",
ResourceType::MfaDevice => "mfa",
ResourceType::SigningCertificate => "signing-certificate",
ResourceType::SamlProvider => "saml-provider",
ResourceType::OidcProvider => "oidc-provider",
ResourceType::StsAssumedRole => "assumed-role",
ResourceType::StsFederatedUser => "federated-user",
ResourceType::StsSession => "session",
ResourceType::Tenant => "ou",
};
let normalized_path = if path.is_empty() || path == "/" {
String::new()
} else {
let mut p = path.to_string();
if !p.starts_with('/') {
p.insert(0, '/');
}
if !p.ends_with('/') {
p.push('/');
}
p[1..].to_string()
};
if normalized_path.is_empty() {
format!(
"arn:wami:{}::{}:{}/{}",
service, account_id, resource_prefix, name
)
} else {
format!(
"arn:wami:{}::{}:{}/{}{}",
service, account_id, resource_prefix, normalized_path, name
)
}
}
}
impl dyn CloudProvider {
pub fn tenant_aware_path(tenant_id: Option<&str>, base_path: &str) -> String {
match tenant_id {
Some(tid) if !tid.is_empty() => {
let normalized_base = base_path.trim_end_matches('/');
format!("{}/tenants/{}/", normalized_base, tid)
}
_ => base_path.to_string(),
}
}
pub fn extract_tenant_from_path(path: &str) -> Option<String> {
if path.contains("/tenants/") {
let parts: Vec<&str> = path.split("/tenants/").collect();
if parts.len() > 1 {
let tenant_part = parts[1].trim_end_matches('/');
if !tenant_part.is_empty() {
return Some(tenant_part.to_string());
}
}
}
None
}
}
pub use aws::AwsProvider;
pub use azure::AzureProvider;
pub use custom::CustomProvider;
pub use gcp::GcpProvider;