Files
rustfs/crates/iam/src/manager.rs
T
Zhengchao An e3a8234bc9 fix: 12 P1 reliability/security defects from the full-repo audit (backlog#806) (#4256)
* fix(rio): reject corrupted short compressed/encrypted blocks instead of panicking

DecompressReader::poll_read and DecryptReader::poll_read sliced the block
body with a fixed `[0..16]` index to read the length varint. The body length
comes from an untrusted 24-bit header field, so a corrupted/truncated block
shorter than 16 bytes made the slice panic and crash the request task — a
read-path DoS on GET of tiered/corrupted data.

Pass the whole (arbitrary-length-safe) slice to uvarint and reject a
non-positive or out-of-range length prefix with InvalidData. Adds a repro
test for each reader; all existing round-trip tests still pass.

Refs rustfs/backlog#812

* fix(utils): close SSRF bypass via IPv4-mapped IPv6 addresses

validate_outbound_ip branched on the IpAddr variant, and the V6 branch's
is_loopback/is_unicast_link_local/is_unique_local checks never inspect the
embedded IPv4 of an IPv4-mapped address (::ffff:a.b.c.d). The metadata guard
also only matched the plain V4 169.254.169.254. So ::ffff:127.0.0.1,
::ffff:10.0.0.5 and ::ffff:169.254.169.254 all passed the outbound guard,
letting an attacker reach loopback/private/metadata endpoints.

Normalize IPv4-mapped IPv6 to its embedded IPv4 (via to_ipv4_mapped, which
matches only the true mapped form) before classification. Adds reject tests
for mapped loopback/private/metadata and an allow test for public IPv6.

Refs rustfs/backlog#813

* fix(ecstore): streaming last-part loss, GCS tier Range/remove, stat_all_dirs alignment

Four confirmed data-reliability defects:

- put_object_multipart_stream: the CompleteMultipartUpload part-collection loop
  used exclusive `1..total_parts_count`, dropping the final part (and collecting
  zero parts for a single-part object) — silently truncating the completed object.
  Extracted collect_complete_parts (1..=total_parts_count) with unit tests.
- GCS warm backend get() ignored the requested byte range, returning the whole
  object for a Range GET; now applies ReadRange::segment like the other backends.
- GCS warm backend remove() was an empty stub, so deleting a tiered object left
  it on GCS forever; now deletes via StorageControl (added a control-plane client),
  and in_use() actually lists (prefix-scoped) instead of always returning false.
- stat_all_dirs skipped None disk slots and dropped JoinErrors, returning a
  compressed, misaligned error vector; heal_object_dir then zipped it against the
  full disks array and could make_volume on the WRONG disk. Now returns one
  index-aligned entry per slot (None -> DiskNotFound), and heal no longer
  pre-fills the drive report (which would double it). Added an alignment test.

Refs rustfs/backlog#807

* fix(kms): stop Vault backend from destroying/reviving keys on failure

Two confirmed key-safety defects in the Vault KV2 backend:

- get_key_material() 'self-healed' a decrypt or wrong-length failure by minting a
  fresh random master key and overwriting the stored value. That destroys the
  original key material, making every DEK ever wrapped by it permanently
  undecryptable. Decryption must never mutate the stored key: both branches now
  return a cryptographic_error instead. (The empty-material bootstrap path, which
  only fills a never-initialized key, is intentionally left intact.)
- cancel_key_deletion() reset key_state to Enabled only in the returned response
  and never persisted it, so the key stayed PendingDeletion in storage and would
  still be reaped. It now writes the state back via update_key_metadata_in_storage
  and fails the request if the write fails.

Adds ignored (Vault-requiring) integration tests documenting both behaviours.

The third item (VaultTransit key state only in memory -> revived as Enabled after
restart) is deferred: a fail-closed guard would break restart availability for all
transit keys; the correct fix needs a persistent metadata store + Vault integration
testing. Tracked in rustfs/backlog#808.

Refs rustfs/backlog#808

* fix(admin): clamp STS AssumeRole duration; persist ImportBucketMetadata to disk

Two confirmed admin-API defects:

- Standard AssumeRole used the raw client-supplied DurationSeconds with no upper
  bound, so a caller could mint near-permanent temporary credentials. Clamp it to
  the AWS/MinIO STS window [900, 43200] (with 0 -> default 3600) via a shared
  clamp_assume_role_duration helper, and build the exp claim with saturating_add.
  This matches the existing AssumeRoleWithWebIdentity path.
- ImportBucketMetadata only mutated an in-memory map and returned 200, silently
  dropping every imported config. It now persists each non-empty config via
  metadata_sys::update (which merges onto existing on-disk metadata) and returns
  InternalError if a write fails. Mapping extracted to imported_configs_to_persist
  with unit tests.

Refs rustfs/backlog#809

* fix(heal): enqueue displacing request in release builds

push_displacing_lower_priority folded the real enqueue call into
debug_assert_eq!(self.push(request), Accepted). In release builds
(debug_assertions off) the whole macro — including its argument — is compiled
out, so after evicting a lower-priority queued item the new high-priority
request was silently dropped and never healed. Hoist self.push(request) out of
the assertion so the side effect runs in all builds. Adds a --release regression
test.

Refs rustfs/backlog#811

* fix(iam): propagate real delete_policy backend errors instead of swallowing them

delete_policy's is_from_notify path had its error handling inverted: a real
backend failure (disk IO / insufficient quorum) evicted the cache and returned
Ok(()), reporting a phantom success while policy.json survived on disk (to be
reloaded on the next full IAM reload); NoSuchPolicy — which should be idempotent
success — returned Err. Propagate real errors and let NoSuchPolicy fall through
to the idempotent cache-evict + Ok, matching delete_user / the notification
handler in the same file. Adds a backend-error-injection regression test.

Refs rustfs/backlog#810

* fix(utils): also normalize IPv4-compatible IPv6 in the SSRF guard

The initial fix only unwrapped IPv4-mapped (::ffff:a.b.c.d) addresses; the
deprecated IPv4-compatible form (::a.b.c.d, e.g. ::127.0.0.1 / ::169.254.169.254)
still bypassed the guard. Reject pure-IPv6 specials (::, ::1, fe80::, fc00::)
first, then normalize BOTH embedded-IPv4 forms before the IPv4 rules. Adds tests
for compatible-form loopback/metadata and confirms ::1 / :: stay rejected.

Found by adversarial review of the initial fix. Refs rustfs/backlog#813

* fix(ecstore): fix the same last-part loss in the parallel streaming path

put_object_multipart_stream_parallel had the identical off-by-one
(1..total_parts_count) that truncated the last part / produced zero parts for a
single-part upload — reachable when concurrent stream parts are enabled. Reuse
collect_complete_parts, which now returns an error instead of panicking on a gap
in the parts map. Adds a missing-part error test.

Found by adversarial review of the initial fix. Refs rustfs/backlog#807

* fix(kms): local backend must preserve key material on status change

LocalKmsClient (the default KMS backend) regenerated the master key material on
enable_key/disable_key/schedule_key_deletion/cancel_key_deletion — a pure status
change. A single disable+enable cycle therefore destroyed the original key,
making every DEK ever wrapped by it permanently undecryptable (silent data loss,
no network needed). Preserve the existing material via get_key_material and
re-save with only the status changed. Adds a hermetic regression test that wraps
a DEK, cycles all four status methods, and asserts the DEK still decrypts.

Found by adversarial review of the Vault fix. Refs rustfs/backlog#808

* test(rio): cover the length-prefix guard; correct its comment

Add a DecompressReader test that feeds an unterminated length varint so uvarint
returns 0 and the new guard (not the downstream codec) produces the InvalidData
error, and reword the guard comment which overclaimed that the > len bound
prevents a reachable panic (it is belt-and-suspenders). No behavior change.

Found by adversarial review. Refs rustfs/backlog#812

* test(rio): build test block headers via vec! to satisfy clippy

The new corrupted-block tests built the header with Vec::new() + repeated push,
tripping clippy::vec_init_then_push (-D warnings in CI). Construct the fixed
header bytes with vec![] instead. No behavior change.

---------

Co-authored-by: houseme <housemecn@gmail.com>
2026-07-04 14:24:02 +08:00

2969 lines
104 KiB
Rust

// 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::is_iam_first_cluster_node_local;
use crate::sys::{get_claims_from_token_with_secret, get_claims_from_token_with_secret_allow_missing_exp};
use crate::{
cache::{Cache, CacheEntity, LockedCache},
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, SITE_REPLICATOR_CLAIM,
SITE_REPLICATOR_SERVICE_ACCOUNT, STATUS_DISABLED, STATUS_ENABLED, UpdateServiceAccountOpts,
},
};
use futures::future::join_all;
use rustfs_credentials::{Credentials, EMBEDDED_POLICY_TYPE, INHERITED_POLICY_TYPE};
use rustfs_madmin::{AccountStatus, AddOrUpdateUserReq, GroupDesc};
use rustfs_policy::{
arn::ARN,
auth::{self, UserIdentity, is_secret_key_valid, jwt_sign},
format::Format,
policy::{Policy, PolicyDoc, default::DEFAULT_POLICIES, iam_policy_claim_name_sa},
};
use rustfs_utils::{get_env_opt_str, path::path_join_buf};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::sync::atomic::{AtomicU8, AtomicU64};
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::{debug, error};
const IAM_FORMAT_FILE: &str = "format.json";
const IAM_FORMAT_VERSION_1: i32 = 1;
#[cfg(not(test))]
const INITIAL_LOAD_RETRY_DELAY: Duration = Duration::from_secs(1);
#[cfg(test)]
const INITIAL_LOAD_RETRY_DELAY: Duration = Duration::from_millis(1);
#[cfg(not(test))]
const TEMP_USER_PERSISTENCE_VERIFY_RETRY_DELAY: Duration = Duration::from_millis(200);
#[cfg(test)]
const TEMP_USER_PERSISTENCE_VERIFY_RETRY_DELAY: Duration = Duration::from_millis(1);
const TEMP_USER_PERSISTENCE_VERIFY_MAX_RETRIES: usize = 5;
#[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<T> {
pub cache: Cache,
pub api: T,
pub state: Arc<AtomicU8>,
pub loading: Arc<AtomicBool>,
pub roles: HashMap<ARN, Vec<String>>,
pub send_chan: Sender<i64>,
pub last_timestamp: AtomicI64,
pub sync_failures: AtomicU64,
pub sync_successes: AtomicU64,
pub last_sync_duration_millis: AtomicU64,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct IamSyncMetricsSnapshot {
pub last_sync_duration_millis: u64,
pub since_last_sync_millis: u64,
pub sync_failures: u64,
pub sync_successes: u64,
}
impl<T> IamCache<T>
where
T: Store,
{
async fn verify_temp_user_persistence(&self, access_key: &str) -> Result<()> {
for attempt in 0..=TEMP_USER_PERSISTENCE_VERIFY_MAX_RETRIES {
match self.api.load_user_identity(access_key, UserType::Sts).await {
Ok(_) => return Ok(()),
Err(err)
if attempt < TEMP_USER_PERSISTENCE_VERIFY_MAX_RETRIES
&& (is_err_no_such_user(&err) || is_err_config_not_found(&err)) =>
{
warn!(
access_key,
attempt = attempt + 1,
max_retries = TEMP_USER_PERSISTENCE_VERIFY_MAX_RETRIES,
error = ?err,
"temporary IAM user not yet visible after save; retrying persistence verification"
);
tokio::time::sleep(TEMP_USER_PERSISTENCE_VERIFY_RETRY_DELAY).await;
}
Err(err) => return Err(err),
}
}
unreachable!("temp-user persistence verification loop should return on success or terminal error")
}
/// 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) -> Result<Arc<Self>> {
let (sender, receiver) = mpsc::channel::<i64>(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),
sync_failures: AtomicU64::new(0),
sync_successes: AtomicU64::new(0),
last_sync_duration_millis: AtomicU64::new(0),
});
sys.clone().init(receiver).await?;
Ok(sys)
}
/// Initialize the IAM system
async fn init(self: Arc<Self>, receiver: Receiver<i64>) -> 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;
let mut load_error = None;
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);
warn!(
attempts = MAX_RETRIES,
error = ?e,
"IAM initial load failed"
);
load_error = Some(e);
} else {
warn!(attempt = attempt + 1, max_attempts = MAX_RETRIES, "IAM load retry scheduled");
tokio::time::sleep(INITIAL_LOAD_RETRY_DELAY).await;
}
} else {
break;
}
}
if let Some(err) = load_error {
return Err(err);
}
self.state.store(IamState::Ready as u8, Ordering::SeqCst);
debug!(state = "ready", "IAM manager ready");
// Background ticker for synchronization
// Check if environment variable is set
let skip_background_task = get_env_opt_str("RUSTFS_SKIP_BACKGROUND_TASK").is_some();
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() => {
debug!(source = "ticker", "IAM reload tick");
if let Err(err) =s.clone().load().await{
warn!(source = "ticker", error = ?err, "IAM reload failed");
}
},
i = receiver.recv() => {
debug!(source = "receiver", "IAM reload signal received");
match i {
Some(t) => {
let last = s.last_timestamp.load(Ordering::Relaxed);
if last <= t {
debug!(source = "receiver", "IAM reload accepted");
if let Err(err) =s.clone().load().await{
warn!(source = "receiver", error = ?err, "IAM reload failed");
}
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<Self>) -> Result<()> {
let started_at = std::time::Instant::now();
match self.api.load_all(&self.cache).await {
Ok(()) => {
self.last_timestamp
.store(OffsetDateTime::now_utc().unix_timestamp(), Ordering::Relaxed);
self.sync_successes.fetch_add(1, Ordering::Relaxed);
self.last_sync_duration_millis
.store(started_at.elapsed().as_millis().min(u128::from(u64::MAX)) as u64, Ordering::Relaxed);
Ok(())
}
Err(err) => {
self.sync_failures.fetch_add(1, Ordering::Relaxed);
Err(err)
}
}
}
pub fn sync_metrics_snapshot(&self) -> IamSyncMetricsSnapshot {
let now_secs = OffsetDateTime::now_utc().unix_timestamp();
let last_sync_secs = self.last_timestamp.load(Ordering::Relaxed);
let since_last_sync_millis = if last_sync_secs > 0 && now_secs >= last_sync_secs {
((now_secs - last_sync_secs) as u64).saturating_mul(1000)
} else {
0
};
IamSyncMetricsSnapshot {
last_sync_duration_millis: self.last_sync_duration_millis.load(Ordering::Relaxed),
since_last_sync_millis,
sync_failures: self.sync_failures.load(Ordering::Relaxed),
sync_successes: self.sync_successes.load(Ordering::Relaxed),
}
}
pub async fn load_user(&self, access_key: &str) -> Result<()> {
let mut users_map: HashMap<String, UserIdentity> = 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();
match self.api.load_user(access_key, UserType::Svc, &mut users_map).await {
Ok(()) => {}
Err(err) if is_err_no_such_user(&err) => {}
Err(err) => return Err(err),
}
let parent_user = users_map.get(access_key).map(|svc| svc.credentials.parent_user.clone());
if let Some(parent_user) = parent_user {
match self.api.load_user(&parent_user, UserType::Reg, &mut users_map).await {
Ok(()) => {}
Err(err) if is_err_no_such_user(&err) => {}
Err(err) => return Err(err),
}
let _ = self
.api
.load_mapped_policy(&parent_user, UserType::Reg, false, &mut user_policy_map)
.await;
} else {
match self.api.load_user(access_key, UserType::Reg, &mut users_map).await {
Ok(()) => {}
Err(err) if is_err_no_such_user(&err) => {}
Err(err) => return Err(err),
}
if users_map.contains_key(access_key) {
let _ = self
.api
.load_mapped_policy(access_key, UserType::Reg, false, &mut user_policy_map)
.await;
}
match self.api.load_user(access_key, UserType::Sts, &mut sts_users_map).await {
Ok(()) => {}
Err(err) if is_err_no_such_user(&err) => {}
Err(err) => return Err(err),
}
let has_sts_user = sts_users_map.get(access_key);
if let Some(parent) = has_sts_user.map(|sts| sts.credentials.parent_user.clone()) {
let _ = self
.api
.load_mapped_policy(&parent, UserType::Sts, false, &mut sts_policy_map)
.await;
if let Some(plc) = sts_policy_map.get(&parent) {
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;
}
}
}
}
}
for plc in user_policy_map.values() {
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;
}
}
}
self.cache.with_write_lock(|cache| {
let now = OffsetDateTime::now_utc();
for (key, user) in users_map.iter() {
cache.add_or_update_user(key, user, now);
}
for (key, user_policy) in user_policy_map.iter() {
cache.add_or_update_user_policy(key, user_policy, now);
}
for (key, sts_user) in sts_users_map.iter() {
cache.add_or_update_sts_account(key, sts_user, now);
}
for (key, sts_policy) in sts_policy_map.iter() {
cache.add_or_update_sts_policy(key, sts_policy, now);
}
for (key, policy_doc) in policy_docs_map.iter() {
cache.add_or_update_policy_doc(key, policy_doc, now);
}
});
Ok(())
}
// TODO: Check if exists, whether retry is possible
#[tracing::instrument(level = "debug", skip(self))]
async fn save_iam_formatter(self: Arc<Self>) -> 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(());
}
if !is_iam_first_cluster_node_local().await {
return Ok(());
}
self.api.save_iam_config(IAMFormat::new_version_1(), path).await
}
pub async fn get_user(&self, access_key: &str) -> Option<UserIdentity> {
let cache = self.cache.snapshot();
cache
.users
.get(access_key)
.cloned()
.or_else(|| cache.sts_accounts.get(access_key).cloned())
}
pub async fn get_mapped_policy(&self, name: &str, is_group: bool) -> Option<MappedPolicy> {
let cache = self.cache.snapshot();
if is_group {
cache.group_policies.get(name).cloned()
} else {
cache.user_policies.get(name).cloned()
}
}
pub async fn get_policy(&self, name: &str) -> Result<Policy> {
if name.is_empty() {
return Err(Error::InvalidArgument);
}
let policies = MappedPolicy::new(name).to_slice();
let cache = self.cache.snapshot();
let policy_docs = Arc::clone(&cache.policy_docs);
drop(cache);
let mut to_merge = Vec::new();
for policy in policies {
if policy.is_empty() {
continue;
}
let v = policy_docs.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<PolicyDoc> {
if name.is_empty() {
return Err(Error::InvalidArgument);
}
self.cache
.snapshot()
.policy_docs
.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 cache = self.cache.snapshot();
let user_policy_cache = Arc::clone(&cache.user_policies);
let group_policy_cache = Arc::clone(&cache.group_policies);
let users_cache = Arc::clone(&cache.users);
let mut users = Vec::new();
let mut stale_user_policies = Vec::new();
user_policy_cache.iter().for_each(|(k, v)| {
if !users_cache.contains_key(k) {
stale_user_policies.push(k.to_owned());
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 !stale_user_policies.is_empty() {
self.cache.with_write_lock(|cache| {
let now = OffsetDateTime::now_utc();
for user in stale_user_policies.iter() {
cache.delete_user_policy(user, now);
}
});
}
if !users.is_empty() || !groups.is_empty() {
return Err(Error::PolicyInUse);
}
if let Err(err) = self.api.delete_policy_doc(name).await {
// A real backend failure (disk IO, insufficient quorum, etc.) means the on-disk
// policy was NOT removed: propagate the error so callers do not report a phantom
// success and evict a policy that is still persisted (it would reappear on the
// next full IAM reload).
if !is_err_no_such_policy(&err) {
return Err(err);
}
// NoSuchPolicy means the doc is already gone on the backend; treat the delete as
// idempotently successful and fall through to evict any stale cache entry below.
}
}
self.cache.delete_policy_doc(name, OffsetDateTime::now_utc());
Ok(())
}
pub async fn set_policy(&self, name: &str, policy: Policy) -> Result<OffsetDateTime> {
if name.is_empty() || policy.is_empty() {
return Err(Error::InvalidArgument);
}
let cache = self.cache.snapshot();
let policy_doc = cache
.policy_docs
.get(name)
.map(|v| {
let mut p = v.clone();
p.update(policy.clone());
p
})
.unwrap_or_else(|| PolicyDoc::new(policy));
self.api.save_policy_doc(name, policy_doc.clone()).await?;
let now = OffsetDateTime::now_utc();
self.cache.add_or_update_policy_doc(name, &policy_doc, now);
Ok(now)
}
pub async fn list_polices(&self, bucket_name: &str) -> Result<HashMap<String, Policy>> {
let mut m = HashMap::new();
self.api.load_policy_docs(&mut m).await?;
set_default_canned_policies(&mut m);
let policy_docs_cache = CacheEntity::new(m.clone());
self.cache
.with_write_lock(|cache| cache.replace_policy_docs(policy_docs_cache));
let items: Vec<_> = m.into_iter().map(|(k, v)| (k, v.policy)).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();
let cache = self.cache.snapshot();
let policy_docs = Arc::clone(&cache.policy_docs);
drop(cache);
for policy in MappedPolicy::new(name).to_slice() {
if policy.is_empty() {
continue;
}
if let Some(v) = policy_docs.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;
}
self.cache.with_write_lock(|cache| {
let now = OffsetDateTime::now_utc();
for (k, v) in m.iter() {
cache.add_or_update_policy_doc(k, v, now);
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<HashMap<String, PolicyDoc>> {
let mut m = HashMap::new();
self.api.load_policy_docs(&mut m).await?;
set_default_canned_policies(&mut m);
let policy_docs_cache = CacheEntity::new(m.clone());
self.cache
.with_write_lock(|cache| cache.replace_policy_docs(policy_docs_cache));
let items: Vec<_> = m.into_iter().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<HashMap<String, PolicyDoc>> {
let cache = self.cache.snapshot();
let items: Vec<_> = cache.policy_docs.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<Vec<UserIdentity>> {
let cache = self.cache.snapshot();
let users = Arc::clone(&cache.users);
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);
}
}
}
let sts_accounts = Arc::clone(&cache.sts_accounts);
for (_, v) in sts_accounts.iter() {
if v.credentials.parent_user == access_key {
user_exists = true;
if v.credentials.is_temp() && !v.credentials.is_service_account() {
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<Vec<Credentials>> {
let cache = self.cache.snapshot();
let sts_accounts = Arc::clone(&cache.sts_accounts);
Ok(sts_accounts
.values()
.filter_map(|x| {
if !access_key.is_empty()
&& x.credentials.parent_user.as_str() == access_key
&& x.credentials.is_temp()
&& !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())
}
pub async fn list_service_accounts(&self, access_key: &str) -> Result<Vec<Credentials>> {
let cache = self.cache.snapshot();
let users = Arc::clone(&cache.users);
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<OffsetDateTime> {
if cred.access_key.is_empty() || cred.parent_user.is_empty() {
return Err(Error::InvalidArgument);
}
let cache = self.cache.snapshot();
let users = Arc::clone(&cache.users);
if let Some(x) = users.get(&cred.access_key)
&& x.credentials.is_service_account()
{
return Err(Error::IAMActionNotAllowed);
}
drop(cache);
drop(users);
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<OffsetDateTime> {
let cache = self.cache.snapshot();
let Some(ui) = cache.users.get(name).cloned() else {
return Err(Error::NoSuchServiceAccount(name.to_string()));
};
if !ui.credentials.is_service_account() {
return Err(Error::NoSuchServiceAccount(name.to_string()));
}
drop(cache);
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;
}
let token_without_expiration = cr.expiration.is_none();
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<String, Value> = if token_without_expiration {
get_claims_from_token_with_secret_allow_missing_exp(&cr.session_token, &current_secret_key)?
} else {
get_claims_from_token_with_secret(&cr.session_token, &current_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()));
}
}
if let Some(expiration) = opts.expiration {
m.insert("exp".to_owned(), Value::Number(serde_json::Number::from(expiration.unix_timestamp())));
}
m.insert("accessKey".to_owned(), Value::String(name.to_owned()));
if name == SITE_REPLICATOR_SERVICE_ACCOUNT && opts.allow_site_replicator_account {
m.insert(SITE_REPLICATOR_CLAIM.to_owned(), Value::Bool(true));
}
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<Vec<String>>) -> Result<Vec<String>> {
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, _) = match self.policy_db_get_internal(group, true, present).await {
Ok(result) => result,
Err(err) if is_err_no_such_group(&err) => continue,
Err(err) => return Err(err),
};
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<String>, OffsetDateTime)> {
if is_group {
let cache = self.cache.snapshot();
let groups = Arc::clone(&cache.groups);
drop(cache);
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) {
self.cache.add_or_update_group(name, p, OffsetDateTime::now_utc());
}
m.get(name).cloned().ok_or_else(|| Error::NoSuchGroup(name.to_string()))?
}
};
if g.status == STATUS_DISABLED {
return Ok((Vec::new(), OffsetDateTime::now_utc()));
}
let cache = self.cache.snapshot();
let group_policies = Arc::clone(&cache.group_policies);
drop(cache);
if let Some(policy) = group_policies.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
&& !is_err_no_such_policy(&err)
{
return Err(err);
}
if let Some(p) = m.get(name) {
self.cache.add_or_update_group_policy(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 cache = self.cache.snapshot();
let users = Arc::clone(&cache.users);
let user_policies = Arc::clone(&cache.user_policies);
let sts_policies = Arc::clone(&cache.sts_policies);
let groups_cache = Arc::clone(&cache.groups);
let group_policies = Arc::clone(&cache.group_policies);
let user_group_memberships = Arc::clone(&cache.user_group_memberships);
drop(cache);
let u = users.get(name).cloned().unwrap_or_default();
if !u.credentials.is_valid() {
return Ok((Vec::new(), OffsetDateTime::now_utc()));
}
let mp = match user_policies.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
&& !is_err_no_such_policy(&err)
{
return Err(err);
}
if let Some(p) = m.get(name) {
self.cache.add_or_update_user_policy(name, p, OffsetDateTime::now_utc());
p.clone()
} else {
match sts_policies.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
&& !is_err_no_such_policy(&err)
{
return Err(err);
}
if let Some(p) = m.get(name) {
self.cache.add_or_update_sts_policy(name, p, OffsetDateTime::now_utc());
p.clone()
} else {
MappedPolicy::default()
}
}
}
}
}
};
let mut policies: HashSet<String> = mp.to_slice().into_iter().collect();
if let Some(groups) = u.credentials.groups.as_ref() {
for group in groups.iter() {
if groups_cache.get(group).filter(|v| v.status == STATUS_DISABLED).is_some() {
return Ok((Vec::new(), OffsetDateTime::now_utc()));
}
let mp = match group_policies.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
&& !is_err_no_such_policy(&err)
{
return Err(err);
}
if let Some(p) = m.get(group) {
self.cache.add_or_update_group_policy(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 user_group_memberships.get(name).cloned().unwrap_or_default().iter() {
if groups_cache.get(group).filter(|v| v.status == STATUS_DISABLED).is_some() {
return Ok((Vec::new(), OffsetDateTime::now_utc()));
}
let mp = match group_policies.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
&& !is_err_no_such_policy(&err)
{
return Err(err);
}
if let Some(p) = m.get(group) {
self.cache.add_or_update_group_policy(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<OffsetDateTime> {
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
&& !is_err_no_such_policy(&err)
{
return Err(err);
}
if is_group {
self.cache.delete_group_policy(name, OffsetDateTime::now_utc());
} else if user_type == UserType::Sts {
self.cache.delete_sts_policy(name, OffsetDateTime::now_utc());
} else {
self.cache.delete_user_policy(name, OffsetDateTime::now_utc());
}
return Ok(OffsetDateTime::now_utc());
}
let mp = MappedPolicy::new(policy);
let cache = self.cache.snapshot();
let policy_docs_cache = Arc::clone(&cache.policy_docs);
drop(cache);
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 {
self.cache.add_or_update_group_policy(name, &mp, OffsetDateTime::now_utc());
} else if user_type == UserType::Sts {
self.cache.add_or_update_sts_policy(name, &mp, OffsetDateTime::now_utc());
} else {
self.cache.add_or_update_user_policy(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<OffsetDateTime> {
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);
}
let sts_policy_update = 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?;
Some(mp)
} else {
None
};
let u = UserIdentity::new(cred.clone());
self.api
.save_user_identity(access_key, UserType::Sts, u.clone(), None)
.await?;
self.verify_temp_user_persistence(access_key).await?;
let now = self.cache.with_write_lock(|cache| {
let now = OffsetDateTime::now_utc();
if let Some(mp) = &sts_policy_update {
cache.add_or_update_sts_policy(&cred.parent_user, mp, now);
}
cache.add_or_update_sts_account(access_key, &u, now);
now
});
Ok(now)
}
pub async fn get_user_info(&self, name: &str) -> Result<rustfs_madmin::UserInfo> {
let cache = self.cache.snapshot();
let users = Arc::clone(&cache.users);
let policies = Arc::clone(&cache.user_policies);
let group_members = Arc::clone(&cache.user_group_memberships);
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<HashMap<String, rustfs_madmin::UserInfo>> {
let mut m = HashMap::new();
let cache = self.cache.snapshot();
let users = Arc::clone(&cache.users);
let policies = Arc::clone(&cache.user_policies);
let group_members = Arc::clone(&cache.user_group_memberships);
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<HashMap<String, rustfs_madmin::UserInfo>> {
let cache = self.cache.snapshot();
let users = Arc::clone(&cache.users);
let policies_cache = Arc::clone(&cache.user_policies);
let group_members = Arc::clone(&cache.user_group_memberships);
let group_policy_cache = Arc::clone(&cache.group_policies);
let policy_docs_cache = Arc::clone(&cache.policy_docs);
drop(cache);
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_from_docs(policy_docs_cache.as_ref(), &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<String, String> {
let mut m = HashMap::new();
let cache = self.cache.snapshot();
cache.user_policies.iter().for_each(|(k, v)| {
m.insert(k.clone(), v.policies.clone());
});
cache.sts_policies.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<OffsetDateTime> {
let cache = self.cache.snapshot();
let users = Arc::clone(&cache.users);
if let Some(x) = users.get(access_key) {
warn!(error = ?x, "IAM user already exists");
if x.credentials.is_temp() {
return Err(Error::IAMActionNotAllowed);
}
}
drop(cache);
drop(users);
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 {
let cache = self.cache.snapshot();
let member_of = cache.user_group_memberships.get(access_key).cloned();
drop(cache);
if let Some(member_of) = member_of {
for member in member_of.iter() {
let _ = self
.remove_members_from_group(member, vec![access_key.to_string()], false)
.await?;
}
}
let mut service_accounts_to_delete = Vec::new();
let mut temp_accounts_to_delete = HashSet::new();
let cache = self.cache.snapshot();
for (_, v) in cache.users.iter() {
let u = &v.credentials;
if u.parent_user.as_str() == access_key {
if u.is_service_account() {
service_accounts_to_delete.push(u.access_key.clone());
}
if u.is_temp() {
temp_accounts_to_delete.insert(u.access_key.clone());
}
}
}
for (_, v) in cache.sts_accounts.iter() {
let u = &v.credentials;
if u.parent_user.as_str() == access_key {
temp_accounts_to_delete.insert(u.access_key.clone());
}
}
drop(cache);
for access_key in service_accounts_to_delete.iter() {
let _ = self.api.delete_user_identity(access_key, UserType::Svc).await;
}
for access_key in temp_accounts_to_delete.iter() {
let _ = self.api.delete_user_identity(access_key, UserType::Sts).await;
}
self.cache.with_write_lock(|cache| {
let now = OffsetDateTime::now_utc();
for access_key in service_accounts_to_delete.iter() {
cache.delete_user(access_key, now);
}
for access_key in temp_accounts_to_delete.iter() {
cache.delete_sts_account(access_key, now);
cache.delete_user(access_key, now);
}
});
}
let _ = self.api.delete_mapped_policy(access_key, utype, false).await;
self.cache.delete_user_policy(access_key, OffsetDateTime::now_utc());
if let Err(err) = self.api.delete_user_identity(access_key, utype).await
&& !is_err_no_such_user(&err)
{
return Err(err);
}
self.cache.with_write_lock(|cache| {
let now = OffsetDateTime::now_utc();
if utype == UserType::Sts {
cache.delete_sts_account(access_key, now);
}
cache.delete_user(access_key, now);
});
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 cache = self.cache.snapshot();
let users = Arc::clone(&cache.users);
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);
drop(cache);
drop(users);
self.api
.save_user_identity(access_key, UserType::Reg, u.clone(), None)
.await?;
self.update_user_with_claims(access_key, u)
}
/// Add SSH public key for a user (for SFTP authentication)
pub async fn add_user_ssh_public_key(&self, access_key: &str, public_key: &str) -> Result<()> {
if access_key.is_empty() || public_key.is_empty() {
return Err(Error::InvalidArgument);
}
let cache = self.cache.snapshot();
let users = Arc::clone(&cache.users);
let u = match users.get(access_key) {
Some(u) => u,
None => return Err(Error::NoSuchUser(access_key.to_string())),
};
let mut user_identity = u.clone();
user_identity.add_ssh_public_key(public_key);
drop(cache);
drop(users);
self.api
.save_user_identity(access_key, UserType::Reg, user_identity.clone(), None)
.await?;
self.update_user_with_claims(access_key, user_identity)
}
pub async fn set_user_status(&self, access_key: &str, status: AccountStatus) -> Result<OffsetDateTime> {
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 cache = self.cache.snapshot();
let users = Arc::clone(&cache.users);
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()
});
drop(cache);
drop(users);
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(if u.credentials.expiration.is_none() {
extract_jwt_claims_allow_missing_exp(&u)?
} else {
extract_jwt_claims(&u)?
});
}
if u.credentials.is_temp() && !u.credentials.is_service_account() {
self.cache.add_or_update_sts_account(k, &u, OffsetDateTime::now_utc());
} else {
self.cache.add_or_update_user(k, &u, OffsetDateTime::now_utc());
}
Ok(())
}
pub async fn is_temp_user(&self, access_key: &str) -> Result<(bool, String)> {
let u = match self.get_user(access_key).await {
Some(u) => u,
None => return Err(Error::NoSuchUser(access_key.to_string())),
};
if u.credentials.is_temp() {
Ok((true, u.credentials.parent_user))
} else {
Ok((false, String::new()))
}
}
pub async fn add_users_to_group(&self, group: &str, members: Vec<String>) -> Result<OffsetDateTime> {
if group.is_empty() {
return Err(Error::InvalidArgument);
}
let cache = self.cache.snapshot();
let users_cache = Arc::clone(&cache.users);
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 cache.groups.get(group) {
Some(res) => {
let mut gi = res.clone();
let mut uniq_set: HashSet<String, std::collections::hash_map::RandomState> =
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()),
};
drop(cache);
self.api.save_group_info(group, gi.clone()).await?;
let now = self.cache.with_write_lock(|cache| {
let now = OffsetDateTime::now_utc();
cache.add_or_update_group(group, &gi, now);
let user_group_memberships = Arc::clone(&cache.state().user_group_memberships);
members.iter().for_each(|member| {
let mut m = user_group_memberships.get(member).cloned().unwrap_or_default();
m.insert(group.to_string());
cache.add_or_update_user_group_membership(member, &m, now);
});
now
});
Ok(now)
}
pub async fn set_group_status(&self, name: &str, enable: bool) -> Result<OffsetDateTime> {
if name.is_empty() {
return Err(Error::InvalidArgument);
}
let cache = self.cache.snapshot();
let groups = Arc::clone(&cache.groups);
let mut gi = match groups.get(name) {
Some(gi) => gi.clone(),
None => return Err(Error::NoSuchGroup(name.to_string())),
};
drop(cache);
if enable {
gi.status = STATUS_ENABLED.to_owned();
} else {
gi.status = STATUS_DISABLED.to_owned();
}
self.api.save_group_info(name, gi.clone()).await?;
self.cache.add_or_update_group(name, &gi, OffsetDateTime::now_utc());
Ok(OffsetDateTime::now_utc())
}
pub async fn get_group_description(&self, name: &str) -> Result<GroupDesc> {
let cache = self.cache.snapshot();
let gi = cache
.groups
.get(name)
.cloned()
.ok_or_else(|| Error::NoSuchGroup(name.to_string()))?;
let cached_mapped_policy = cache.group_policies.get(name).cloned();
drop(cache);
let mapped_policy = if let Some(policy) = cached_mapped_policy {
Some(policy)
} else {
let mut policies = HashMap::new();
if let Err(err) = self.api.load_mapped_policy(name, UserType::Reg, true, &mut policies).await
&& !is_err_no_such_policy(&err)
{
return Err(err);
}
if let Some(policy) = policies.get(name).cloned() {
self.cache
.add_or_update_group_policy(name, &policy, OffsetDateTime::now_utc());
Some(policy)
} else {
None
}
};
Ok(build_group_desc(name, gi, mapped_policy))
}
pub async fn list_groups(&self) -> Result<Vec<String>> {
Ok(self.cache.snapshot().groups.keys().cloned().collect())
}
pub async fn update_groups(&self) -> Result<Vec<String>> {
let mut groups_set = HashSet::new();
let mut groups = HashMap::new();
self.api.load_groups(&mut groups).await?;
let mut group_policies = HashMap::new();
self.api
.load_mapped_policies(UserType::Reg, true, &mut group_policies)
.await?;
self.cache.with_write_lock(|cache| {
let now = OffsetDateTime::now_utc();
for (group, gi) in groups.iter() {
cache.add_or_update_group(group, gi, now);
groups_set.insert(group.to_string());
}
for (group, gi) in group_policies.iter() {
cache.add_or_update_group_policy(group, gi, now);
groups_set.insert(group.to_string());
}
});
Ok(groups_set.into_iter().collect())
}
pub async fn remove_members_from_group(
&self,
name: &str,
members: Vec<String>,
update_cache_only: bool,
) -> Result<OffsetDateTime> {
let cache = self.cache.snapshot();
let mut gi = cache
.groups
.get(name)
.cloned()
.ok_or_else(|| Error::NoSuchGroup(name.to_string()))?;
drop(cache);
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::<Vec<String>>();
if !update_cache_only {
self.api.save_group_info(name, gi.clone()).await?;
}
let now = self.cache.with_write_lock(|cache| {
let now = OffsetDateTime::now_utc();
cache.add_or_update_group(name, &gi, now);
let user_group_memberships = Arc::clone(&cache.state().user_group_memberships);
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_user_group_membership(member, &m, now);
}
});
now
});
Ok(now)
}
pub async fn remove_users_from_group(&self, group: &str, members: Vec<String>) -> Result<OffsetDateTime> {
if group.is_empty() {
return Err(Error::InvalidArgument);
}
let cache = self.cache.snapshot();
let users_cache = Arc::clone(&cache.users);
let groups_cache = Arc::clone(&cache.groups);
drop(cache);
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 = if members.is_empty() {
// Reload from backend so we see latest members (e.g. after user was deleted elsewhere)
let mut m = HashMap::new();
self.api.load_group(group, &mut m).await?;
m.get(group).cloned().ok_or_else(|| Error::NoSuchGroup(group.to_string()))?
} else {
groups_cache
.get(group)
.cloned()
.ok_or_else(|| 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
&& !is_err_no_such_policy(&err)
{
return Err(err);
}
if let Err(err) = self.api.delete_group_info(group).await
&& !is_err_no_such_group(&err)
{
return Err(err);
}
let now = self.cache.with_write_lock(|cache| {
let now = OffsetDateTime::now_utc();
self.remove_group_from_memberships_map_unlocked(cache, group, now);
cache.delete_group(group, now);
cache.delete_group_policy(group, now);
now
});
return Ok(now);
}
self.remove_members_from_group(group, members, false).await
}
fn remove_group_from_memberships_map_unlocked(&self, cache: &mut LockedCache, group: &str, now: OffsetDateTime) {
let user_group_memberships = Arc::clone(&cache.state().user_group_memberships);
for (k, v) in user_group_memberships.iter() {
if v.contains(group) {
let mut m = v.clone();
m.remove(group);
cache.add_or_update_user_group_membership(k, &m, now);
}
}
}
fn update_group_memberships_map_unlocked(&self, cache: &mut LockedCache, group: &str, gi: &GroupInfo, now: OffsetDateTime) {
let user_group_memberships = Arc::clone(&cache.state().user_group_memberships);
for member in gi.members.iter() {
let mut m = user_group_memberships.get(member).cloned().unwrap_or_default();
m.insert(group.to_string());
cache.add_or_update_user_group_membership(member, &m, now);
}
}
fn remove_user_from_cached_groups(&self, cache: &mut LockedCache, user: &str, now: OffsetDateTime) {
let member_of = cache.state().user_group_memberships.get(user).cloned().unwrap_or_default();
for group in member_of.iter() {
if let Some(group_info) = cache.state().groups.get(group).cloned() {
let mut group_info = group_info;
group_info.members.retain(|member| member != user);
cache.add_or_update_group(group, &group_info, now);
}
}
cache.delete_user_group_membership(user, now);
}
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.cache.with_write_lock(|cache| {
let now = OffsetDateTime::now_utc();
self.remove_group_from_memberships_map_unlocked(cache, group, now);
cache.delete_group(group, now);
cache.delete_group_policy(group, now);
});
return Ok(());
}
let gi = m[group].clone();
self.cache.with_write_lock(|cache| {
let now = OffsetDateTime::now_utc();
cache.add_or_update_group(group, &gi, now);
self.remove_group_from_memberships_map_unlocked(cache, group, now);
self.update_group_memberships_map_unlocked(cache, group, &gi, now);
});
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);
}
self.cache.with_write_lock(|cache| {
let now = OffsetDateTime::now_utc();
cache.delete_policy_doc(policy, now);
let user_policy_cache = Arc::clone(&cache.state().user_policies);
let users_cache = Arc::clone(&cache.state().users);
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_user_policy(k, now);
continue;
}
set.remove(policy);
let mp = MappedPolicy::new(&set.iter().cloned().collect::<Vec<_>>().join(","));
cache.add_or_update_user_policy(k, &mp, now);
}
}
let group_policy_cache = Arc::clone(&cache.state().group_policies);
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::<Vec<_>>().join(","));
cache.add_or_update_group_policy(k, &mp, now);
}
}
});
return Ok(());
}
self.cache.with_write_lock(|cache| {
cache.add_or_update_policy_doc(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);
}
self.cache.with_write_lock(|cache| {
let now = OffsetDateTime::now_utc();
if is_group {
cache.delete_group_policy(name, now);
} else if user_type == UserType::Sts {
cache.delete_sts_policy(name, now);
} else {
cache.delete_user_policy(name, now);
}
});
return Ok(());
}
let mp = m[name].clone();
self.cache.with_write_lock(|cache| {
let now = OffsetDateTime::now_utc();
if is_group {
cache.add_or_update_group_policy(name, &mp, now);
} else if user_type == UserType::Sts {
cache.add_or_update_sts_policy(name, &mp, now);
} else {
cache.add_or_update_user_policy(name, &mp, now);
}
});
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);
}
let mut service_accounts_to_delete = Vec::new();
let mut temp_accounts_to_delete = HashSet::new();
if user_type == UserType::Reg {
let cache = self.cache.snapshot();
for (_, v) in cache.users.iter() {
let u = &v.credentials;
if u.parent_user.as_str() == name && u.is_service_account() {
service_accounts_to_delete.push(u.access_key.clone());
}
}
for (_, u) in cache.sts_accounts.iter() {
let u = &u.credentials;
if u.parent_user.as_str() == name {
temp_accounts_to_delete.insert(u.access_key.clone());
}
}
drop(cache);
for access_key in service_accounts_to_delete.iter() {
let _ = self.api.delete_user_identity(access_key, UserType::Svc).await;
}
for access_key in temp_accounts_to_delete.iter() {
let _ = self.api.delete_user_identity(access_key, UserType::Sts).await;
}
}
self.cache.with_write_lock(|cache| {
let now = OffsetDateTime::now_utc();
match user_type {
UserType::Sts => cache.delete_sts_account(name, now),
UserType::Reg | UserType::Svc => cache.delete_user(name, now),
UserType::None => {}
}
self.remove_user_from_cached_groups(cache, name, now);
if user_type == UserType::Reg {
for access_key in service_accounts_to_delete.iter() {
cache.delete_user(access_key, now);
}
for access_key in temp_accounts_to_delete.iter() {
cache.delete_sts_account(access_key, now);
cache.delete_user(access_key, now);
}
}
cache.delete_user_policy(name, now);
});
return Ok(());
}
let u = m[name].clone();
let mut user_policy_update = None;
let mut sts_policy_update = None;
match user_type {
UserType::Sts => {
let parent_user = u.credentials.parent_user.clone();
let mut policies = HashMap::new();
if let Err(err) = self
.api
.load_mapped_policy(&parent_user, user_type, false, &mut policies)
.await
{
if !is_err_no_such_policy(&err) {
return Err(err);
}
} else if let Some(policy) = policies.get(&parent_user).cloned() {
sts_policy_update = Some((parent_user, policy));
}
}
UserType::Reg => {
let mut policies = HashMap::new();
if let Err(err) = self.api.load_mapped_policy(name, user_type, false, &mut policies).await {
if !is_err_no_such_policy(&err) {
return Err(err);
}
} else if let Some(policy) = policies.get(name).cloned() {
user_policy_update = Some((name.to_string(), policy));
}
}
UserType::Svc => {
let parent_user = u.credentials.parent_user.clone();
let cache = self.cache.snapshot();
let parent_user_type = if cache.users.contains_key(&parent_user) {
UserType::Reg
} else {
UserType::Sts
};
drop(cache);
if parent_user_type == UserType::Reg {
let mut policies = HashMap::new();
if let Err(err) = self
.api
.load_mapped_policy(&parent_user, UserType::Reg, false, &mut policies)
.await
{
if !is_err_no_such_policy(&err) {
return Err(err);
}
} else if let Some(policy) = policies.get(&parent_user).cloned() {
user_policy_update = Some((parent_user, policy));
}
} else {
let mut policies = HashMap::new();
if let Err(err) = self
.api
.load_mapped_policy(&parent_user, UserType::Sts, false, &mut policies)
.await
{
if !is_err_no_such_policy(&err) {
return Err(err);
}
} else if let Some(policy) = policies.get(&parent_user).cloned() {
sts_policy_update = Some((parent_user, policy));
}
}
}
UserType::None => {}
}
self.cache.with_write_lock(|cache| {
let now = OffsetDateTime::now_utc();
match user_type {
UserType::Sts => {
cache.add_or_update_sts_account(name, &u, now);
}
UserType::Reg | UserType::Svc => {
cache.add_or_update_user(name, &u, now);
}
UserType::None => {}
}
if let Some((name, policy)) = &user_policy_update {
cache.add_or_update_user_policy(name, policy, now);
}
if let Some((name, policy)) = &sts_policy_update {
cache.add_or_update_sts_policy(name, policy, now);
}
});
Ok(())
}
}
pub fn get_default_policyes() -> HashMap<String, PolicyDoc> {
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<String, PolicyDoc>) {
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<String> {
crate::root_credentials::token_signing_key()
}
pub fn extract_jwt_claims(u: &UserIdentity) -> Result<HashMap<String, Value>> {
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"))
}
pub fn extract_jwt_claims_allow_missing_exp(u: &UserIdentity) -> Result<HashMap<String, Value>> {
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_allow_missing_exp(&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 cache = cache.snapshot();
filter_policies_from_docs(cache.policy_docs.as_ref(), policy_name, bucket_name)
}
fn filter_policies_from_docs(policy_docs: &CacheEntity<PolicyDoc>, 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) = policy_docs.get(&policy)
&& (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))
}
fn build_group_desc(name: &str, group_info: GroupInfo, mapped_policy: Option<MappedPolicy>) -> GroupDesc {
let (policy, updated_at) = mapped_policy
.map(|policy| (policy.policies, Some(policy.update_at)))
.unwrap_or_else(|| (String::new(), Some(OffsetDateTime::now_utc())));
GroupDesc {
name: name.to_string(),
policy,
members: group_info.members,
updated_at,
status: group_info.status,
}
}
#[cfg(test)]
mod tests {
use super::*;
use rustfs_policy::policy::{Policy, PolicyDoc};
use serde_json::json;
use std::{
collections::HashMap,
sync::{
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
},
};
#[derive(Clone)]
struct FailingInitialLoadStore;
#[async_trait::async_trait]
impl Store for FailingInitialLoadStore {
fn has_watcher(&self) -> bool {
false
}
async fn save_iam_config<Item: Serialize + Send>(&self, _item: Item, _path: impl AsRef<str> + Send) -> Result<()> {
Ok(())
}
async fn load_iam_config<Item: serde::de::DeserializeOwned>(&self, _path: impl AsRef<str> + Send) -> Result<Item> {
Err(Error::ConfigNotFound)
}
async fn delete_iam_config(&self, _path: impl AsRef<str> + Send) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn save_user_identity(
&self,
_name: &str,
_user_type: UserType,
_item: UserIdentity,
_ttl: Option<usize>,
) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn delete_user_identity(&self, _name: &str, _user_type: UserType) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn load_user_identity(&self, _name: &str, _user_type: UserType) -> Result<UserIdentity> {
Err(Error::InvalidArgument)
}
async fn load_user(&self, _name: &str, _user_type: UserType, _m: &mut HashMap<String, UserIdentity>) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn load_users(&self, _user_type: UserType, _m: &mut HashMap<String, UserIdentity>) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn load_secret_key(&self, _name: &str, _user_type: UserType) -> Result<String> {
Err(Error::InvalidArgument)
}
async fn save_group_info(&self, _name: &str, _item: GroupInfo) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn delete_group_info(&self, _name: &str) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn load_group(&self, _name: &str, _m: &mut HashMap<String, GroupInfo>) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn load_groups(&self, _m: &mut HashMap<String, GroupInfo>) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn save_policy_doc(&self, _name: &str, _item: PolicyDoc) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn delete_policy_doc(&self, _name: &str) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn load_policy(&self, _name: &str) -> Result<PolicyDoc> {
Err(Error::InvalidArgument)
}
async fn load_policy_doc(&self, _name: &str, _m: &mut HashMap<String, PolicyDoc>) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn load_policy_docs(&self, _m: &mut HashMap<String, PolicyDoc>) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn save_mapped_policy(
&self,
_name: &str,
_user_type: UserType,
_is_group: bool,
_item: MappedPolicy,
_ttl: Option<usize>,
) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn delete_mapped_policy(&self, _name: &str, _user_type: UserType, _is_group: bool) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn load_mapped_policy(
&self,
_name: &str,
_user_type: UserType,
_is_group: bool,
_m: &mut HashMap<String, MappedPolicy>,
) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn load_mapped_policies(
&self,
_user_type: UserType,
_is_group: bool,
_m: &mut HashMap<String, MappedPolicy>,
) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn load_all(&self, _cache: &Cache) -> Result<()> {
Err(Error::Io(std::io::Error::other("initial load failed")))
}
}
#[derive(Clone)]
struct DelayedTempUserVisibilityStore {
saved_user: Arc<Mutex<Option<UserIdentity>>>,
load_attempts: Arc<AtomicUsize>,
visible_after_attempt: usize,
}
impl DelayedTempUserVisibilityStore {
fn new(visible_after_attempt: usize) -> Self {
Self {
saved_user: Arc::new(Mutex::new(None)),
load_attempts: Arc::new(AtomicUsize::new(0)),
visible_after_attempt,
}
}
}
#[async_trait::async_trait]
impl Store for DelayedTempUserVisibilityStore {
fn has_watcher(&self) -> bool {
false
}
async fn save_iam_config<Item: Serialize + Send>(&self, _item: Item, _path: impl AsRef<str> + Send) -> Result<()> {
Ok(())
}
async fn load_iam_config<Item: serde::de::DeserializeOwned>(&self, _path: impl AsRef<str> + Send) -> Result<Item> {
Err(Error::ConfigNotFound)
}
async fn delete_iam_config(&self, _path: impl AsRef<str> + Send) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn save_user_identity(
&self,
_name: &str,
_user_type: UserType,
item: UserIdentity,
_ttl: Option<usize>,
) -> Result<()> {
*self.saved_user.lock().expect("saved_user mutex poisoned") = Some(item);
Ok(())
}
async fn delete_user_identity(&self, _name: &str, _user_type: UserType) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn load_user_identity(&self, name: &str, _user_type: UserType) -> Result<UserIdentity> {
let attempt = self.load_attempts.fetch_add(1, Ordering::SeqCst) + 1;
if attempt <= self.visible_after_attempt {
return Err(Error::NoSuchUser(name.to_string()));
}
self.saved_user
.lock()
.expect("saved_user mutex poisoned")
.clone()
.ok_or_else(|| Error::NoSuchUser(name.to_string()))
}
async fn load_user(&self, _name: &str, _user_type: UserType, _m: &mut HashMap<String, UserIdentity>) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn load_users(&self, _user_type: UserType, _m: &mut HashMap<String, UserIdentity>) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn load_secret_key(&self, _name: &str, _user_type: UserType) -> Result<String> {
Err(Error::InvalidArgument)
}
async fn save_group_info(&self, _name: &str, _item: GroupInfo) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn delete_group_info(&self, _name: &str) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn load_group(&self, _name: &str, _m: &mut HashMap<String, GroupInfo>) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn load_groups(&self, _m: &mut HashMap<String, GroupInfo>) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn save_policy_doc(&self, _name: &str, _item: PolicyDoc) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn delete_policy_doc(&self, _name: &str) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn load_policy(&self, _name: &str) -> Result<PolicyDoc> {
Err(Error::InvalidArgument)
}
async fn load_policy_doc(&self, _name: &str, _m: &mut HashMap<String, PolicyDoc>) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn load_policy_docs(&self, _m: &mut HashMap<String, PolicyDoc>) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn save_mapped_policy(
&self,
_name: &str,
_user_type: UserType,
_is_group: bool,
_item: MappedPolicy,
_ttl: Option<usize>,
) -> Result<()> {
Ok(())
}
async fn delete_mapped_policy(&self, _name: &str, _user_type: UserType, _is_group: bool) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn load_mapped_policy(
&self,
_name: &str,
_user_type: UserType,
_is_group: bool,
_m: &mut HashMap<String, MappedPolicy>,
) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn load_mapped_policies(
&self,
_user_type: UserType,
_is_group: bool,
_m: &mut HashMap<String, MappedPolicy>,
) -> Result<()> {
Err(Error::InvalidArgument)
}
async fn load_all(&self, _cache: &Cache) -> Result<()> {
Ok(())
}
}
fn build_test_temp_credentials() -> Credentials {
Credentials {
access_key: "STSACCESS123".to_string(),
secret_key: "test-secret-key".to_string(),
session_token: "test-session-token".to_string(),
expiration: Some(OffsetDateTime::now_utc() + time::Duration::hours(1)),
status: STATUS_ENABLED.to_string(),
parent_user: "parent-user".to_string(),
groups: Some(vec!["console-admins".to_string()]),
claims: None,
name: None,
description: None,
}
}
fn build_test_iam_cache<T: Store>(api: T) -> IamCache<T> {
let (sender, _receiver) = mpsc::channel::<i64>(1);
IamCache {
cache: Cache::default(),
api,
state: Arc::new(AtomicU8::new(IamState::Ready as u8)),
loading: Arc::new(AtomicBool::new(false)),
roles: HashMap::new(),
send_chan: sender,
last_timestamp: AtomicI64::new(0),
sync_failures: AtomicU64::new(0),
sync_successes: AtomicU64::new(0),
last_sync_duration_millis: AtomicU64::new(0),
}
}
#[tokio::test]
async fn set_temp_user_retries_until_sts_identity_becomes_visible() {
let store = DelayedTempUserVisibilityStore::new(2);
let cache = build_test_iam_cache(store.clone());
let cred = build_test_temp_credentials();
let updated_at = cache
.set_temp_user(&cred.access_key, &cred, None)
.await
.expect("temp user should succeed once persistence becomes visible");
assert!(updated_at <= OffsetDateTime::now_utc());
assert_eq!(store.load_attempts.load(Ordering::SeqCst), 3);
let snapshot = cache.cache.snapshot();
let sts_identity = snapshot
.sts_accounts
.get(&cred.access_key)
.expect("verified temp user should be cached");
assert_eq!(sts_identity.credentials.parent_user, cred.parent_user);
}
#[tokio::test]
async fn set_temp_user_fails_when_sts_identity_never_becomes_visible() {
let store = DelayedTempUserVisibilityStore::new(TEMP_USER_PERSISTENCE_VERIFY_MAX_RETRIES + 1);
let cache = build_test_iam_cache(store.clone());
let cred = build_test_temp_credentials();
let err = cache
.set_temp_user(&cred.access_key, &cred, None)
.await
.expect_err("temp user should fail when persistence never becomes visible");
assert!(matches!(err, Error::NoSuchUser(user) if user == cred.access_key));
assert_eq!(store.load_attempts.load(Ordering::SeqCst), TEMP_USER_PERSISTENCE_VERIFY_MAX_RETRIES + 1);
let snapshot = cache.cache.snapshot();
assert!(
!snapshot.sts_accounts.contains_key(&cred.access_key),
"cache must not publish a temp user that was not durably visible"
);
}
#[tokio::test]
async fn test_init_keeps_error_state_when_initial_load_fails() {
let (sender, receiver) = mpsc::channel::<i64>(1);
let sys = Arc::new(IamCache {
api: FailingInitialLoadStore,
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),
sync_failures: AtomicU64::new(0),
sync_successes: AtomicU64::new(0),
last_sync_duration_millis: AtomicU64::new(0),
});
let result = Arc::clone(&sys).init(receiver).await;
assert!(matches!(result, Err(Error::Io(_))));
assert!(!sys.is_ready());
assert_eq!(sys.state.load(Ordering::SeqCst), IamState::Error as u8);
assert_eq!(sys.sync_failures.load(Ordering::Relaxed), 3);
}
#[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_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),
allow_site_replicator_account: false,
};
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());
}
#[test]
fn test_build_group_desc_preserves_policy_for_disabled_group() {
let group_info = GroupInfo {
status: STATUS_DISABLED.to_string(),
members: vec!["alice".to_string()],
..Default::default()
};
let mapped_policy = MappedPolicy::new("readonly");
let desc = build_group_desc("ops", group_info, Some(mapped_policy));
assert_eq!(desc.name, "ops");
assert_eq!(desc.status, STATUS_DISABLED);
assert_eq!(desc.policy, "readonly");
assert_eq!(desc.members, vec!["alice".to_string()]);
assert!(desc.updated_at.is_some());
}
#[tokio::test]
async fn delete_policy_propagates_backend_error_and_keeps_cache() {
// Regression: on the admin delete path (is_from_notify = true), a real backend delete
// failure (here Error::InvalidArgument, standing in for disk IO / insufficient quorum)
// must propagate — NOT be swallowed as Ok(()) while evicting the still-persisted policy.
let cache = build_test_iam_cache(FailingInitialLoadStore);
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()),
};
cache
.cache
.add_or_update_policy_doc("permissive-policy", &policy_doc, OffsetDateTime::now_utc());
let result = cache.delete_policy("permissive-policy", true).await;
// Pre-fix this returned Ok(()) (phantom success) and evicted the cache entry.
assert!(
result.is_err(),
"delete_policy must surface a real backend delete failure instead of reporting success"
);
assert!(
cache.cache.snapshot().policy_docs.contains_key("permissive-policy"),
"cache must not evict a policy whose backend delete failed"
);
}
}