use chrono::{DateTime, Utc};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use fakecloud_core::multi_account::{AccountState, MultiAccountState};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IamUser {
pub user_name: String,
pub user_id: String,
pub arn: String,
pub path: String,
pub created_at: DateTime<Utc>,
pub tags: Vec<Tag>,
pub permissions_boundary: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IamAccessKey {
pub access_key_id: String,
pub secret_access_key: String,
pub user_name: String,
pub status: String,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IamRole {
pub role_name: String,
pub role_id: String,
pub arn: String,
pub path: String,
pub assume_role_policy_document: String,
pub created_at: DateTime<Utc>,
pub description: Option<String>,
pub max_session_duration: i32,
pub tags: Vec<Tag>,
pub permissions_boundary: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IamPolicy {
pub policy_name: String,
pub policy_id: String,
pub arn: String,
pub path: String,
pub description: String,
pub created_at: DateTime<Utc>,
pub tags: Vec<Tag>,
pub default_version_id: String,
pub versions: Vec<PolicyVersion>,
pub next_version_num: u32,
pub attachment_count: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyVersion {
pub version_id: String,
pub document: String,
pub is_default: bool,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IamGroup {
pub group_name: String,
pub group_id: String,
pub arn: String,
pub path: String,
pub created_at: DateTime<Utc>,
pub members: Vec<String>, pub inline_policies: BTreeMap<String, String>, pub attached_policies: Vec<String>, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IamInstanceProfile {
pub instance_profile_name: String,
pub instance_profile_id: String,
pub arn: String,
pub path: String,
pub created_at: DateTime<Utc>,
pub roles: Vec<String>, pub tags: Vec<Tag>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tag {
pub key: String,
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoginProfile {
pub user_name: String,
pub created_at: DateTime<Utc>,
pub password_reset_required: bool,
#[serde(default)]
pub password: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SamlProvider {
pub arn: String,
pub name: String,
pub saml_metadata_document: String,
pub created_at: DateTime<Utc>,
pub valid_until: DateTime<Utc>,
pub tags: Vec<Tag>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OidcProvider {
pub arn: String,
pub url: String,
pub client_id_list: Vec<String>,
pub thumbprint_list: Vec<String>,
pub created_at: DateTime<Utc>,
pub tags: Vec<Tag>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerCertificate {
pub server_certificate_name: String,
pub server_certificate_id: String,
pub arn: String,
pub path: String,
pub certificate_body: String,
pub certificate_chain: Option<String>,
pub upload_date: DateTime<Utc>,
pub expiration: DateTime<Utc>,
pub tags: Vec<Tag>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SigningCertificate {
pub certificate_id: String,
pub user_name: String,
pub certificate_body: String,
pub status: String,
pub upload_date: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountPasswordPolicy {
pub minimum_password_length: u32,
pub require_symbols: bool,
pub require_numbers: bool,
pub require_uppercase_characters: bool,
pub require_lowercase_characters: bool,
pub allow_users_to_change_password: bool,
pub max_password_age: u32,
pub password_reuse_prevention: u32,
pub hard_expiry: bool,
}
impl Default for AccountPasswordPolicy {
fn default() -> Self {
Self {
minimum_password_length: 6,
require_symbols: false,
require_numbers: false,
require_uppercase_characters: false,
require_lowercase_characters: false,
allow_users_to_change_password: false,
max_password_age: 0,
password_reuse_prevention: 0,
hard_expiry: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VirtualMfaDevice {
pub serial_number: String,
pub base32_string_seed: String,
pub qr_code_png: String,
pub enable_date: Option<DateTime<Utc>>,
pub user: Option<String>,
pub tags: Vec<Tag>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceLinkedRoleDeletion {
pub deletion_task_id: String,
pub status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CredentialIdentity {
pub arn: String,
pub user_id: String,
pub account_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StsTempCredential {
pub access_key_id: String,
pub secret_access_key: String,
pub session_token: String,
pub principal_arn: String,
pub user_id: String,
pub account_id: String,
pub expiration: DateTime<Utc>,
#[serde(default)]
pub session_policies: Vec<String>,
#[serde(default)]
pub mfa_present: bool,
#[serde(default = "default_issued_at")]
pub issued_at: DateTime<Utc>,
#[serde(default)]
pub federated_provider: Option<String>,
}
fn default_issued_at() -> DateTime<Utc> {
Utc::now()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SecretLookup {
pub secret_access_key: String,
pub session_token: Option<String>,
pub principal_arn: String,
pub user_id: String,
pub account_id: String,
pub session_policies: Vec<String>,
pub principal_tags: Option<BTreeMap<String, String>>,
pub mfa_present: bool,
pub token_issued_at: Option<DateTime<Utc>>,
pub federated_provider: Option<String>,
}
pub fn tags_to_hashmap(tags: &[Tag]) -> Option<BTreeMap<String, String>> {
if tags.is_empty() {
return None;
}
Some(
tags.iter()
.map(|t| (t.key.clone(), t.value.clone()))
.collect(),
)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SshPublicKey {
pub ssh_public_key_id: String,
pub user_name: String,
pub ssh_public_key_body: String,
pub status: String,
pub upload_date: DateTime<Utc>,
pub fingerprint: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccessKeyLastUsed {
pub last_used_date: DateTime<Utc>,
pub service_name: String,
pub region: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IamState {
pub account_id: String,
pub users: BTreeMap<String, IamUser>,
pub access_keys: BTreeMap<String, Vec<IamAccessKey>>, pub roles: BTreeMap<String, IamRole>,
pub policies: BTreeMap<String, IamPolicy>, pub role_policies: BTreeMap<String, Vec<String>>, pub role_inline_policies: BTreeMap<String, BTreeMap<String, String>>, pub user_policies: BTreeMap<String, Vec<String>>, pub user_inline_policies: BTreeMap<String, BTreeMap<String, String>>, pub groups: BTreeMap<String, IamGroup>,
pub instance_profiles: BTreeMap<String, IamInstanceProfile>,
pub login_profiles: BTreeMap<String, LoginProfile>,
pub saml_providers: BTreeMap<String, SamlProvider>, pub oidc_providers: BTreeMap<String, OidcProvider>, pub server_certificates: BTreeMap<String, ServerCertificate>, pub signing_certificates: BTreeMap<String, Vec<SigningCertificate>>, pub account_aliases: Vec<String>,
pub account_password_policy: Option<AccountPasswordPolicy>,
pub virtual_mfa_devices: BTreeMap<String, VirtualMfaDevice>, pub service_linked_role_deletions: BTreeMap<String, ServiceLinkedRoleDeletion>,
pub credential_identities: BTreeMap<String, CredentialIdentity>,
pub sts_temp_credentials: BTreeMap<String, StsTempCredential>,
pub credential_report_generated: bool,
pub ssh_public_keys: BTreeMap<String, Vec<SshPublicKey>>, pub access_key_last_used: BTreeMap<String, AccessKeyLastUsed>,
#[serde(default)]
pub service_specific_credentials: BTreeMap<String, Vec<ServiceSpecificCredential>>, #[serde(default)]
pub extra_tags: BTreeMap<String, Vec<(String, String)>>,
#[serde(default)]
pub organizations_root_credentials_management: bool,
#[serde(default)]
pub organizations_root_sessions: bool,
#[serde(default)]
pub service_last_accessed_jobs: BTreeMap<String, ServiceLastAccessedJob>,
#[serde(default)]
pub organizations_access_reports: BTreeMap<String, OrganizationsAccessReport>,
#[serde(default)]
pub global_endpoint_token_version: Option<String>,
#[serde(default)]
pub delegation_requests: BTreeMap<String, DelegationRequest>,
#[serde(default)]
pub outbound_web_identity_federation_enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DelegationRequest {
pub id: String,
pub owner_account_id: Option<String>,
pub description: String,
pub request_message: Option<String>,
pub requestor_workflow_id: String,
pub redirect_url: Option<String>,
pub notification_channel: String,
pub session_duration: i64,
pub only_send_by_owner: bool,
pub status: String,
pub notes: Option<String>,
pub created_at: DateTime<Utc>,
pub policy_template_arn: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceSpecificCredential {
pub credential_id: String,
pub user_name: String,
pub service_name: String,
pub service_user_name: String,
pub service_password: String,
pub status: String,
pub create_date: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceLastAccessedJob {
pub job_id: String,
pub status: String,
pub job_creation_date: DateTime<Utc>,
pub arn: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrganizationsAccessReport {
pub job_id: String,
pub status: String,
pub created_at: DateTime<Utc>,
pub entity_path: String,
}
impl IamState {
pub fn new(account_id: &str) -> Self {
Self {
account_id: account_id.to_string(),
users: BTreeMap::new(),
access_keys: BTreeMap::new(),
roles: BTreeMap::new(),
policies: BTreeMap::new(),
role_policies: BTreeMap::new(),
role_inline_policies: BTreeMap::new(),
user_policies: BTreeMap::new(),
user_inline_policies: BTreeMap::new(),
groups: BTreeMap::new(),
instance_profiles: BTreeMap::new(),
login_profiles: BTreeMap::new(),
saml_providers: BTreeMap::new(),
oidc_providers: BTreeMap::new(),
server_certificates: BTreeMap::new(),
signing_certificates: BTreeMap::new(),
account_aliases: Vec::new(),
account_password_policy: None,
virtual_mfa_devices: BTreeMap::new(),
service_linked_role_deletions: BTreeMap::new(),
credential_identities: BTreeMap::new(),
sts_temp_credentials: BTreeMap::new(),
credential_report_generated: false,
ssh_public_keys: BTreeMap::new(),
access_key_last_used: BTreeMap::new(),
service_specific_credentials: BTreeMap::new(),
extra_tags: BTreeMap::new(),
organizations_root_credentials_management: false,
organizations_root_sessions: false,
service_last_accessed_jobs: BTreeMap::new(),
organizations_access_reports: BTreeMap::new(),
global_endpoint_token_version: None,
delegation_requests: BTreeMap::new(),
outbound_web_identity_federation_enabled: false,
}
}
pub fn reset(&mut self) {
let account_id = self.account_id.clone();
*self = Self::new(&account_id);
}
pub fn credential_secret(&mut self, access_key_id: &str) -> Option<SecretLookup> {
for keys in self.access_keys.values() {
for key in keys {
if key.access_key_id == access_key_id && key.status == "Active" {
if let Some(user) = self.users.get(&key.user_name) {
return Some(SecretLookup {
secret_access_key: key.secret_access_key.clone(),
session_token: None,
principal_arn: user.arn.clone(),
user_id: user.user_id.clone(),
account_id: self.account_id.clone(),
session_policies: Vec::new(),
principal_tags: tags_to_hashmap(&user.tags),
mfa_present: false,
token_issued_at: None,
federated_provider: None,
});
}
}
}
}
let now = Utc::now();
if let Some(temp) = self.sts_temp_credentials.get(access_key_id) {
if temp.expiration > now {
let principal_tags = self.resolve_role_tags(&temp.principal_arn);
return Some(SecretLookup {
secret_access_key: temp.secret_access_key.clone(),
session_token: Some(temp.session_token.clone()),
principal_arn: temp.principal_arn.clone(),
user_id: temp.user_id.clone(),
account_id: temp.account_id.clone(),
session_policies: temp.session_policies.clone(),
principal_tags,
mfa_present: temp.mfa_present,
token_issued_at: Some(temp.issued_at),
federated_provider: temp.federated_provider.clone(),
});
}
self.sts_temp_credentials.remove(access_key_id);
}
None
}
pub fn credential_secret_readonly(&self, access_key_id: &str) -> Option<SecretLookup> {
for keys in self.access_keys.values() {
for key in keys {
if key.access_key_id == access_key_id && key.status == "Active" {
if let Some(user) = self.users.get(&key.user_name) {
return Some(SecretLookup {
secret_access_key: key.secret_access_key.clone(),
session_token: None,
principal_arn: user.arn.clone(),
user_id: user.user_id.clone(),
account_id: self.account_id.clone(),
session_policies: Vec::new(),
principal_tags: tags_to_hashmap(&user.tags),
mfa_present: false,
token_issued_at: None,
federated_provider: None,
});
}
}
}
}
let now = Utc::now();
let temp = self.sts_temp_credentials.get(access_key_id)?;
if temp.expiration <= now {
return None;
}
let principal_tags = self.resolve_role_tags(&temp.principal_arn);
Some(SecretLookup {
secret_access_key: temp.secret_access_key.clone(),
session_token: Some(temp.session_token.clone()),
principal_arn: temp.principal_arn.clone(),
user_id: temp.user_id.clone(),
account_id: temp.account_id.clone(),
session_policies: temp.session_policies.clone(),
principal_tags,
mfa_present: temp.mfa_present,
token_issued_at: Some(temp.issued_at),
federated_provider: temp.federated_provider.clone(),
})
}
fn resolve_role_tags(&self, principal_arn: &str) -> Option<BTreeMap<String, String>> {
let parts: Vec<&str> = principal_arn.split(':').collect();
if parts.len() < 6 {
return None;
}
let resource = parts[5];
if let Some(rest) = resource.strip_prefix("assumed-role/") {
let role_name = rest.split('/').next()?;
let role = self.roles.get(role_name)?;
return tags_to_hashmap(&role.tags);
}
None
}
}
impl AccountState for IamState {
fn new_for_account(account_id: &str, _region: &str, _endpoint: &str) -> Self {
Self::new(account_id)
}
}
pub type SharedIamState = std::sync::Arc<RwLock<MultiAccountState<IamState>>>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IamSnapshot {
pub schema_version: u32,
#[serde(default)]
pub accounts: Option<MultiAccountState<IamState>>,
#[serde(default)]
pub state: Option<IamState>,
}
pub const IAM_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
#[cfg(test)]
mod tests {
use super::*;
use fakecloud_aws::arn::Arn;
fn iam_user(name: &str, account_id: &str) -> IamUser {
IamUser {
user_name: name.to_string(),
user_id: format!("AIDA{}", name.to_uppercase()),
arn: Arn::global("iam", account_id, &format!("user/{name}")).to_string(),
path: "/".to_string(),
created_at: Utc::now(),
tags: Vec::new(),
permissions_boundary: None,
}
}
fn iam_key(user: &str, akid: &str, secret: &str) -> IamAccessKey {
IamAccessKey {
access_key_id: akid.to_string(),
secret_access_key: secret.to_string(),
user_name: user.to_string(),
status: "Active".to_string(),
created_at: Utc::now(),
}
}
#[test]
fn credential_secret_returns_iam_user_key() {
let mut state = IamState::new("123456789012");
state
.users
.insert("alice".to_string(), iam_user("alice", "123456789012"));
state.access_keys.insert(
"alice".to_string(),
vec![iam_key("alice", "FKIAALICE", "secret-alice")],
);
let lookup = state.credential_secret("FKIAALICE").unwrap();
assert_eq!(lookup.secret_access_key, "secret-alice");
assert_eq!(lookup.principal_arn, "arn:aws:iam::123456789012:user/alice");
assert_eq!(lookup.account_id, "123456789012");
assert_eq!(lookup.session_token, None);
}
#[test]
fn credential_secret_returns_sts_temp_credential_when_unexpired() {
let mut state = IamState::new("123456789012");
state.sts_temp_credentials.insert(
"FSIATEMPKEY".to_string(),
StsTempCredential {
access_key_id: "FSIATEMPKEY".to_string(),
secret_access_key: "temp-secret".to_string(),
session_token: "temp-token".to_string(),
principal_arn: "arn:aws:sts::123456789012:assumed-role/R/s".to_string(),
user_id: "AROA:session".to_string(),
account_id: "123456789012".to_string(),
expiration: Utc::now() + chrono::Duration::minutes(30),
session_policies: Vec::new(),
mfa_present: false,
issued_at: Utc::now(),
federated_provider: None,
},
);
let lookup = state.credential_secret("FSIATEMPKEY").unwrap();
assert_eq!(lookup.secret_access_key, "temp-secret");
assert_eq!(lookup.session_token.as_deref(), Some("temp-token"));
assert_eq!(
lookup.principal_arn,
"arn:aws:sts::123456789012:assumed-role/R/s"
);
}
#[test]
fn credential_secret_purges_expired_sts_credentials() {
let mut state = IamState::new("123456789012");
state.sts_temp_credentials.insert(
"FSIAOLD".to_string(),
StsTempCredential {
access_key_id: "FSIAOLD".to_string(),
secret_access_key: "s".to_string(),
session_token: "t".to_string(),
principal_arn: "arn".to_string(),
user_id: "id".to_string(),
account_id: "123456789012".to_string(),
expiration: Utc::now() - chrono::Duration::seconds(1),
session_policies: Vec::new(),
mfa_present: false,
issued_at: Utc::now(),
federated_provider: None,
},
);
assert!(state.credential_secret("FSIAOLD").is_none());
assert!(!state.sts_temp_credentials.contains_key("FSIAOLD"));
}
#[test]
fn credential_secret_readonly_does_not_purge() {
let mut state = IamState::new("123456789012");
state.sts_temp_credentials.insert(
"FSIAOLD".to_string(),
StsTempCredential {
access_key_id: "FSIAOLD".to_string(),
secret_access_key: "s".to_string(),
session_token: "t".to_string(),
principal_arn: "arn".to_string(),
user_id: "id".to_string(),
account_id: "123456789012".to_string(),
expiration: Utc::now() - chrono::Duration::seconds(1),
session_policies: Vec::new(),
mfa_present: false,
issued_at: Utc::now(),
federated_provider: None,
},
);
assert!(state.credential_secret_readonly("FSIAOLD").is_none());
assert!(state.sts_temp_credentials.contains_key("FSIAOLD"));
}
#[test]
fn credential_secret_returns_none_for_unknown_akid() {
let mut state = IamState::new("123456789012");
assert!(state.credential_secret("FKIAUNKNOWN").is_none());
}
#[test]
fn credential_secret_skips_inactive_key() {
let mut state = IamState::new("123456789012");
let mut key = iam_key("alice", "FKIAALICE", "secret123");
key.status = "Inactive".to_string();
state.access_keys.insert("alice".to_string(), vec![key]);
assert!(state.credential_secret("FKIAALICE").is_none());
}
}