// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use crate::error::{Error, Result, is_err_config_not_found}; use crate::sys::get_claims_from_token_with_secret; use crate::{ cache::{Cache, CacheEntity}, error::{Error as IamError, is_err_no_such_group, is_err_no_such_policy, is_err_no_such_user}, store::{GroupInfo, MappedPolicy, Store, UserType, object::IAM_CONFIG_PREFIX}, sys::{ MAX_SVCSESSION_POLICY_SIZE, SESSION_POLICY_NAME, SESSION_POLICY_NAME_EXTRACTED, STATUS_DISABLED, STATUS_ENABLED, UpdateServiceAccountOpts, }, }; use futures::future::join_all; use rustfs_ecstore::global::get_global_action_cred; use rustfs_madmin::{AccountStatus, AddOrUpdateUserReq, GroupDesc}; use rustfs_policy::{ arn::ARN, auth::{self, Credentials, UserIdentity, is_secret_key_valid, jwt_sign}, format::Format, policy::{ EMBEDDED_POLICY_TYPE, INHERITED_POLICY_TYPE, Policy, PolicyDoc, default::DEFAULT_POLICIES, iam_policy_claim_name_sa, }, }; use rustfs_utils::path::path_join_buf; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::sync::atomic::AtomicU8; use std::{ collections::{HashMap, HashSet}, sync::{ Arc, atomic::{AtomicBool, AtomicI64, Ordering}, }, time::Duration, }; use time::OffsetDateTime; use tokio::{ select, sync::{ mpsc, mpsc::{Receiver, Sender}, }, }; use tracing::warn; use tracing::{error, info}; const IAM_FORMAT_FILE: &str = "format.json"; const IAM_FORMAT_VERSION_1: i32 = 1; #[derive(Serialize, Deserialize)] struct IAMFormat { version: i32, } impl IAMFormat { fn new_version_1() -> Self { IAMFormat { version: IAM_FORMAT_VERSION_1, } } } fn get_iam_format_file_path() -> String { path_join_buf(&[&IAM_CONFIG_PREFIX, IAM_FORMAT_FILE]) } #[repr(u8)] #[derive(Debug, PartialEq)] pub enum IamState { Uninitialized = 0, Loading = 1, Ready = 2, Error = 3, } pub struct IamCache { pub cache: Cache, pub api: T, pub state: Arc, pub loading: Arc, pub roles: HashMap>, pub send_chan: Sender, pub last_timestamp: AtomicI64, } impl IamCache where T: Store, { /// Create a new IAM system instance /// # Arguments /// * `api` - The storage backend implementing the Store trait /// /// # Returns /// An Arc-wrapped instance of IamSystem pub(crate) async fn new(api: T) -> Arc { let (sender, receiver) = mpsc::channel::(100); let sys = Arc::new(Self { api, cache: Cache::default(), state: Arc::new(AtomicU8::new(IamState::Uninitialized as u8)), loading: Arc::new(AtomicBool::new(false)), send_chan: sender, roles: HashMap::new(), last_timestamp: AtomicI64::new(0), }); sys.clone().init(receiver).await.unwrap(); sys } /// Initialize the IAM system async fn init(self: Arc, receiver: Receiver) -> Result<()> { self.state.store(IamState::Loading as u8, Ordering::SeqCst); // Ensure the IAM format file is persisted first self.clone().save_iam_formatter().await?; // Critical: Load all existing users/policies into memory cache const MAX_RETRIES: usize = 3; for attempt in 0..MAX_RETRIES { if let Err(e) = self.clone().load().await { if attempt == MAX_RETRIES - 1 { self.state.store(IamState::Error as u8, Ordering::SeqCst); error!("IAM fail to load initial data after {} attempts: {:?}", MAX_RETRIES, e); return Err(e); } else { warn!("IAM load failed, retrying... attempt {}", attempt + 1); tokio::time::sleep(Duration::from_secs(1)).await; } } else { break; } } self.state.store(IamState::Ready as u8, Ordering::SeqCst); info!("IAM System successfully initialized and marked as READY"); // Background ticker for synchronization // Check if environment variable is set let skip_background_task = std::env::var("RUSTFS_SKIP_BACKGROUND_TASK").is_ok(); if !skip_background_task { // Background thread starts periodic updates or receives signal updates tokio::spawn({ let s = Arc::clone(&self); async move { let ticker = tokio::time::interval(Duration::from_secs(120)); tokio::pin!(ticker, receiver); loop { select! { _ = ticker.tick() => { info!("iam load ticker"); if let Err(err) =s.clone().load().await{ error!("iam load err {:?}", err); } }, i = receiver.recv() => { info!("iam load receiver"); match i { Some(t) => { let last = s.last_timestamp.load(Ordering::Relaxed); if last <= t { info!("iam load receiver load"); if let Err(err) =s.clone().load().await{ error!("iam load err {:?}", err); } ticker.reset(); } }, None => return, } } } } } }); } Ok(()) } /// Check if IAM system is ready pub fn is_ready(&self) -> bool { self.state.load(Ordering::SeqCst) == IamState::Ready as u8 } async fn _notify(&self) { self.send_chan.send(OffsetDateTime::now_utc().unix_timestamp()).await.unwrap(); } async fn load(self: Arc) -> Result<()> { // debug!("load iam to cache"); self.api.load_all(&self.cache).await?; self.last_timestamp .store(OffsetDateTime::now_utc().unix_timestamp(), Ordering::Relaxed); Ok(()) } pub async fn load_user(&self, access_key: &str) -> Result<()> { let mut users_map: HashMap = HashMap::new(); let mut user_policy_map = HashMap::new(); let mut sts_users_map = HashMap::new(); let mut sts_policy_map = HashMap::new(); let mut policy_docs_map = HashMap::new(); let _ = self.api.load_user(access_key, UserType::Svc, &mut users_map).await; let parent_user = users_map.get(access_key).map(|svc| svc.credentials.parent_user.clone()); if let Some(parent_user) = parent_user { let _ = self.api.load_user(&parent_user, UserType::Reg, &mut users_map).await; let _ = self .api .load_mapped_policy(&parent_user, UserType::Reg, false, &mut user_policy_map) .await; } else { let _ = self.api.load_user(access_key, UserType::Reg, &mut users_map).await; if users_map.contains_key(access_key) { let _ = self .api .load_mapped_policy(access_key, UserType::Reg, false, &mut user_policy_map) .await; } let _ = self.api.load_user(access_key, UserType::Sts, &mut sts_users_map).await; let has_sts_user = sts_users_map.get(access_key); let sts_parent = has_sts_user.map(|sts| sts.credentials.parent_user.clone()); if let Some(parent) = sts_parent { let _ = self .api .load_mapped_policy(&parent, UserType::Sts, false, &mut sts_policy_map) .await; } let sts_user = has_sts_user.map(|sts| sts.credentials.access_key.clone()); if let Some(ref sts) = sts_user { if let Some(plc) = sts_policy_map.get(sts) { for p in plc.to_slice().iter() { if !policy_docs_map.contains_key(p) { let _ = self.api.load_policy_doc(p, &mut policy_docs_map).await; } } } } } if let Some(plc) = user_policy_map.get(access_key) { for p in plc.to_slice().iter() { if !policy_docs_map.contains_key(p) { let _ = self.api.load_policy_doc(p, &mut policy_docs_map).await; } } } if let Some(user) = users_map.get(access_key) { Cache::add_or_update(&self.cache.users, access_key, user, OffsetDateTime::now_utc()); } if let Some(user_policy) = user_policy_map.get(access_key) { Cache::add_or_update(&self.cache.user_policies, access_key, user_policy, OffsetDateTime::now_utc()); } if let Some(sts_user) = sts_users_map.get(access_key) { Cache::add_or_update(&self.cache.sts_accounts, access_key, sts_user, OffsetDateTime::now_utc()); } if let Some(sts_policy) = sts_policy_map.get(access_key) { Cache::add_or_update(&self.cache.sts_policies, access_key, sts_policy, OffsetDateTime::now_utc()); } if let Some(policy_doc) = policy_docs_map.get(access_key) { Cache::add_or_update(&self.cache.policy_docs, access_key, policy_doc, OffsetDateTime::now_utc()); } Ok(()) } // TODO: Check if exists, whether retry is possible #[tracing::instrument(level = "debug", skip(self))] async fn save_iam_formatter(self: Arc) -> Result<()> { let path = get_iam_format_file_path(); let iam_fmt: Format = match self.api.load_iam_config(&path).await { Ok(v) => v, Err(err) => { if !is_err_config_not_found(&err) { return Err(err); } Format::default() } }; if iam_fmt.version >= IAM_FORMAT_VERSION_1 { return Ok(()); } self.api.save_iam_config(IAMFormat::new_version_1(), path).await } pub async fn get_user(&self, access_key: &str) -> Option { self.cache .users .load() .get(access_key) .cloned() .or_else(|| self.cache.sts_accounts.load().get(access_key).cloned()) } pub async fn get_mapped_policy(&self, name: &str, is_group: bool) -> Option { if is_group { self.cache.group_policies.load().get(name).cloned() } else { self.cache.user_policies.load().get(name).cloned() } } pub async fn get_policy(&self, name: &str) -> Result { if name.is_empty() { return Err(Error::InvalidArgument); } let policies = MappedPolicy::new(name).to_slice(); let mut to_merge = Vec::new(); for policy in policies { if policy.is_empty() { continue; } let v = self .cache .policy_docs .load() .get(&policy) .cloned() .ok_or(Error::NoSuchPolicy)?; to_merge.push(v.policy); } if to_merge.is_empty() { return Err(Error::NoSuchPolicy); } Ok(Policy::merge_policies(to_merge)) } pub async fn get_policy_doc(&self, name: &str) -> Result { if name.is_empty() { return Err(Error::InvalidArgument); } self.cache.policy_docs.load().get(name).cloned().ok_or(Error::NoSuchPolicy) } pub async fn delete_policy(&self, name: &str, is_from_notify: bool) -> Result<()> { if name.is_empty() { return Err(Error::InvalidArgument); } if is_from_notify { let user_policy_cache = self.cache.user_policies.load(); let group_policy_cache = self.cache.group_policies.load(); let users_cache = self.cache.users.load(); let mut users = Vec::new(); user_policy_cache.iter().for_each(|(k, v)| { if !users_cache.contains_key(k) { Cache::delete(&self.cache.user_policies, k, OffsetDateTime::now_utc()); return; } if v.policy_set().contains(name) { users.push(k.to_owned()); } }); let mut groups = Vec::new(); group_policy_cache.iter().for_each(|(k, v)| { if v.policy_set().contains(name) { groups.push(k.to_owned()); } }); if !users.is_empty() || !groups.is_empty() { return Err(Error::PolicyInUse); } if let Err(err) = self.api.delete_policy_doc(name).await { if !is_err_no_such_policy(&err) { Cache::delete(&self.cache.policy_docs, name, OffsetDateTime::now_utc()); return Ok(()); } return Err(err); } } Cache::delete(&self.cache.policy_docs, name, OffsetDateTime::now_utc()); Ok(()) } pub async fn set_policy(&self, name: &str, policy: Policy) -> Result { if name.is_empty() || policy.is_empty() { return Err(Error::InvalidArgument); } let policy_doc = self .cache .policy_docs .load() .get(name) .map(|v| { let mut p = v.clone(); p.update(policy.clone()); p }) .unwrap_or(PolicyDoc::new(policy)); self.api.save_policy_doc(name, policy_doc.clone()).await?; let now = OffsetDateTime::now_utc(); Cache::add_or_update(&self.cache.policy_docs, name, &policy_doc, now); Ok(now) } pub async fn list_polices(&self, bucket_name: &str) -> Result> { let mut m = HashMap::new(); self.api.load_policy_docs(&mut m).await?; set_default_canned_policies(&mut m); let cache = CacheEntity::new(m.clone()).update_load_time(); self.cache.policy_docs.store(Arc::new(cache)); let items: Vec<_> = m.into_iter().map(|(k, v)| (k, v.policy.clone())).collect(); let futures: Vec<_> = items.iter().map(|(_, policy)| policy.match_resource(bucket_name)).collect(); let results = join_all(futures).await; let filtered = items .into_iter() .zip(results) .filter_map(|((k, policy), matches)| { if bucket_name.is_empty() || matches { Some((k, policy)) } else { None } }) .collect(); Ok(filtered) } pub async fn merge_policies(&self, name: &str) -> (String, Policy) { let mut policies = Vec::new(); let mut to_merge = Vec::new(); let mut miss_policies = Vec::new(); for policy in MappedPolicy::new(name).to_slice() { if policy.is_empty() { continue; } if let Some(v) = self.cache.policy_docs.load().get(&policy).cloned() { policies.push(policy); to_merge.push(v.policy); } else { miss_policies.push(policy); } } if !miss_policies.is_empty() { let mut m = HashMap::new(); for policy in miss_policies { let _ = self.api.load_policy_doc(&policy, &mut m).await; } for (k, v) in m.iter() { Cache::add_or_update(&self.cache.policy_docs, k, v, OffsetDateTime::now_utc()); policies.push(k.clone()); to_merge.push(v.policy.clone()); } } (policies.join(","), Policy::merge_policies(to_merge)) } pub async fn list_policy_docs(&self, bucket_name: &str) -> Result> { let mut m = HashMap::new(); self.api.load_policy_docs(&mut m).await?; set_default_canned_policies(&mut m); let cache = CacheEntity::new(m.clone()).update_load_time(); self.cache.policy_docs.store(Arc::new(cache)); let items: Vec<_> = m.into_iter().map(|(k, v)| (k, v.clone())).collect(); let futures: Vec<_> = items .iter() .map(|(_, policy_doc)| policy_doc.policy.match_resource(bucket_name)) .collect(); let results = join_all(futures).await; let filtered = items .into_iter() .zip(results) .filter_map(|((k, policy_doc), matches)| { if bucket_name.is_empty() || matches { Some((k, policy_doc)) } else { None } }) .collect(); Ok(filtered) } pub async fn list_policy_docs_internal(&self, bucket_name: &str) -> Result> { let cache = self.cache.policy_docs.load(); let items: Vec<_> = cache.iter().map(|(k, v)| (k.clone(), v.clone())).collect(); let futures: Vec<_> = items .iter() .map(|(_, policy_doc)| policy_doc.policy.match_resource(bucket_name)) .collect(); let results = join_all(futures).await; let ret = items .into_iter() .zip(results) .filter_map(|((k, policy_doc), matches)| { if bucket_name.is_empty() || matches { Some((k, policy_doc)) } else { None } }) .collect(); Ok(ret) } pub async fn list_temp_accounts(&self, access_key: &str) -> Result> { let users = self.cache.users.load(); let mut user_exists = false; let mut ret = Vec::new(); for (_, v) in users.iter() { let is_derived = v.credentials.is_service_account() || v.credentials.is_temp(); if !is_derived && v.credentials.access_key.as_str() == access_key { user_exists = true; } else if is_derived && v.credentials.parent_user == access_key { user_exists = true; if v.credentials.is_temp() { let mut u = v.clone(); u.credentials.secret_key = String::new(); u.credentials.session_token = String::new(); ret.push(u); } } } if !user_exists { return Err(Error::NoSuchUser(access_key.to_string())); } Ok(ret) } pub async fn list_sts_accounts(&self, access_key: &str) -> Result> { let users = self.cache.users.load(); Ok(users .values() .filter_map(|x| { if !access_key.is_empty() && x.credentials.parent_user.as_str() == access_key && x.credentials.is_temp() { let mut c = x.credentials.clone(); c.secret_key = String::new(); c.session_token = String::new(); return Some(c); } None }) .collect()) } pub async fn list_service_accounts(&self, access_key: &str) -> Result> { let users = self.cache.users.load(); Ok(users .values() .filter_map(|x| { if !access_key.is_empty() && x.credentials.parent_user.as_str() == access_key && x.credentials.is_service_account() { let mut c = x.credentials.clone(); c.secret_key = String::new(); c.session_token = String::new(); return Some(c); } None }) .collect()) } /// create a service account and update cache pub async fn add_service_account(&self, cred: Credentials) -> Result { if cred.access_key.is_empty() || cred.parent_user.is_empty() { return Err(Error::InvalidArgument); } let users = self.cache.users.load(); if let Some(x) = users.get(&cred.access_key) { if x.credentials.is_service_account() { return Err(Error::IAMActionNotAllowed); } } let u = UserIdentity::new(cred); self.api .save_user_identity(&u.credentials.access_key, UserType::Svc, u.clone(), None) .await?; self.update_user_with_claims(&u.credentials.access_key, u.clone())?; Ok(OffsetDateTime::now_utc()) } pub async fn update_service_account(&self, name: &str, opts: UpdateServiceAccountOpts) -> Result { let Some(ui) = self.cache.users.load().get(name).cloned() else { return Err(Error::NoSuchServiceAccount(name.to_string())); }; if !ui.credentials.is_service_account() { return Err(Error::NoSuchServiceAccount(name.to_string())); } let mut cr = ui.credentials.clone(); let current_secret_key = cr.secret_key.clone(); if let Some(secret) = opts.secret_key { if !is_secret_key_valid(&secret) { return Err(Error::InvalidSecretKeyLength); } cr.secret_key = secret; } if opts.name.is_some() { cr.name = opts.name; } if opts.description.is_some() { cr.description = opts.description; } if opts.expiration.is_some() { // TODO: check expiration cr.expiration = opts.expiration; } if let Some(status) = opts.status { match status.as_str() { val if val == AccountStatus::Enabled.as_ref() => cr.status = auth::ACCOUNT_ON.to_owned(), val if val == AccountStatus::Disabled.as_ref() => cr.status = auth::ACCOUNT_OFF.to_owned(), auth::ACCOUNT_ON => cr.status = auth::ACCOUNT_ON.to_owned(), auth::ACCOUNT_OFF => cr.status = auth::ACCOUNT_OFF.to_owned(), _ => cr.status = auth::ACCOUNT_OFF.to_owned(), } } let mut m: HashMap = get_claims_from_token_with_secret(&cr.session_token, ¤t_secret_key)?; m.remove(SESSION_POLICY_NAME_EXTRACTED); let nosp = if let Some(policy) = &opts.session_policy { policy.version.is_empty() && policy.statements.is_empty() } else { false }; if m.contains_key(SESSION_POLICY_NAME) && nosp { m.remove(SESSION_POLICY_NAME); m.insert(iam_policy_claim_name_sa(), Value::String(INHERITED_POLICY_TYPE.to_owned())); } if let Some(session_policy) = &opts.session_policy { session_policy.validate()?; if !session_policy.version.is_empty() && !session_policy.statements.is_empty() { let policy_buf = serde_json::to_vec(&session_policy)?; if policy_buf.len() > MAX_SVCSESSION_POLICY_SIZE { return Err(Error::PolicyTooLarge); } m.insert( SESSION_POLICY_NAME.to_owned(), Value::String(base64_simd::URL_SAFE_NO_PAD.encode_to_string(&policy_buf)), ); m.insert(iam_policy_claim_name_sa(), Value::String(EMBEDDED_POLICY_TYPE.to_owned())); } } m.insert("accessKey".to_owned(), Value::String(name.to_owned())); cr.session_token = jwt_sign(&m, &cr.secret_key)?; let u = UserIdentity::new(cr); self.api .save_user_identity(&u.credentials.access_key, UserType::Svc, u.clone(), None) .await?; self.update_user_with_claims(&u.credentials.access_key, u.clone())?; Ok(OffsetDateTime::now_utc()) } pub async fn policy_db_get(&self, name: &str, groups: &Option>) -> Result> { if name.is_empty() { return Err(Error::InvalidArgument); } let (mut policies, _) = self.policy_db_get_internal(name, false, false).await?; let present = !policies.is_empty(); if let Some(groups) = groups { for group in groups.iter() { let (gp, _) = self.policy_db_get_internal(group, true, present).await?; gp.iter().for_each(|v| { policies.push(v.clone()); }); } } Ok(policies) } async fn policy_db_get_internal( &self, name: &str, is_group: bool, policy_present: bool, ) -> Result<(Vec, OffsetDateTime)> { if is_group { let groups = self.cache.groups.load(); let g = match groups.get(name) { Some(p) => p.clone(), None => { let mut m = HashMap::new(); self.api.load_group(name, &mut m).await?; if let Some(p) = m.get(name) { Cache::add_or_update(&self.cache.groups, name, p, OffsetDateTime::now_utc()); } m.get(name).cloned().ok_or(Error::NoSuchGroup(name.to_string()))? } }; if g.status == STATUS_DISABLED { return Ok((Vec::new(), OffsetDateTime::now_utc())); } if let Some(policy) = self.cache.group_policies.load().get(name) { return Ok((policy.to_slice(), policy.update_at)); } if !policy_present { let mut m = HashMap::new(); if let Err(err) = self.api.load_mapped_policy(name, UserType::Reg, true, &mut m).await { if !is_err_no_such_policy(&err) { return Err(err); } } if let Some(p) = m.get(name) { Cache::add_or_update(&self.cache.group_policies, name, p, OffsetDateTime::now_utc()); return Ok((p.to_slice(), p.update_at)); } return Ok((Vec::new(), OffsetDateTime::now_utc())); } return Ok((Vec::new(), OffsetDateTime::now_utc())); } let users = self.cache.users.load(); let u = users.get(name).cloned().unwrap_or_default(); if !u.credentials.is_valid() { return Ok((Vec::new(), OffsetDateTime::now_utc())); } let mp = match self.cache.user_policies.load().get(name) { Some(p) => p.clone(), None => { let mut m = HashMap::new(); if let Err(err) = self.api.load_mapped_policy(name, UserType::Reg, false, &mut m).await { if !is_err_no_such_policy(&err) { return Err(err); } } if let Some(p) = m.get(name) { Cache::add_or_update(&self.cache.user_policies, name, p, OffsetDateTime::now_utc()); p.clone() } else { match self.cache.sts_policies.load().get(name) { Some(p) => p.clone(), None => { let mut m = HashMap::new(); if let Err(err) = self.api.load_mapped_policy(name, UserType::Sts, false, &mut m).await { if !is_err_no_such_policy(&err) { return Err(err); } } if let Some(p) = m.get(name) { Cache::add_or_update(&self.cache.sts_policies, name, p, OffsetDateTime::now_utc()); p.clone() } else { MappedPolicy::default() } } } } } }; let mut policies: HashSet = mp.to_slice().into_iter().collect(); if let Some(groups) = u.credentials.groups.as_ref() { for group in groups.iter() { if self .cache .groups .load() .get(group) .filter(|v| v.status == STATUS_DISABLED) .is_some() { return Ok((Vec::new(), OffsetDateTime::now_utc())); } let mp = match self.cache.group_policies.load().get(group) { Some(p) => p.clone(), None => { let mut m = HashMap::new(); if let Err(err) = self.api.load_mapped_policy(group, UserType::Reg, true, &mut m).await { if !is_err_no_such_policy(&err) { return Err(err); } } if let Some(p) = m.get(group) { Cache::add_or_update(&self.cache.group_policies, group, p, OffsetDateTime::now_utc()); p.clone() } else { MappedPolicy::default() } } }; mp.to_slice().iter().for_each(|v| { policies.insert(v.clone()); }); } } let update_at = mp.update_at; for group in self .cache .user_group_memberships .load() .get(name) .cloned() .unwrap_or_default() .iter() { if self .cache .groups .load() .get(group) .filter(|v| v.status == STATUS_DISABLED) .is_some() { return Ok((Vec::new(), OffsetDateTime::now_utc())); } let mp = match self.cache.group_policies.load().get(group) { Some(p) => p.clone(), None => { let mut m = HashMap::new(); if let Err(err) = self.api.load_mapped_policy(group, UserType::Reg, true, &mut m).await { if !is_err_no_such_policy(&err) { return Err(err); } } if let Some(p) = m.get(group) { Cache::add_or_update(&self.cache.group_policies, group, p, OffsetDateTime::now_utc()); p.clone() } else { MappedPolicy::default() } } }; mp.to_slice().iter().for_each(|v| { policies.insert(v.clone()); }); } Ok((policies.into_iter().collect(), update_at)) } pub async fn policy_db_set(&self, name: &str, user_type: UserType, is_group: bool, policy: &str) -> Result { if name.is_empty() { return Err(Error::InvalidArgument); } if policy.is_empty() { if let Err(err) = self.api.delete_mapped_policy(name, user_type, is_group).await { if !is_err_no_such_policy(&err) { return Err(err); } } if is_group { Cache::delete(&self.cache.group_policies, name, OffsetDateTime::now_utc()); } else if user_type == UserType::Sts { Cache::delete(&self.cache.sts_policies, name, OffsetDateTime::now_utc()); } else { Cache::delete(&self.cache.user_policies, name, OffsetDateTime::now_utc()); } return Ok(OffsetDateTime::now_utc()); } let mp = MappedPolicy::new(policy); let policy_docs_cache = self.cache.policy_docs.load(); for p in mp.to_slice() { if !policy_docs_cache.contains_key(&p) { return Err(Error::NoSuchPolicy); } } self.api .save_mapped_policy(name, user_type, is_group, mp.clone(), None) .await?; if is_group { Cache::add_or_update(&self.cache.group_policies, name, &mp, OffsetDateTime::now_utc()); } else if user_type == UserType::Sts { Cache::add_or_update(&self.cache.sts_policies, name, &mp, OffsetDateTime::now_utc()); } else { Cache::add_or_update(&self.cache.user_policies, name, &mp, OffsetDateTime::now_utc()); } Ok(OffsetDateTime::now_utc()) } pub async fn set_temp_user(&self, access_key: &str, cred: &Credentials, policy_name: Option<&str>) -> Result { if access_key.is_empty() || !cred.is_temp() || cred.is_expired() || cred.parent_user.is_empty() { error!( "set temp user invalid argument, access_key: {}, is_temp: {}, is_expired: {}, parent_user_empty: {}", access_key, cred.is_temp(), cred.is_expired(), cred.parent_user.is_empty() ); return Err(Error::InvalidArgument); } if let Some(policy) = policy_name { let mp = MappedPolicy::new(policy); let (_, combined_policy_stmt) = filter_policies(&self.cache, &mp.policies, "temp"); if combined_policy_stmt.is_empty() { return Err(Error::other(format!("Required policy not found: {}", IamError::NoSuchPolicy))); } self.api .save_mapped_policy(&cred.parent_user, UserType::Sts, false, mp.clone(), None) .await?; Cache::add_or_update(&self.cache.sts_policies, &cred.parent_user, &mp, OffsetDateTime::now_utc()); } let u = UserIdentity::new(cred.clone()); self.api .save_user_identity(access_key, UserType::Sts, u.clone(), None) .await?; Cache::add_or_update(&self.cache.sts_accounts, access_key, &u, OffsetDateTime::now_utc()); Ok(OffsetDateTime::now_utc()) } pub async fn get_user_info(&self, name: &str) -> Result { let users = self.cache.users.load(); let policies = self.cache.user_policies.load(); let group_members = self.cache.user_group_memberships.load(); let u = match users.get(name) { Some(u) => u, None => return Err(Error::NoSuchUser(name.to_string())), }; if u.credentials.is_temp() || u.credentials.is_service_account() { return Err(Error::IAMActionNotAllowed); } let mut uinfo = rustfs_madmin::UserInfo { status: if u.credentials.is_valid() { AccountStatus::Enabled } else { AccountStatus::Disabled }, updated_at: u.update_at, ..Default::default() }; if let Some(p) = policies.get(name) { uinfo.policy_name = Some(p.policies.clone()); uinfo.updated_at = Some(p.update_at); } if let Some(members) = group_members.get(name) { uinfo.member_of = Some(members.iter().cloned().collect()); } Ok(uinfo) } // returns all users (not STS or service accounts) pub async fn get_users(&self) -> Result> { let mut m = HashMap::new(); let users = self.cache.users.load(); let policies = self.cache.user_policies.load(); let group_members = self.cache.user_group_memberships.load(); for (k, v) in users.iter() { if v.credentials.is_temp() || v.credentials.is_service_account() { continue; } let mut u = rustfs_madmin::UserInfo { status: if v.credentials.is_valid() { AccountStatus::Enabled } else { AccountStatus::Disabled }, updated_at: v.update_at, ..Default::default() }; if let Some(p) = policies.get(k) { u.policy_name = Some(p.policies.clone()); u.updated_at = Some(p.update_at); } if let Some(members) = group_members.get(k) { u.member_of = Some(members.iter().cloned().collect()); } m.insert(k.clone(), u); } Ok(m) } pub async fn get_bucket_users(&self, bucket_name: &str) -> Result> { let users = self.cache.users.load(); let policies_cache = self.cache.user_policies.load(); let group_members = self.cache.user_group_memberships.load(); let group_policy_cache = self.cache.group_policies.load(); let mut ret = HashMap::new(); for (k, v) in users.iter() { if v.credentials.is_temp() || v.credentials.is_service_account() { continue; } let mut policies = Vec::new(); if let Some(p) = policies_cache.get(k) { policies.push(p.policies.clone()); if let Some(groups) = group_members.get(k) { for group in groups { if let Some(p) = group_policy_cache.get(group) { policies.push(p.policies.clone()); } } } } let matched_policies = filter_policies(&self.cache, &policies.join(","), bucket_name).0; if matched_policies.is_empty() { continue; } let mut u = rustfs_madmin::UserInfo { policy_name: Some(matched_policies), status: if v.credentials.is_valid() { AccountStatus::Enabled } else { AccountStatus::Disabled }, updated_at: v.update_at, ..Default::default() }; if let Some(members) = group_members.get(k) { u.member_of = Some(members.iter().cloned().collect()); } ret.insert(k.clone(), u); } Ok(ret) } pub async fn get_users_with_mapped_policies(&self) -> HashMap { let mut m = HashMap::new(); self.cache.user_policies.load().iter().for_each(|(k, v)| { m.insert(k.clone(), v.policies.clone()); }); self.cache.sts_policies.load().iter().for_each(|(k, v)| { m.insert(k.clone(), v.policies.clone()); }); m } pub async fn add_user(&self, access_key: &str, args: &AddOrUpdateUserReq) -> Result { let users = self.cache.users.load(); if let Some(x) = users.get(access_key) { warn!("user already exists: {:?}", x); if x.credentials.is_temp() { return Err(Error::IAMActionNotAllowed); } } let status = { match &args.status { AccountStatus::Enabled => auth::ACCOUNT_ON, _ => auth::ACCOUNT_OFF, } }; let user_entry = UserIdentity::from(Credentials { access_key: access_key.to_string(), secret_key: args.secret_key.to_string(), status: status.to_owned(), ..Default::default() }); self.api .save_user_identity(access_key, UserType::Reg, user_entry.clone(), None) .await?; self.update_user_with_claims(access_key, user_entry)?; Ok(OffsetDateTime::now_utc()) } pub async fn delete_user(&self, access_key: &str, utype: UserType) -> Result<()> { if access_key.is_empty() { return Err(Error::InvalidArgument); } if utype == UserType::Reg { if let Some(member_of) = self.cache.user_group_memberships.load().get(access_key) { for member in member_of.iter() { let _ = self .remove_members_from_group(member, vec![access_key.to_string()], false) .await?; } } let users_cache = self.cache.users.load(); for (_, v) in users_cache.iter() { let u = &v.credentials; if u.parent_user.as_str() == access_key { if u.is_service_account() { let _ = self.api.delete_user_identity(&u.access_key, UserType::Svc).await; Cache::delete(&self.cache.users, &u.access_key, OffsetDateTime::now_utc()); } if u.is_temp() { let _ = self.api.delete_user_identity(&u.access_key, UserType::Sts).await; Cache::delete(&self.cache.sts_accounts, &u.access_key, OffsetDateTime::now_utc()); Cache::delete(&self.cache.users, &u.access_key, OffsetDateTime::now_utc()); } } } } let _ = self.api.delete_mapped_policy(access_key, utype, false).await; Cache::delete(&self.cache.user_policies, access_key, OffsetDateTime::now_utc()); if let Err(err) = self.api.delete_user_identity(access_key, utype).await { if !is_err_no_such_user(&err) { return Err(err); } } if utype == UserType::Sts { Cache::delete(&self.cache.sts_accounts, access_key, OffsetDateTime::now_utc()); } Cache::delete(&self.cache.users, access_key, OffsetDateTime::now_utc()); Ok(()) } pub async fn update_user_secret_key(&self, access_key: &str, secret_key: &str) -> Result<()> { if access_key.is_empty() || secret_key.is_empty() { return Err(Error::InvalidArgument); } let users = self.cache.users.load(); let u = match users.get(access_key) { Some(u) => u, None => return Err(Error::NoSuchUser(access_key.to_string())), }; let mut cred = u.credentials.clone(); cred.secret_key = secret_key.to_string(); let u = UserIdentity::from(cred); self.api .save_user_identity(access_key, UserType::Reg, u.clone(), None) .await?; self.update_user_with_claims(access_key, u) } pub async fn set_user_status(&self, access_key: &str, status: AccountStatus) -> Result { if access_key.is_empty() { return Err(Error::InvalidArgument); } if !access_key.is_empty() && status != AccountStatus::Enabled && status != AccountStatus::Disabled { return Err(Error::InvalidArgument); } let users = self.cache.users.load(); let u = match users.get(access_key) { Some(u) => u, None => return Err(Error::NoSuchUser(access_key.to_string())), }; if u.credentials.is_temp() || u.credentials.is_service_account() { return Err(Error::IAMActionNotAllowed); } let status = { match status { AccountStatus::Enabled => auth::ACCOUNT_ON, _ => auth::ACCOUNT_OFF, } }; let user_entry = UserIdentity::from(Credentials { access_key: access_key.to_string(), secret_key: u.credentials.secret_key.clone(), status: status.to_owned(), ..Default::default() }); self.api .save_user_identity(access_key, UserType::Reg, user_entry.clone(), None) .await?; self.update_user_with_claims(access_key, user_entry)?; Ok(OffsetDateTime::now_utc()) } fn update_user_with_claims(&self, k: &str, u: UserIdentity) -> Result<()> { let mut u = u; if !u.credentials.session_token.is_empty() { u.credentials.claims = Some(extract_jwt_claims(&u)?); } if u.credentials.is_temp() && !u.credentials.is_service_account() { Cache::add_or_update(&self.cache.sts_accounts, k, &u, OffsetDateTime::now_utc()); } else { Cache::add_or_update(&self.cache.users, k, &u, OffsetDateTime::now_utc()); } Ok(()) } pub async fn is_temp_user(&self, access_key: &str) -> Result<(bool, String)> { let users = self.cache.users.load(); let u = match users.get(access_key) { Some(u) => u, None => return Err(Error::NoSuchUser(access_key.to_string())), }; if u.credentials.is_temp() { Ok((true, u.credentials.parent_user.clone())) } else { Ok((false, String::new())) } } pub async fn add_users_to_group(&self, group: &str, members: Vec) -> Result { if group.is_empty() { return Err(Error::InvalidArgument); } let users_cache = self.cache.users.load(); for member in members.iter() { if let Some(u) = users_cache.get(member) { if u.credentials.is_temp() || u.credentials.is_service_account() { return Err(Error::IAMActionNotAllowed); } } else { return Err(Error::NoSuchUser(member.to_string())); } } let gi = match self.cache.groups.load().get(group) { Some(res) => { let mut gi = res.clone(); let mut uniq_set: HashSet = HashSet::from_iter(gi.members.iter().cloned()); uniq_set.extend(members.iter().cloned()); gi.members = uniq_set.into_iter().collect(); gi } None => GroupInfo::new(members.clone()), }; self.api.save_group_info(group, gi.clone()).await?; Cache::add_or_update(&self.cache.groups, group, &gi, OffsetDateTime::now_utc()); let user_group_memberships = self.cache.user_group_memberships.load(); members.iter().for_each(|member| { if let Some(m) = user_group_memberships.get(member) { let mut m = m.clone(); m.insert(group.to_string()); Cache::add_or_update(&self.cache.user_group_memberships, member, &m, OffsetDateTime::now_utc()); } }); Ok(OffsetDateTime::now_utc()) } pub async fn set_group_status(&self, name: &str, enable: bool) -> Result { if name.is_empty() { return Err(Error::InvalidArgument); } let groups = self.cache.groups.load(); let mut gi = match groups.get(name) { Some(gi) => gi.clone(), None => return Err(Error::NoSuchGroup(name.to_string())), }; if enable { gi.status = STATUS_ENABLED.to_owned(); } else { gi.status = STATUS_DISABLED.to_owned(); } self.api.save_group_info(name, gi.clone()).await?; Cache::add_or_update(&self.cache.groups, name, &gi, OffsetDateTime::now_utc()); Ok(OffsetDateTime::now_utc()) } pub async fn get_group_description(&self, name: &str) -> Result { let (ps, updated_at) = self.policy_db_get_internal(name, true, false).await?; let policy = ps.join(","); let gi = self .cache .groups .load() .get(name) .cloned() .ok_or(Error::NoSuchGroup(name.to_string()))?; Ok(GroupDesc { name: name.to_string(), policy, members: gi.members, updated_at: Some(updated_at), status: gi.status, }) } pub async fn list_groups(&self) -> Result> { Ok(self.cache.groups.load().keys().cloned().collect()) } pub async fn update_groups(&self) -> Result> { let mut groups_set = HashSet::new(); let mut m = HashMap::new(); self.api.load_groups(&mut m).await?; for (group, gi) in m.iter() { Cache::add_or_update(&self.cache.groups, group, gi, OffsetDateTime::now_utc()); groups_set.insert(group.to_string()); } let mut m = HashMap::new(); self.api.load_mapped_policies(UserType::Reg, true, &mut m).await?; for (group, gi) in m.iter() { Cache::add_or_update(&self.cache.group_policies, group, gi, OffsetDateTime::now_utc()); groups_set.insert(group.to_string()); } Ok(groups_set.into_iter().collect()) } pub async fn remove_members_from_group( &self, name: &str, members: Vec, update_cache_only: bool, ) -> Result { let mut gi = self .cache .groups .load() .get(name) .cloned() .ok_or(Error::NoSuchGroup(name.to_string()))?; let s: HashSet<&String> = HashSet::from_iter(gi.members.iter()); let d: HashSet<&String> = HashSet::from_iter(members.iter()); gi.members = s.difference(&d).map(|v| v.to_string()).collect::>(); if !update_cache_only { self.api.save_group_info(name, gi.clone()).await?; } Cache::add_or_update(&self.cache.groups, name, &gi, OffsetDateTime::now_utc()); let user_group_memberships = self.cache.user_group_memberships.load(); members.iter().for_each(|member| { if let Some(m) = user_group_memberships.get(member) { let mut m = m.clone(); m.remove(name); Cache::add_or_update(&self.cache.user_group_memberships, member, &m, OffsetDateTime::now_utc()); } }); Ok(OffsetDateTime::now_utc()) } pub async fn remove_users_from_group(&self, group: &str, members: Vec) -> Result { if group.is_empty() { return Err(Error::InvalidArgument); } let users_cache = self.cache.users.load(); for member in members.iter() { if let Some(u) = users_cache.get(member) { if u.credentials.is_temp() || u.credentials.is_service_account() { return Err(Error::IAMActionNotAllowed); } } else { return Err(Error::NoSuchUser(member.to_string())); } } let gi = self .cache .groups .load() .get(group) .cloned() .ok_or(Error::NoSuchGroup(group.to_string()))?; if members.is_empty() && !gi.members.is_empty() { return Err(Error::GroupNotEmpty); } if members.is_empty() { if let Err(err) = self.api.delete_mapped_policy(group, UserType::Reg, true).await { if !is_err_no_such_policy(&err) { return Err(err); } } if let Err(err) = self.api.delete_group_info(group).await { if !is_err_no_such_group(&err) { return Err(err); } } Cache::delete(&self.cache.groups, group, OffsetDateTime::now_utc()); Cache::delete(&self.cache.group_policies, group, OffsetDateTime::now_utc()); return Ok(OffsetDateTime::now_utc()); } self.remove_members_from_group(group, members, false).await } fn remove_group_from_memberships_map(&self, group: &str) { let user_group_memberships = self.cache.user_group_memberships.load(); for (k, v) in user_group_memberships.iter() { if v.contains(group) { let mut m = v.clone(); m.remove(group); Cache::add_or_update(&self.cache.user_group_memberships, k, &m, OffsetDateTime::now_utc()); } } } fn update_group_memberships_map(&self, group: &str, gi: &GroupInfo) { let user_group_memberships = self.cache.user_group_memberships.load(); for member in gi.members.iter() { if let Some(m) = user_group_memberships.get(member) { let mut m = m.clone(); m.insert(group.to_string()); Cache::add_or_update(&self.cache.user_group_memberships, member, &m, OffsetDateTime::now_utc()); } } } pub async fn group_notification_handler(&self, group: &str) -> Result<()> { let mut m = HashMap::new(); if let Err(err) = self.api.load_group(group, &mut m).await { if !is_err_no_such_group(&err) { return Err(err); } self.remove_group_from_memberships_map(group); Cache::delete(&self.cache.groups, group, OffsetDateTime::now_utc()); Cache::delete(&self.cache.group_policies, group, OffsetDateTime::now_utc()); return Ok(()); } let gi = m[group].clone(); Cache::add_or_update(&self.cache.groups, group, &gi, OffsetDateTime::now_utc()); self.remove_group_from_memberships_map(group); self.update_group_memberships_map(group, &gi); Ok(()) } pub async fn policy_notification_handler(&self, policy: &str) -> Result<()> { let mut m = HashMap::new(); if let Err(err) = self.api.load_policy_doc(policy, &mut m).await { if !is_err_no_such_policy(&err) { return Err(err); } Cache::delete(&self.cache.policy_docs, policy, OffsetDateTime::now_utc()); let user_policy_cache = self.cache.user_policies.load(); let users_cache = self.cache.users.load(); for (k, v) in user_policy_cache.iter() { let mut set = v.policy_set(); if set.contains(policy) { if !users_cache.contains_key(k) { Cache::delete(&self.cache.user_policies, k, OffsetDateTime::now_utc()); continue; } set.remove(policy); let mp = MappedPolicy::new(&set.iter().cloned().collect::>().join(",")); Cache::add_or_update(&self.cache.user_policies, k, &mp, OffsetDateTime::now_utc()); } } let group_policy_cache = self.cache.group_policies.load(); for (k, v) in group_policy_cache.iter() { let mut set = v.policy_set(); if set.contains(policy) { set.remove(policy); let mp = MappedPolicy::new(&set.iter().cloned().collect::>().join(",")); Cache::add_or_update(&self.cache.group_policies, k, &mp, OffsetDateTime::now_utc()); } } return Ok(()); } Cache::add_or_update(&self.cache.policy_docs, policy, &m[policy], OffsetDateTime::now_utc()); Ok(()) } pub async fn policy_mapping_notification_handler(&self, name: &str, user_type: UserType, is_group: bool) -> Result<()> { let mut m = HashMap::new(); if let Err(err) = self.api.load_mapped_policy(name, user_type, is_group, &mut m).await { if !is_err_no_such_policy(&err) { return Err(err); } if is_group { Cache::delete(&self.cache.group_policies, name, OffsetDateTime::now_utc()); } else if user_type == UserType::Sts { Cache::delete(&self.cache.sts_policies, name, OffsetDateTime::now_utc()); } else { Cache::delete(&self.cache.user_policies, name, OffsetDateTime::now_utc()); } return Ok(()); } let mp = m[name].clone(); if is_group { Cache::add_or_update(&self.cache.group_policies, name, &mp, OffsetDateTime::now_utc()); } else if user_type == UserType::Sts { Cache::delete(&self.cache.sts_policies, name, OffsetDateTime::now_utc()); } else { Cache::add_or_update(&self.cache.user_policies, name, &mp, OffsetDateTime::now_utc()); } Ok(()) } pub async fn user_notification_handler(&self, name: &str, user_type: UserType) -> Result<()> { let mut m = HashMap::new(); if let Err(err) = self.api.load_user(name, user_type, &mut m).await { if !is_err_no_such_user(&err) { return Err(err); } if user_type == UserType::Sts { Cache::delete(&self.cache.sts_accounts, name, OffsetDateTime::now_utc()); } else { Cache::delete(&self.cache.users, name, OffsetDateTime::now_utc()); } let member_of = self.cache.user_group_memberships.load(); if let Some(m) = member_of.get(name) { for group in m.iter() { if let Err(err) = self.remove_members_from_group(group, vec![name.to_string()], true).await { if !is_err_no_such_group(&err) { return Err(err); } } } } if user_type == UserType::Reg { let users_cache = self.cache.users.load(); for (_, v) in users_cache.iter() { let u = &v.credentials; if u.parent_user.as_str() == name && u.is_service_account() { let _ = self.api.delete_user_identity(&u.access_key, UserType::Svc).await; Cache::delete(&self.cache.users, &u.access_key, OffsetDateTime::now_utc()); } } let sts_accounts = self.cache.sts_accounts.load(); if let Some(u) = sts_accounts.get(name) { let u = &u.credentials; if u.parent_user.as_str() == name { let _ = self.api.delete_user_identity(&u.access_key, UserType::Sts).await; Cache::delete(&self.cache.sts_accounts, &u.access_key, OffsetDateTime::now_utc()); } } } Cache::delete(&self.cache.user_policies, name, OffsetDateTime::now_utc()); return Ok(()); } let u = m[name].clone(); match user_type { UserType::Sts => { let name = u.credentials.parent_user; let mut m = HashMap::new(); if let Err(err) = self.api.load_mapped_policy(&name, user_type, false, &mut m).await { if !is_err_no_such_policy(&err) { return Err(err); } return Ok(()); } Cache::add_or_update(&self.cache.sts_policies, &name, &m[&name], OffsetDateTime::now_utc()); } UserType::Reg => { let mut m = HashMap::new(); if let Err(err) = self.api.load_mapped_policy(name, user_type, false, &mut m).await { if !is_err_no_such_policy(&err) { return Err(err); } return Ok(()); } Cache::add_or_update(&self.cache.sts_policies, name, &m[name], OffsetDateTime::now_utc()); } UserType::Svc => { let users_cache = self.cache.users.load(); if let Some(u) = users_cache.get(&u.credentials.parent_user) { let mut m = HashMap::new(); if let Err(err) = self .api .load_mapped_policy(&u.credentials.parent_user, UserType::Reg, false, &mut m) .await { if !is_err_no_such_policy(&err) { return Err(err); } return Ok(()); } Cache::add_or_update( &self.cache.user_policies, &u.credentials.parent_user, &m[&u.credentials.parent_user], OffsetDateTime::now_utc(), ); } else { let mut m = HashMap::new(); if let Err(err) = self .api .load_mapped_policy(&u.credentials.parent_user, UserType::Sts, false, &mut m) .await { if !is_err_no_such_policy(&err) { return Err(err); } return Ok(()); } Cache::add_or_update( &self.cache.sts_policies, &u.credentials.parent_user, &m[&u.credentials.parent_user], OffsetDateTime::now_utc(), ); } } UserType::None => {} } Ok(()) } } pub fn get_default_policyes() -> HashMap { let default_policies = &DEFAULT_POLICIES; default_policies .iter() .map(|(n, p)| { ( n.to_string(), PolicyDoc { version: 1, policy: p.clone(), ..Default::default() }, ) }) .collect() } fn set_default_canned_policies(policies: &mut HashMap) { let default_policies = &DEFAULT_POLICIES; for (k, v) in default_policies.iter() { let name = k.to_string(); policies.entry(name).or_insert_with(|| PolicyDoc::default_policy(v.clone())); } } pub fn get_token_signing_key() -> Option { if let Some(s) = get_global_action_cred() { Some(s.secret_key.clone()) } else { None } } pub fn extract_jwt_claims(u: &UserIdentity) -> Result> { let Some(sys_key) = get_token_signing_key() else { return Err(Error::other("global active sk not init")); }; let keys = vec![&sys_key, &u.credentials.secret_key]; for key in keys { if let Ok(claims) = get_claims_from_token_with_secret(&u.credentials.session_token, key) { return Ok(claims); } } Err(Error::other("unable to extract claims")) } fn filter_policies(cache: &Cache, policy_name: &str, bucket_name: &str) -> (String, Policy) { let mp = MappedPolicy::new(policy_name).to_slice(); let mut policies = Vec::new(); let mut to_merge = Vec::new(); for policy in mp { if policy.is_empty() { continue; } if let Some(p) = cache.policy_docs.load().get(&policy) { if bucket_name.is_empty() || pollster::block_on(p.policy.match_resource(bucket_name)) { policies.push(policy); to_merge.push(p.policy.clone()); } } } (policies.join(","), Policy::merge_policies(to_merge)) } #[cfg(test)] mod tests { use super::*; use rustfs_policy::policy::{Policy, PolicyDoc}; use serde_json::json; use std::collections::HashMap; #[test] fn test_iam_format_new_version_1() { let format = IAMFormat::new_version_1(); assert_eq!(format.version, IAM_FORMAT_VERSION_1); assert_eq!(format.version, 1); } #[test] fn test_get_iam_format_file_path() { let path = get_iam_format_file_path(); assert!(path.contains(IAM_FORMAT_FILE)); assert!(path.contains(&*IAM_CONFIG_PREFIX)); assert_eq!(path, format!("{}/{}", *IAM_CONFIG_PREFIX, IAM_FORMAT_FILE)); } #[test] fn test_get_default_policies() { let policies = get_default_policyes(); // Should contain some default policies assert!(!policies.is_empty()); // Check that all values are PolicyDoc for (name, policy_doc) in &policies { assert!(!name.is_empty()); // PolicyDoc.version is i64, not String assert!(policy_doc.version >= 0); } } #[test] fn test_get_token_signing_key() { // This function returns the global action credential's secret key // In test environment, it might be None let key = get_token_signing_key(); // Just verify it doesn't panic and returns an Option if let Some(k) = key { assert!(!k.is_empty()); } // This is acceptable in test environment when None } #[test] fn test_extract_jwt_claims_basic() { let user_identity = UserIdentity { version: 1, credentials: Credentials { access_key: "test-access-key".to_string(), secret_key: "test-secret-key".to_string(), session_token: "invalid-token".to_string(), // Invalid token for testing error handling expiration: None, status: "enabled".to_string(), parent_user: "".to_string(), groups: None, claims: Some({ let mut claims = HashMap::new(); claims.insert("sub".to_string(), json!("test-user")); claims.insert("aud".to_string(), json!("test-audience")); claims }), name: None, description: None, }, update_at: Some(OffsetDateTime::now_utc()), }; let result = extract_jwt_claims(&user_identity); // In test environment without proper JWT setup, this should fail assert!(result.is_err()); } #[test] fn test_extract_jwt_claims_no_claims() { let user_identity = UserIdentity { version: 1, credentials: Credentials { access_key: "test-access-key".to_string(), secret_key: "test-secret-key".to_string(), session_token: "".to_string(), // Empty token expiration: None, status: "enabled".to_string(), parent_user: "".to_string(), groups: None, claims: None, name: None, description: None, }, update_at: Some(OffsetDateTime::now_utc()), }; let result = extract_jwt_claims(&user_identity); // Should fail with empty session token assert!(result.is_err()); } #[test] fn test_filter_policies_empty_bucket() { let cache = Cache::default(); let policy_name = "test-policy"; let bucket_name = ""; let (name, policy) = filter_policies(&cache, policy_name, bucket_name); // When cache is empty, should return empty name and empty policy assert_eq!(name, ""); assert!(policy.statements.is_empty()); } #[test] fn test_filter_policies_with_bucket() { let cache = Cache::default(); let policy_name = "test-policy"; let bucket_name = "test-bucket"; let (name, policy) = filter_policies(&cache, policy_name, bucket_name); // When cache is empty, should return empty name and empty policy regardless of bucket assert_eq!(name, ""); assert!(policy.statements.is_empty()); } #[test] fn test_constants() { // Test that constants are properly defined assert_eq!(IAM_FORMAT_FILE, "format.json"); assert_eq!(IAM_FORMAT_VERSION_1, 1); } #[test] fn test_iam_format_serialization() { let format = IAMFormat::new_version_1(); // Test serialization let serialized = serde_json::to_string(&format).unwrap(); assert!(serialized.contains("\"version\":1")); // Test deserialization let deserialized: IAMFormat = serde_json::from_str(&serialized).unwrap(); assert_eq!(deserialized.version, format.version); } #[test] fn test_mapped_policy_operations() { let policy_name = "test-policy"; let mapped_policy = MappedPolicy::new(policy_name); // Test that MappedPolicy can be created let policies = mapped_policy.to_slice(); assert!(!policies.is_empty()); assert!(policies.iter().any(|p| p.contains(policy_name))); } #[test] fn test_user_identity_structure() { let credentials = Credentials { access_key: "AKIAIOSFODNN7EXAMPLE".to_string(), secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string(), session_token: "".to_string(), expiration: None, status: "enabled".to_string(), parent_user: "parent-user".to_string(), groups: Some(vec!["group1".to_string(), "group2".to_string()]), claims: None, name: None, description: None, }; let user_identity = UserIdentity { version: 1, credentials, update_at: Some(OffsetDateTime::now_utc()), }; // Test basic structure assert_eq!(user_identity.version, 1); assert_eq!(user_identity.credentials.access_key, "AKIAIOSFODNN7EXAMPLE"); assert_eq!(user_identity.credentials.secret_key, "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"); assert_eq!(user_identity.credentials.status, "enabled"); assert_eq!(user_identity.credentials.parent_user, "parent-user"); assert_eq!(user_identity.credentials.groups, Some(vec!["group1".to_string(), "group2".to_string()])); } #[test] fn test_policy_structure() { let policy = Policy { id: Default::default(), version: "2012-10-17".to_string(), statements: vec![], }; // Test basic structure assert_eq!(policy.version, "2012-10-17"); assert_eq!(policy.statements.len(), 0); assert!(policy.is_empty()); } #[test] fn test_policy_doc_structure() { let policy = Policy { id: Default::default(), version: "2012-10-17".to_string(), statements: vec![], }; let policy_doc = PolicyDoc { version: 1, policy, create_date: Some(OffsetDateTime::now_utc()), update_date: Some(OffsetDateTime::now_utc()), }; // Test basic structure assert_eq!(policy_doc.version, 1); assert_eq!(policy_doc.policy.version, "2012-10-17"); assert!(policy_doc.policy.statements.is_empty()); } #[test] fn test_group_info_basic() { // Test that GroupInfo can be created and used let group_info = GroupInfo { version: 1, status: STATUS_ENABLED.to_string(), members: vec!["user1".to_string(), "user2".to_string()], update_at: Some(OffsetDateTime::now_utc()), }; assert_eq!(group_info.version, 1); assert_eq!(group_info.status, STATUS_ENABLED); assert_eq!(group_info.members.len(), 2); assert!(group_info.members.contains(&"user1".to_string())); assert!(group_info.members.contains(&"user2".to_string())); } #[test] fn test_update_service_account_opts() { let policy = Policy { id: Default::default(), version: "2012-10-17".to_string(), statements: vec![], }; let opts = UpdateServiceAccountOpts { secret_key: Some("new-secret-key".to_string()), status: Some(STATUS_ENABLED.to_string()), name: Some("service-account-name".to_string()), description: Some("Updated service account".to_string()), expiration: None, session_policy: Some(policy.clone()), }; assert_eq!(opts.secret_key, Some("new-secret-key".to_string())); assert_eq!(opts.status, Some(STATUS_ENABLED.to_string())); assert_eq!(opts.name, Some("service-account-name".to_string())); assert_eq!(opts.description, Some("Updated service account".to_string())); assert!(opts.session_policy.is_some()); assert!(opts.expiration.is_none()); } #[test] fn test_status_constants() { // Test that status constants are properly defined assert_eq!(STATUS_ENABLED, "enabled"); assert_eq!(STATUS_DISABLED, "disabled"); } #[test] fn test_session_policy_constants() { // Test session policy related constants - these are compile-time constants // so we just verify they exist and have expected values assert_eq!(SESSION_POLICY_NAME, "sessionPolicy"); assert_eq!(SESSION_POLICY_NAME_EXTRACTED, "sessionPolicy-extracted"); // MAX_SVCSESSION_POLICY_SIZE is a positive constant defined at compile time assert_eq!(MAX_SVCSESSION_POLICY_SIZE, 4096); // Verify the actual expected value } #[test] fn test_credentials_validation() { let credentials = Credentials { access_key: "AKIAIOSFODNN7EXAMPLE".to_string(), secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string(), session_token: "".to_string(), expiration: None, status: "on".to_string(), parent_user: "".to_string(), groups: None, claims: None, name: None, description: None, }; // Test validation methods assert!(credentials.is_valid()); assert!(!credentials.is_expired()); assert!(!credentials.is_temp()); assert!(!credentials.is_service_account()); } #[test] fn test_credentials_with_session_token() { let credentials = Credentials { access_key: "AKIAIOSFODNN7EXAMPLE".to_string(), secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string(), session_token: "session-token".to_string(), expiration: Some(OffsetDateTime::now_utc() + time::Duration::hours(1)), status: "on".to_string(), parent_user: "".to_string(), groups: None, claims: None, name: None, description: None, }; // Test temp credentials assert!(credentials.is_valid()); assert!(!credentials.is_expired()); assert!(credentials.is_temp()); } #[test] fn test_policy_merge() { let policy1 = Policy { id: Default::default(), version: "2012-10-17".to_string(), statements: vec![], }; let policy2 = Policy { id: Default::default(), version: "2012-10-17".to_string(), statements: vec![], }; let merged = Policy::merge_policies(vec![policy1, policy2]); assert_eq!(merged.version, "2012-10-17"); assert!(merged.statements.is_empty()); assert!(merged.is_empty()); } }