refactor: Restructure project layout and clean up dependencies (#30)

This commit introduces a significant reorganization of the project structure to improve maintainability and clarity.

Key changes include:
- Adjusted the directory layout for a more logical module organization.
- Removed unused crate dependencies, reducing the overall project size and potentially speeding up build times.
- Updated import paths and configuration files to reflect the structural changes.
This commit is contained in:
houseme
2025-07-02 19:33:12 +08:00
committed by GitHub
parent 0be4264eb1
commit 5826396cd0
322 changed files with 977 additions and 1542 deletions
+350
View File
@@ -0,0 +1,350 @@
// 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 std::{
collections::{HashMap, HashSet},
ops::{Deref, DerefMut},
ptr,
sync::Arc,
};
use arc_swap::{ArcSwap, AsRaw, Guard};
use rustfs_policy::{
auth::UserIdentity,
policy::{Args, PolicyDoc},
};
use time::OffsetDateTime;
use tracing::warn;
use crate::store::{GroupInfo, MappedPolicy};
pub struct Cache {
pub policy_docs: ArcSwap<CacheEntity<PolicyDoc>>,
pub users: ArcSwap<CacheEntity<UserIdentity>>,
pub user_policies: ArcSwap<CacheEntity<MappedPolicy>>,
pub sts_accounts: ArcSwap<CacheEntity<UserIdentity>>,
pub sts_policies: ArcSwap<CacheEntity<MappedPolicy>>,
pub groups: ArcSwap<CacheEntity<GroupInfo>>,
pub user_group_memberships: ArcSwap<CacheEntity<HashSet<String>>>,
pub group_policies: ArcSwap<CacheEntity<MappedPolicy>>,
}
impl Default for Cache {
fn default() -> Self {
Self {
policy_docs: ArcSwap::new(Arc::new(CacheEntity::default())),
users: ArcSwap::new(Arc::new(CacheEntity::default())),
user_policies: ArcSwap::new(Arc::new(CacheEntity::default())),
sts_accounts: ArcSwap::new(Arc::new(CacheEntity::default())),
sts_policies: ArcSwap::new(Arc::new(CacheEntity::default())),
groups: ArcSwap::new(Arc::new(CacheEntity::default())),
user_group_memberships: ArcSwap::new(Arc::new(CacheEntity::default())),
group_policies: ArcSwap::new(Arc::new(CacheEntity::default())),
}
}
}
impl Cache {
pub fn ptr_eq<Base, A, B>(a: A, b: B) -> bool
where
A: AsRaw<Base>,
B: AsRaw<Base>,
{
let a = a.as_raw();
let b = b.as_raw();
ptr::eq(a, b)
}
fn exec<T: Clone>(target: &ArcSwap<CacheEntity<T>>, t: OffsetDateTime, mut op: impl FnMut(&mut CacheEntity<T>)) {
let mut cur = target.load();
loop {
// If the current update time is later than the execution time,
// the background task is loaded and the current operation does not need to be performed.
if cur.load_time >= t {
return;
}
let mut new = CacheEntity::clone(&cur);
op(&mut new);
// Replace content with CAS atoms
let prev = target.compare_and_swap(&*cur, Arc::new(new));
let swapped = Self::ptr_eq(&*cur, &*prev);
if swapped {
return;
} else {
cur = prev;
}
}
}
pub fn add_or_update<T: Clone>(target: &ArcSwap<CacheEntity<T>>, key: &str, value: &T, t: OffsetDateTime) {
Self::exec(target, t, |map: &mut CacheEntity<T>| {
map.insert(key.to_string(), value.clone());
})
}
pub fn delete<T: Clone>(target: &ArcSwap<CacheEntity<T>>, key: &str, t: OffsetDateTime) {
Self::exec(target, t, |map: &mut CacheEntity<T>| {
map.remove(key);
})
}
pub fn build_user_group_memberships(&self) {
let groups = self.groups.load();
let mut user_group_memberships = HashMap::new();
for (group_name, group) in groups.iter() {
for user_name in &group.members {
user_group_memberships
.entry(user_name.clone())
.or_insert_with(HashSet::new)
.insert(group_name.clone());
}
}
self.user_group_memberships
.store(Arc::new(CacheEntity::new(user_group_memberships)));
}
}
impl CacheInner {
#[inline]
pub fn get_user(&self, user_name: &str) -> Option<&UserIdentity> {
self.users.get(user_name).or_else(|| self.sts_accounts.get(user_name))
}
// fn get_policy(&self, _name: &str, _groups: &[String]) -> crate::Result<Vec<Policy>> {
// todo!()
// }
// /// 如果是临时用户,返回 Ok(Some(partent_name)))
// /// 如果不是临时用户,返回 Ok(None)
// fn is_temp_user(&self, user_name: &str) -> crate::Result<Option<&str>> {
// let user = self
// .get_user(user_name)
// .ok_or_else(|| Error::NoSuchUser(user_name.to_owned()))?;
// if user.credentials.is_temp() {
// Ok(Some(&user.credentials.parent_user))
// } else {
// Ok(None)
// }
// }
// /// 如果是临时用户,返回 Ok(Some(partent_name)))
// /// 如果不是临时用户,返回 Ok(None)
// fn is_service_account(&self, user_name: &str) -> crate::Result<Option<&str>> {
// let user = self
// .get_user(user_name)
// .ok_or_else(|| Error::NoSuchUser(user_name.to_owned()))?;
// if user.credentials.is_service_account() {
// Ok(Some(&user.credentials.parent_user))
// } else {
// Ok(None)
// }
// }
// todo
pub fn is_allowed_sts(&self, _args: &Args, _parent: &str) -> bool {
warn!("unimplement is_allowed_sts");
false
}
// todo
pub fn is_allowed_service_account(&self, _args: &Args, _parent: &str) -> bool {
warn!("unimplement is_allowed_sts");
false
}
pub fn is_allowed(&self, _args: Args) -> bool {
todo!()
}
pub fn policy_db_get(&self, _name: &str, _groups: &[String]) -> Vec<String> {
todo!()
}
}
#[derive(Clone)]
pub struct CacheEntity<T> {
map: HashMap<String, T>,
/// The time of the reload
load_time: OffsetDateTime,
}
impl<T> Deref for CacheEntity<T> {
type Target = HashMap<String, T>;
fn deref(&self) -> &Self::Target {
&self.map
}
}
impl<T> DerefMut for CacheEntity<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.map
}
}
impl<T> CacheEntity<T> {
pub fn new(map: HashMap<String, T>) -> Self {
Self {
map,
load_time: OffsetDateTime::UNIX_EPOCH,
}
}
}
impl<T> Default for CacheEntity<T> {
fn default() -> Self {
Self {
map: HashMap::new(),
load_time: OffsetDateTime::UNIX_EPOCH,
}
}
}
impl<T> CacheEntity<T> {
pub fn update_load_time(mut self) -> Self {
self.load_time = OffsetDateTime::now_utc();
self
}
}
pub type G<T> = Guard<Arc<CacheEntity<T>>>;
pub struct CacheInner {
pub policy_docs: G<PolicyDoc>,
pub users: G<UserIdentity>,
pub user_policies: G<MappedPolicy>,
pub sts_accounts: G<UserIdentity>,
pub sts_policies: G<MappedPolicy>,
pub groups: G<GroupInfo>,
pub user_group_memberships: G<HashSet<String>>,
pub group_policies: G<MappedPolicy>,
}
impl From<&Cache> for CacheInner {
fn from(value: &Cache) -> Self {
Self {
policy_docs: value.policy_docs.load(),
users: value.users.load(),
user_policies: value.user_policies.load(),
sts_accounts: value.sts_accounts.load(),
sts_policies: value.sts_policies.load(),
groups: value.groups.load(),
user_group_memberships: value.user_group_memberships.load(),
group_policies: value.group_policies.load(),
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use arc_swap::ArcSwap;
use futures::future::join_all;
use time::OffsetDateTime;
use super::CacheEntity;
use crate::cache::Cache;
#[tokio::test]
async fn test_cache_entity_add() {
let cache = ArcSwap::new(Arc::new(CacheEntity::<usize>::default()));
let mut f = vec![];
for (index, key) in (0..100).map(|x| x.to_string()).enumerate() {
let c = &cache;
f.push(async move {
Cache::add_or_update(c, &key, &index, OffsetDateTime::now_utc());
});
}
join_all(f).await;
let cache = cache.load();
for (index, key) in (0..100).map(|x| x.to_string()).enumerate() {
assert_eq!(cache.get(&key), Some(&index));
}
}
#[tokio::test]
async fn test_cache_entity_update() {
let cache = ArcSwap::new(Arc::new(CacheEntity::<usize>::default()));
let mut f = vec![];
for (index, key) in (0..100).map(|x| x.to_string()).enumerate() {
let c = &cache;
f.push(async move {
Cache::add_or_update(c, &key, &index, OffsetDateTime::now_utc());
});
}
join_all(f).await;
let cache_load = cache.load();
for (index, key) in (0..100).map(|x| x.to_string()).enumerate() {
assert_eq!(cache_load.get(&key), Some(&index));
}
let mut f = vec![];
for (index, key) in (0..100).map(|x| x.to_string()).enumerate() {
let c = &cache;
f.push(async move {
Cache::add_or_update(c, &key, &(index * 1000), OffsetDateTime::now_utc());
});
}
join_all(f).await;
let cache_load = cache.load();
for (index, key) in (0..100).map(|x| x.to_string()).enumerate() {
assert_eq!(cache_load.get(&key), Some(&(index * 1000)));
}
}
#[tokio::test]
async fn test_cache_entity_delete() {
let cache = ArcSwap::new(Arc::new(CacheEntity::<usize>::default()));
let mut f = vec![];
for (index, key) in (0..100).map(|x| x.to_string()).enumerate() {
let c = &cache;
f.push(async move {
Cache::add_or_update(c, &key, &index, OffsetDateTime::now_utc());
});
}
join_all(f).await;
let cache_load = cache.load();
for (index, key) in (0..100).map(|x| x.to_string()).enumerate() {
assert_eq!(cache_load.get(&key), Some(&index));
}
let mut f = vec![];
for key in (0..100).map(|x| x.to_string()) {
let c = &cache;
f.push(async move {
Cache::delete(c, &key, OffsetDateTime::now_utc());
});
}
join_all(f).await;
let cache_load = cache.load();
assert!(cache_load.is_empty());
}
}
+435
View File
@@ -0,0 +1,435 @@
// 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 rustfs_policy::policy::Error as PolicyError;
pub type Result<T> = core::result::Result<T, Error>;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
PolicyError(#[from] PolicyError),
#[error("{0}")]
StringError(String),
#[error("crypto: {0}")]
CryptoError(#[from] rustfs_crypto::Error),
#[error("user '{0}' does not exist")]
NoSuchUser(String),
#[error("account '{0}' does not exist")]
NoSuchAccount(String),
#[error("service account '{0}' does not exist")]
NoSuchServiceAccount(String),
#[error("temp account '{0}' does not exist")]
NoSuchTempAccount(String),
#[error("group '{0}' does not exist")]
NoSuchGroup(String),
#[error("policy does not exist")]
NoSuchPolicy,
#[error("policy in use")]
PolicyInUse,
#[error("group not empty")]
GroupNotEmpty,
#[error("invalid arguments specified")]
InvalidArgument,
#[error("not initialized")]
IamSysNotInitialized,
#[error("invalid service type: {0}")]
InvalidServiceType(String),
#[error("malformed credential")]
ErrCredMalformed,
#[error("CredNotInitialized")]
CredNotInitialized,
#[error("invalid access key length")]
InvalidAccessKeyLength,
#[error("invalid secret key length")]
InvalidSecretKeyLength,
#[error("access key contains reserved characters =,")]
ContainsReservedChars,
#[error("group name contains reserved characters =,")]
GroupNameContainsReservedChars,
#[error("jwt err {0}")]
JWTError(jsonwebtoken::errors::Error),
#[error("no access key")]
NoAccessKey,
#[error("invalid token")]
InvalidToken,
#[error("invalid access_key")]
InvalidAccessKey,
#[error("action not allowed")]
IAMActionNotAllowed,
#[error("invalid expiration")]
InvalidExpiration,
#[error("no secret key with access key")]
NoSecretKeyWithAccessKey,
#[error("no access key with secret key")]
NoAccessKeyWithSecretKey,
#[error("policy too large")]
PolicyTooLarge,
#[error("config not found")]
ConfigNotFound,
#[error("io error: {0}")]
Io(std::io::Error),
}
impl PartialEq for Error {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Error::StringError(a), Error::StringError(b)) => a == b,
(Error::NoSuchUser(a), Error::NoSuchUser(b)) => a == b,
(Error::NoSuchAccount(a), Error::NoSuchAccount(b)) => a == b,
(Error::NoSuchServiceAccount(a), Error::NoSuchServiceAccount(b)) => a == b,
(Error::NoSuchTempAccount(a), Error::NoSuchTempAccount(b)) => a == b,
(Error::NoSuchGroup(a), Error::NoSuchGroup(b)) => a == b,
(Error::InvalidServiceType(a), Error::InvalidServiceType(b)) => a == b,
(Error::Io(a), Error::Io(b)) => a.kind() == b.kind() && a.to_string() == b.to_string(),
// For complex types like PolicyError, CryptoError, JWTError, compare string representations
(a, b) => std::mem::discriminant(a) == std::mem::discriminant(b) && a.to_string() == b.to_string(),
}
}
}
impl Clone for Error {
fn clone(&self) -> Self {
match self {
Error::PolicyError(e) => Error::StringError(e.to_string()), // Convert to string since PolicyError may not be cloneable
Error::StringError(s) => Error::StringError(s.clone()),
Error::CryptoError(e) => Error::StringError(format!("crypto: {e}")), // Convert to string
Error::NoSuchUser(s) => Error::NoSuchUser(s.clone()),
Error::NoSuchAccount(s) => Error::NoSuchAccount(s.clone()),
Error::NoSuchServiceAccount(s) => Error::NoSuchServiceAccount(s.clone()),
Error::NoSuchTempAccount(s) => Error::NoSuchTempAccount(s.clone()),
Error::NoSuchGroup(s) => Error::NoSuchGroup(s.clone()),
Error::NoSuchPolicy => Error::NoSuchPolicy,
Error::PolicyInUse => Error::PolicyInUse,
Error::GroupNotEmpty => Error::GroupNotEmpty,
Error::InvalidArgument => Error::InvalidArgument,
Error::IamSysNotInitialized => Error::IamSysNotInitialized,
Error::InvalidServiceType(s) => Error::InvalidServiceType(s.clone()),
Error::ErrCredMalformed => Error::ErrCredMalformed,
Error::CredNotInitialized => Error::CredNotInitialized,
Error::InvalidAccessKeyLength => Error::InvalidAccessKeyLength,
Error::InvalidSecretKeyLength => Error::InvalidSecretKeyLength,
Error::ContainsReservedChars => Error::ContainsReservedChars,
Error::GroupNameContainsReservedChars => Error::GroupNameContainsReservedChars,
Error::JWTError(e) => Error::StringError(format!("jwt err {e}")), // Convert to string
Error::NoAccessKey => Error::NoAccessKey,
Error::InvalidToken => Error::InvalidToken,
Error::InvalidAccessKey => Error::InvalidAccessKey,
Error::IAMActionNotAllowed => Error::IAMActionNotAllowed,
Error::InvalidExpiration => Error::InvalidExpiration,
Error::NoSecretKeyWithAccessKey => Error::NoSecretKeyWithAccessKey,
Error::NoAccessKeyWithSecretKey => Error::NoAccessKeyWithSecretKey,
Error::PolicyTooLarge => Error::PolicyTooLarge,
Error::ConfigNotFound => Error::ConfigNotFound,
Error::Io(e) => Error::Io(std::io::Error::new(e.kind(), e.to_string())),
}
}
}
impl Error {
pub fn other<E>(error: E) -> Self
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
Error::Io(std::io::Error::other(error))
}
}
impl From<rustfs_ecstore::error::StorageError> for Error {
fn from(e: rustfs_ecstore::error::StorageError) -> Self {
match e {
rustfs_ecstore::error::StorageError::ConfigNotFound => Error::ConfigNotFound,
_ => Error::other(e),
}
}
}
impl From<Error> for rustfs_ecstore::error::StorageError {
fn from(e: Error) -> Self {
match e {
Error::ConfigNotFound => rustfs_ecstore::error::StorageError::ConfigNotFound,
_ => rustfs_ecstore::error::StorageError::other(e),
}
}
}
impl From<rustfs_policy::error::Error> for Error {
fn from(e: rustfs_policy::error::Error) -> Self {
match e {
rustfs_policy::error::Error::PolicyTooLarge => Error::PolicyTooLarge,
rustfs_policy::error::Error::InvalidArgument => Error::InvalidArgument,
rustfs_policy::error::Error::InvalidServiceType(s) => Error::InvalidServiceType(s),
rustfs_policy::error::Error::IAMActionNotAllowed => Error::IAMActionNotAllowed,
rustfs_policy::error::Error::InvalidExpiration => Error::InvalidExpiration,
rustfs_policy::error::Error::NoAccessKey => Error::NoAccessKey,
rustfs_policy::error::Error::InvalidToken => Error::InvalidToken,
rustfs_policy::error::Error::InvalidAccessKey => Error::InvalidAccessKey,
rustfs_policy::error::Error::NoSecretKeyWithAccessKey => Error::NoSecretKeyWithAccessKey,
rustfs_policy::error::Error::NoAccessKeyWithSecretKey => Error::NoAccessKeyWithSecretKey,
rustfs_policy::error::Error::Io(e) => Error::Io(e),
rustfs_policy::error::Error::JWTError(e) => Error::JWTError(e),
rustfs_policy::error::Error::NoSuchUser(s) => Error::NoSuchUser(s),
rustfs_policy::error::Error::NoSuchAccount(s) => Error::NoSuchAccount(s),
rustfs_policy::error::Error::NoSuchServiceAccount(s) => Error::NoSuchServiceAccount(s),
rustfs_policy::error::Error::NoSuchTempAccount(s) => Error::NoSuchTempAccount(s),
rustfs_policy::error::Error::NoSuchGroup(s) => Error::NoSuchGroup(s),
rustfs_policy::error::Error::NoSuchPolicy => Error::NoSuchPolicy,
rustfs_policy::error::Error::PolicyInUse => Error::PolicyInUse,
rustfs_policy::error::Error::GroupNotEmpty => Error::GroupNotEmpty,
rustfs_policy::error::Error::InvalidAccessKeyLength => Error::InvalidAccessKeyLength,
rustfs_policy::error::Error::InvalidSecretKeyLength => Error::InvalidSecretKeyLength,
rustfs_policy::error::Error::ContainsReservedChars => Error::ContainsReservedChars,
rustfs_policy::error::Error::GroupNameContainsReservedChars => Error::GroupNameContainsReservedChars,
rustfs_policy::error::Error::CredNotInitialized => Error::CredNotInitialized,
rustfs_policy::error::Error::IamSysNotInitialized => Error::IamSysNotInitialized,
rustfs_policy::error::Error::PolicyError(e) => Error::PolicyError(e),
rustfs_policy::error::Error::StringError(s) => Error::StringError(s),
rustfs_policy::error::Error::CryptoError(e) => Error::CryptoError(e),
rustfs_policy::error::Error::ErrCredMalformed => Error::ErrCredMalformed,
}
}
}
impl From<Error> for std::io::Error {
fn from(e: Error) -> Self {
std::io::Error::other(e)
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Error::other(e)
}
}
impl From<base64_simd::Error> for Error {
fn from(e: base64_simd::Error) -> Self {
Error::other(e)
}
}
pub fn is_err_config_not_found(err: &Error) -> bool {
matches!(err, Error::ConfigNotFound)
}
// pub fn is_err_no_such_user(e: &Error) -> bool {
// matches!(e, Error::NoSuchUser(_))
// }
pub fn is_err_no_such_policy(err: &Error) -> bool {
matches!(err, Error::NoSuchPolicy)
}
pub fn is_err_no_such_user(err: &Error) -> bool {
matches!(err, Error::NoSuchUser(_))
}
pub fn is_err_no_such_account(err: &Error) -> bool {
matches!(err, Error::NoSuchAccount(_))
}
pub fn is_err_no_such_temp_account(err: &Error) -> bool {
matches!(err, Error::NoSuchTempAccount(_))
}
pub fn is_err_no_such_group(err: &Error) -> bool {
matches!(err, Error::NoSuchGroup(_))
}
pub fn is_err_no_such_service_account(err: &Error) -> bool {
matches!(err, Error::NoSuchServiceAccount(_))
}
// pub fn clone_err(e: &Error) -> Error {
// if let Some(e) = e.downcast_ref::<DiskError>() {
// clone_disk_err(e)
// } else if let Some(e) = e.downcast_ref::<std::io::Error>() {
// if let Some(code) = e.raw_os_error() {
// Error::new(std::io::Error::from_raw_os_error(code))
// } else {
// Error::new(std::io::Error::new(e.kind(), e.to_string()))
// }
// } else {
// //TODO: Optimize other types
// Error::msg(e.to_string())
// }
// }
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Error as IoError, ErrorKind};
#[test]
fn test_iam_error_to_io_error_conversion() {
let iam_errors = vec![
Error::NoSuchUser("testuser".to_string()),
Error::NoSuchAccount("testaccount".to_string()),
Error::InvalidArgument,
Error::IAMActionNotAllowed,
Error::PolicyTooLarge,
Error::ConfigNotFound,
];
for iam_error in iam_errors {
let io_error: std::io::Error = iam_error.clone().into();
// Check that conversion creates an io::Error
assert_eq!(io_error.kind(), ErrorKind::Other);
// Check that the error message is preserved
assert!(io_error.to_string().contains(&iam_error.to_string()));
}
}
#[test]
fn test_iam_error_from_storage_error() {
// Test conversion from StorageError
let storage_error = rustfs_ecstore::error::StorageError::ConfigNotFound;
let iam_error: Error = storage_error.into();
assert_eq!(iam_error, Error::ConfigNotFound);
// Test reverse conversion
let back_to_storage: rustfs_ecstore::error::StorageError = iam_error.into();
assert_eq!(back_to_storage, rustfs_ecstore::error::StorageError::ConfigNotFound);
}
#[test]
fn test_iam_error_from_policy_error() {
use rustfs_policy::error::Error as PolicyError;
let policy_errors = vec![
(PolicyError::NoSuchUser("user1".to_string()), Error::NoSuchUser("user1".to_string())),
(PolicyError::NoSuchPolicy, Error::NoSuchPolicy),
(PolicyError::InvalidArgument, Error::InvalidArgument),
(PolicyError::PolicyTooLarge, Error::PolicyTooLarge),
];
for (policy_error, expected_iam_error) in policy_errors {
let converted_iam_error: Error = policy_error.into();
assert_eq!(converted_iam_error, expected_iam_error);
}
}
#[test]
fn test_iam_error_other_function() {
let custom_error = "Custom IAM error";
let iam_error = Error::other(custom_error);
match iam_error {
Error::Io(io_error) => {
assert!(io_error.to_string().contains(custom_error));
assert_eq!(io_error.kind(), ErrorKind::Other);
}
_ => panic!("Expected Io variant"),
}
}
#[test]
fn test_iam_error_from_serde_json() {
// Test conversion from serde_json::Error
let invalid_json = r#"{"invalid": json}"#;
let json_error = serde_json::from_str::<serde_json::Value>(invalid_json).unwrap_err();
let iam_error: Error = json_error.into();
match iam_error {
Error::Io(io_error) => {
assert_eq!(io_error.kind(), ErrorKind::Other);
}
_ => panic!("Expected Io variant"),
}
}
#[test]
fn test_helper_functions() {
// Test helper functions for error type checking
assert!(is_err_config_not_found(&Error::ConfigNotFound));
assert!(!is_err_config_not_found(&Error::NoSuchPolicy));
assert!(is_err_no_such_policy(&Error::NoSuchPolicy));
assert!(!is_err_no_such_policy(&Error::ConfigNotFound));
assert!(is_err_no_such_user(&Error::NoSuchUser("test".to_string())));
assert!(!is_err_no_such_user(&Error::NoSuchAccount("test".to_string())));
assert!(is_err_no_such_account(&Error::NoSuchAccount("test".to_string())));
assert!(!is_err_no_such_account(&Error::NoSuchUser("test".to_string())));
assert!(is_err_no_such_temp_account(&Error::NoSuchTempAccount("test".to_string())));
assert!(!is_err_no_such_temp_account(&Error::NoSuchAccount("test".to_string())));
assert!(is_err_no_such_group(&Error::NoSuchGroup("test".to_string())));
assert!(!is_err_no_such_group(&Error::NoSuchUser("test".to_string())));
assert!(is_err_no_such_service_account(&Error::NoSuchServiceAccount("test".to_string())));
assert!(!is_err_no_such_service_account(&Error::NoSuchAccount("test".to_string())));
}
#[test]
fn test_iam_error_io_preservation() {
// Test that Io variant preserves original io::Error
let original_io = IoError::new(ErrorKind::PermissionDenied, "access denied");
let iam_error = Error::Io(original_io);
let converted_io: std::io::Error = iam_error.into();
// Note: Our clone implementation creates a new io::Error with the same kind and message
// but it becomes ErrorKind::Other when cloned
assert_eq!(converted_io.kind(), ErrorKind::Other);
assert!(converted_io.to_string().contains("access denied"));
}
#[test]
fn test_error_display_format() {
let test_cases = vec![
(Error::NoSuchUser("testuser".to_string()), "user 'testuser' does not exist"),
(Error::NoSuchAccount("testaccount".to_string()), "account 'testaccount' does not exist"),
(Error::InvalidArgument, "invalid arguments specified"),
(Error::IAMActionNotAllowed, "action not allowed"),
(Error::ConfigNotFound, "config not found"),
];
for (error, expected_message) in test_cases {
assert_eq!(error.to_string(), expected_message);
}
}
}
+45
View File
@@ -0,0 +1,45 @@
// 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};
use manager::IamCache;
use rustfs_ecstore::store::ECStore;
use std::sync::{Arc, OnceLock};
use store::object::ObjectStore;
use sys::IamSys;
use tracing::{debug, instrument};
pub mod cache;
pub mod error;
pub mod manager;
pub mod store;
pub mod utils;
pub mod sys;
static IAM_SYS: OnceLock<Arc<IamSys<ObjectStore>>> = OnceLock::new();
#[instrument(skip(ecstore))]
pub async fn init_iam_sys(ecstore: Arc<ECStore>) -> Result<()> {
debug!("init iam system");
let s = IamCache::new(ObjectStore::new(ecstore)).await;
IAM_SYS.get_or_init(move || IamSys::new(s).into());
Ok(())
}
#[inline]
pub fn get() -> Result<Arc<IamSys<ObjectStore>>> {
IAM_SYS.get().map(Arc::clone).ok_or(Error::IamSysNotInitialized)
}
File diff suppressed because it is too large Load Diff
+150
View File
@@ -0,0 +1,150 @@
// 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.
pub mod object;
use crate::cache::Cache;
use crate::error::Result;
use rustfs_policy::{auth::UserIdentity, policy::PolicyDoc};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::collections::{HashMap, HashSet};
use time::OffsetDateTime;
#[async_trait::async_trait]
pub trait Store: Clone + Send + Sync + 'static {
async fn save_iam_config<Item: Serialize + Send>(&self, item: Item, path: impl AsRef<str> + Send) -> Result<()>;
async fn load_iam_config<Item: DeserializeOwned>(&self, path: impl AsRef<str> + Send) -> Result<Item>;
async fn delete_iam_config(&self, path: impl AsRef<str> + Send) -> Result<()>;
async fn save_user_identity(&self, name: &str, user_type: UserType, item: UserIdentity, ttl: Option<usize>) -> Result<()>;
async fn delete_user_identity(&self, name: &str, user_type: UserType) -> Result<()>;
async fn load_user_identity(&self, name: &str, user_type: UserType) -> Result<UserIdentity>;
async fn load_user(&self, name: &str, user_type: UserType, m: &mut HashMap<String, UserIdentity>) -> Result<()>;
async fn load_users(&self, user_type: UserType, m: &mut HashMap<String, UserIdentity>) -> Result<()>;
async fn load_secret_key(&self, name: &str, user_type: UserType) -> Result<String>;
async fn save_group_info(&self, name: &str, item: GroupInfo) -> Result<()>;
async fn delete_group_info(&self, name: &str) -> Result<()>;
async fn load_group(&self, name: &str, m: &mut HashMap<String, GroupInfo>) -> Result<()>;
async fn load_groups(&self, m: &mut HashMap<String, GroupInfo>) -> Result<()>;
async fn save_policy_doc(&self, name: &str, item: PolicyDoc) -> Result<()>;
async fn delete_policy_doc(&self, name: &str) -> Result<()>;
async fn load_policy(&self, name: &str) -> Result<PolicyDoc>;
async fn load_policy_doc(&self, name: &str, m: &mut HashMap<String, PolicyDoc>) -> Result<()>;
async fn load_policy_docs(&self, m: &mut HashMap<String, PolicyDoc>) -> Result<()>;
async fn save_mapped_policy(
&self,
name: &str,
user_type: UserType,
is_group: bool,
item: MappedPolicy,
ttl: Option<usize>,
) -> Result<()>;
async fn delete_mapped_policy(&self, name: &str, user_type: UserType, is_group: bool) -> Result<()>;
async fn load_mapped_policy(
&self,
name: &str,
user_type: UserType,
is_group: bool,
m: &mut HashMap<String, MappedPolicy>,
) -> Result<()>;
async fn load_mapped_policys(&self, user_type: UserType, is_group: bool, m: &mut HashMap<String, MappedPolicy>)
-> Result<()>;
async fn load_all(&self, cache: &Cache) -> Result<()>;
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum UserType {
Svc,
Sts,
Reg,
None,
}
impl UserType {
pub fn prefix(&self) -> &'static str {
match self {
UserType::Svc => "service-accounts/",
UserType::Sts => "sts/",
UserType::Reg => "users/",
UserType::None => "",
}
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct MappedPolicy {
pub version: i64,
pub policies: String,
pub update_at: OffsetDateTime,
}
impl Default for MappedPolicy {
fn default() -> Self {
Self {
version: 0,
policies: "".to_owned(),
update_at: OffsetDateTime::now_utc(),
}
}
}
impl MappedPolicy {
pub fn new(policy: &str) -> Self {
Self {
version: 1,
policies: policy.to_owned(),
update_at: OffsetDateTime::now_utc(),
}
}
pub fn to_slice(&self) -> Vec<String> {
self.policies
.split(",")
.filter(|v| !v.trim().is_empty())
.map(|v| v.to_string())
.collect()
}
pub fn policy_set(&self) -> HashSet<String> {
self.policies
.split(",")
.filter(|v| !v.trim().is_empty())
.map(|v| v.to_string())
.collect()
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct GroupInfo {
pub version: i64,
pub status: String,
pub members: Vec<String>,
pub update_at: Option<OffsetDateTime>,
}
impl GroupInfo {
pub fn new(members: Vec<String>) -> Self {
Self {
version: 1,
status: "enabled".to_owned(),
members,
update_at: Some(OffsetDateTime::now_utc()),
}
}
}
File diff suppressed because it is too large Load Diff
+734
View File
@@ -0,0 +1,734 @@
// 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 as IamError;
use crate::error::is_err_no_such_account;
use crate::error::is_err_no_such_temp_account;
use crate::error::{Error, Result};
use crate::manager::IamCache;
use crate::manager::extract_jwt_claims;
use crate::manager::get_default_policyes;
use crate::store::GroupInfo;
use crate::store::MappedPolicy;
use crate::store::Store;
use crate::store::UserType;
use rustfs_ecstore::global::get_global_action_cred;
use rustfs_madmin::AddOrUpdateUserReq;
use rustfs_madmin::GroupDesc;
use rustfs_policy::arn::ARN;
use rustfs_policy::auth::Credentials;
use rustfs_policy::auth::{
ACCOUNT_ON, UserIdentity, contains_reserved_chars, create_new_credentials_with_metadata, generate_credentials,
is_access_key_valid, is_secret_key_valid,
};
use rustfs_policy::policy::Args;
use rustfs_policy::policy::{EMBEDDED_POLICY_TYPE, INHERITED_POLICY_TYPE, Policy, PolicyDoc, iam_policy_claim_name_sa};
use rustfs_utils::crypto::{base64_decode, base64_encode};
use serde_json::Value;
use serde_json::json;
use std::collections::HashMap;
use std::sync::Arc;
use time::OffsetDateTime;
pub const MAX_SVCSESSION_POLICY_SIZE: usize = 4096;
pub const STATUS_ENABLED: &str = "enabled";
pub const STATUS_DISABLED: &str = "disabled";
pub const POLICYNAME: &str = "policy";
pub const SESSION_POLICY_NAME: &str = "sessionPolicy";
pub const SESSION_POLICY_NAME_EXTRACTED: &str = "sessionPolicy-extracted";
pub struct IamSys<T> {
store: Arc<IamCache<T>>,
roles_map: HashMap<ARN, String>,
}
impl<T: Store> IamSys<T> {
pub fn new(store: Arc<IamCache<T>>) -> Self {
Self {
store,
roles_map: HashMap::new(),
}
}
pub async fn load_group(&self, name: &str) -> Result<()> {
self.store.group_notification_handler(name).await
}
pub async fn load_groups(&self, m: &mut HashMap<String, GroupInfo>) -> Result<()> {
self.store.api.load_groups(m).await
}
pub async fn load_policy(&self, name: &str) -> Result<()> {
self.store.policy_notification_handler(name).await
}
pub async fn load_policy_mapping(&self, name: &str, user_type: UserType, is_group: bool) -> Result<()> {
self.store
.policy_mapping_notification_handler(name, user_type, is_group)
.await
}
pub async fn load_user(&self, name: &str, user_type: UserType) -> Result<()> {
self.store.user_notification_handler(name, user_type).await
}
pub async fn load_users(&self, user_type: UserType, m: &mut HashMap<String, UserIdentity>) -> Result<()> {
self.store.api.load_users(user_type, m).await?;
Ok(())
}
pub async fn load_service_account(&self, name: &str) -> Result<()> {
self.store.user_notification_handler(name, UserType::Svc).await
}
pub async fn delete_policy(&self, name: &str, notify: bool) -> Result<()> {
for k in get_default_policyes().keys() {
if k == name {
return Err(Error::other("system policy can not be deleted"));
}
}
self.store.delete_policy(name, notify).await?;
if notify {
// TODO: implement notification
}
Ok(())
}
pub async fn info_policy(&self, name: &str) -> Result<rustfs_madmin::PolicyInfo> {
let d = self.store.get_policy_doc(name).await?;
let pdata = serde_json::to_string(&d.policy)?;
Ok(rustfs_madmin::PolicyInfo {
policy_name: name.to_string(),
policy: json!(pdata),
create_date: d.create_date,
update_date: d.update_date,
})
}
pub async fn load_mapped_policys(
&self,
user_type: UserType,
is_group: bool,
m: &mut HashMap<String, MappedPolicy>,
) -> Result<()> {
self.store.api.load_mapped_policys(user_type, is_group, m).await
}
pub async fn list_polices(&self, bucket_name: &str) -> Result<HashMap<String, Policy>> {
self.store.list_polices(bucket_name).await
}
pub async fn list_policy_docs(&self, bucket_name: &str) -> Result<HashMap<String, PolicyDoc>> {
self.store.list_policy_docs(bucket_name).await
}
pub async fn set_policy(&self, name: &str, policy: Policy) -> Result<OffsetDateTime> {
self.store.set_policy(name, policy).await
// TODO: notification
}
pub async fn get_role_policy(&self, arn_str: &str) -> Result<(ARN, String)> {
let Some(arn) = ARN::parse(arn_str).ok() else {
return Err(Error::other("Invalid ARN"));
};
let Some(policy) = self.roles_map.get(&arn) else {
return Err(Error::other("No such role"));
};
Ok((arn, policy.clone()))
}
pub async fn delete_user(&self, name: &str, _notify: bool) -> Result<()> {
self.store.delete_user(name, UserType::Reg).await
// TODO: notification
}
pub async fn current_policies(&self, name: &str) -> String {
self.store.merge_policies(name).await.0
}
pub async fn list_bucket_users(&self, bucket_name: &str) -> Result<HashMap<String, rustfs_madmin::UserInfo>> {
self.store.get_bucket_users(bucket_name).await
}
pub async fn list_users(&self) -> Result<HashMap<String, rustfs_madmin::UserInfo>> {
self.store.get_users().await
}
pub async fn set_temp_user(&self, name: &str, cred: &Credentials, policy_name: Option<&str>) -> Result<OffsetDateTime> {
self.store.set_temp_user(name, cred, policy_name).await
// TODO: notification
}
pub async fn is_temp_user(&self, name: &str) -> Result<(bool, String)> {
let Some(u) = self.store.get_user(name).await else {
return Err(IamError::NoSuchUser(name.to_string()));
};
if u.credentials.is_temp() {
Ok((true, u.credentials.parent_user))
} else {
Ok((false, "".to_string()))
}
}
pub async fn is_service_account(&self, name: &str) -> Result<(bool, String)> {
let Some(u) = self.store.get_user(name).await else {
return Err(IamError::NoSuchUser(name.to_string()));
};
if u.credentials.is_service_account() {
Ok((true, u.credentials.parent_user))
} else {
Ok((false, "".to_string()))
}
}
pub async fn get_user_info(&self, name: &str) -> Result<rustfs_madmin::UserInfo> {
self.store.get_user_info(name).await
}
pub async fn set_user_status(&self, name: &str, status: rustfs_madmin::AccountStatus) -> Result<OffsetDateTime> {
self.store.set_user_status(name, status).await
// TODO: notification
}
pub async fn new_service_account(
&self,
parent_user: &str,
groups: Option<Vec<String>>,
opts: NewServiceAccountOpts,
) -> Result<(Credentials, OffsetDateTime)> {
if parent_user.is_empty() {
return Err(IamError::InvalidArgument);
}
if !opts.access_key.is_empty() && opts.secret_key.is_empty() {
return Err(IamError::NoSecretKeyWithAccessKey);
}
if !opts.secret_key.is_empty() && opts.access_key.is_empty() {
return Err(IamError::NoAccessKeyWithSecretKey);
}
if parent_user == opts.access_key {
return Err(IamError::IAMActionNotAllowed);
}
if opts.expiration.is_none() {
return Err(IamError::InvalidExpiration);
}
// TODO: check allow_site_replicator_account
let policy_buf = if let Some(policy) = opts.session_policy {
policy.validate()?;
let buf = serde_json::to_vec(&policy)?;
if buf.len() > MAX_SVCSESSION_POLICY_SIZE {
return Err(IamError::PolicyTooLarge);
}
buf
} else {
Vec::new()
};
let mut m: HashMap<String, Value> = HashMap::new();
m.insert("parent".to_owned(), Value::String(parent_user.to_owned()));
if !policy_buf.is_empty() {
m.insert(SESSION_POLICY_NAME.to_owned(), Value::String(base64_encode(&policy_buf)));
m.insert(iam_policy_claim_name_sa(), Value::String(EMBEDDED_POLICY_TYPE.to_owned()));
} else {
m.insert(iam_policy_claim_name_sa(), Value::String(INHERITED_POLICY_TYPE.to_owned()));
}
if let Some(claims) = opts.claims {
for (k, v) in claims.iter() {
if !m.contains_key(k) {
m.insert(k.to_owned(), v.to_owned());
}
}
}
// set expiration time default to 1 hour
m.insert(
"exp".to_string(),
Value::Number(serde_json::Number::from(
opts.expiration
.map_or(OffsetDateTime::now_utc().unix_timestamp() + 3600, |t| t.unix_timestamp()),
)),
);
let (access_key, secret_key) = if !opts.access_key.is_empty() || !opts.secret_key.is_empty() {
(opts.access_key, opts.secret_key)
} else {
generate_credentials()?
};
let mut cred = create_new_credentials_with_metadata(&access_key, &secret_key, &m, &secret_key)?;
cred.parent_user = parent_user.to_owned();
cred.groups = groups;
cred.status = ACCOUNT_ON.to_owned();
cred.name = opts.name;
cred.description = opts.description;
cred.expiration = opts.expiration;
let create_at = self.store.add_service_account(cred.clone()).await?;
Ok((cred, create_at))
// TODO: notification
}
pub async fn update_service_account(&self, name: &str, opts: UpdateServiceAccountOpts) -> Result<OffsetDateTime> {
self.store.update_service_account(name, opts).await
// TODO: notification
}
pub async fn list_service_accounts(&self, access_key: &str) -> Result<Vec<Credentials>> {
self.store.list_service_accounts(access_key).await
}
pub async fn list_temp_accounts(&self, access_key: &str) -> Result<Vec<UserIdentity>> {
self.store.list_temp_accounts(access_key).await
}
pub async fn list_sts_accounts(&self, access_key: &str) -> Result<Vec<Credentials>> {
self.store.list_sts_accounts(access_key).await
}
pub async fn get_service_account(&self, access_key: &str) -> Result<(Credentials, Option<Policy>)> {
let (mut da, policy) = self.get_service_account_internal(access_key).await?;
da.credentials.secret_key.clear();
da.credentials.session_token.clear();
Ok((da.credentials, policy))
}
async fn get_service_account_internal(&self, access_key: &str) -> Result<(UserIdentity, Option<Policy>)> {
let (sa, claims) = match self.get_account_with_claims(access_key).await {
Ok(res) => res,
Err(err) => {
if is_err_no_such_account(&err) {
return Err(IamError::NoSuchServiceAccount(access_key.to_string()));
}
return Err(err);
}
};
if !sa.credentials.is_service_account() {
return Err(IamError::NoSuchServiceAccount(access_key.to_string()));
}
let op_pt = claims.get(&iam_policy_claim_name_sa());
let op_sp = claims.get(SESSION_POLICY_NAME);
if let (Some(pt), Some(sp)) = (op_pt, op_sp) {
if pt == EMBEDDED_POLICY_TYPE {
let policy = serde_json::from_slice(&base64_decode(sp.as_str().unwrap_or_default().as_bytes())?)?;
return Ok((sa, Some(policy)));
}
}
Ok((sa, None))
}
async fn get_account_with_claims(&self, access_key: &str) -> Result<(UserIdentity, HashMap<String, Value>)> {
let Some(acc) = self.store.get_user(access_key).await else {
return Err(IamError::NoSuchAccount(access_key.to_string()));
};
let m = extract_jwt_claims(&acc)?;
Ok((acc, m))
}
pub async fn get_temporary_account(&self, access_key: &str) -> Result<(Credentials, Option<Policy>)> {
let (mut sa, policy) = match self.get_temp_account(access_key).await {
Ok(res) => res,
Err(err) => {
if is_err_no_such_temp_account(&err) {
// TODO: load_user
match self.get_temp_account(access_key).await {
Ok(res) => res,
Err(err) => return Err(err),
};
}
return Err(err);
}
};
sa.credentials.secret_key.clear();
sa.credentials.session_token.clear();
Ok((sa.credentials, policy))
}
async fn get_temp_account(&self, access_key: &str) -> Result<(UserIdentity, Option<Policy>)> {
let (sa, claims) = match self.get_account_with_claims(access_key).await {
Ok(res) => res,
Err(err) => {
if is_err_no_such_account(&err) {
return Err(IamError::NoSuchTempAccount(access_key.to_string()));
}
return Err(err);
}
};
if !sa.credentials.is_temp() {
return Err(IamError::NoSuchTempAccount(access_key.to_string()));
}
let op_pt = claims.get(&iam_policy_claim_name_sa());
let op_sp = claims.get(SESSION_POLICY_NAME);
if let (Some(pt), Some(sp)) = (op_pt, op_sp) {
if pt == EMBEDDED_POLICY_TYPE {
let policy = serde_json::from_slice(&base64_decode(sp.as_str().unwrap_or_default().as_bytes())?)?;
return Ok((sa, Some(policy)));
}
}
Ok((sa, None))
}
pub async fn get_claims_for_svc_acc(&self, access_key: &str) -> Result<HashMap<String, Value>> {
let Some(u) = self.store.get_user(access_key).await else {
return Err(IamError::NoSuchServiceAccount(access_key.to_string()));
};
if u.credentials.is_service_account() {
return Err(IamError::NoSuchServiceAccount(access_key.to_string()));
}
extract_jwt_claims(&u)
}
pub async fn delete_service_account(&self, access_key: &str, _notify: bool) -> Result<()> {
let Some(u) = self.store.get_user(access_key).await else {
return Ok(());
};
if !u.credentials.is_service_account() {
return Ok(());
}
self.store.delete_user(access_key, UserType::Svc).await
// TODO: notification
}
pub async fn create_user(&self, access_key: &str, args: &AddOrUpdateUserReq) -> Result<OffsetDateTime> {
if !is_access_key_valid(access_key) {
return Err(IamError::InvalidAccessKeyLength);
}
if contains_reserved_chars(access_key) {
return Err(IamError::ContainsReservedChars);
}
if !is_secret_key_valid(&args.secret_key) {
return Err(IamError::InvalidSecretKeyLength);
}
self.store.add_user(access_key, args).await
// TODO: notification
}
pub async fn set_user_secret_key(&self, access_key: &str, secret_key: &str) -> Result<()> {
if !is_access_key_valid(access_key) {
return Err(IamError::InvalidAccessKeyLength);
}
if !is_secret_key_valid(secret_key) {
return Err(IamError::InvalidSecretKeyLength);
}
self.store.update_user_secret_key(access_key, secret_key).await
}
pub async fn check_key(&self, access_key: &str) -> Result<(Option<UserIdentity>, bool)> {
if let Some(sys_cred) = get_global_action_cred() {
if sys_cred.access_key == access_key {
return Ok((Some(UserIdentity::new(sys_cred)), true));
}
}
match self.store.get_user(access_key).await {
Some(res) => {
let ok = res.credentials.is_valid();
Ok((Some(res), ok))
}
None => Ok((None, false)),
}
}
pub async fn get_user(&self, access_key: &str) -> Option<UserIdentity> {
match self.check_key(access_key).await {
Ok((u, _)) => u,
_ => None,
}
}
pub async fn add_users_to_group(&self, group: &str, users: Vec<String>) -> Result<OffsetDateTime> {
if contains_reserved_chars(group) {
return Err(IamError::GroupNameContainsReservedChars);
}
self.store.add_users_to_group(group, users).await
// TODO: notification
}
pub async fn remove_users_from_group(&self, group: &str, users: Vec<String>) -> Result<OffsetDateTime> {
self.store.remove_users_from_group(group, users).await
// TODO: notification
}
pub async fn set_group_status(&self, group: &str, enable: bool) -> Result<OffsetDateTime> {
self.store.set_group_status(group, enable).await
// TODO: notification
}
pub async fn get_group_description(&self, group: &str) -> Result<GroupDesc> {
self.store.get_group_description(group).await
}
pub async fn list_groups(&self) -> Result<Vec<String>> {
self.store.list_groups().await
}
pub async fn policy_db_set(&self, name: &str, user_type: UserType, is_group: bool, policy: &str) -> Result<OffsetDateTime> {
self.store.policy_db_set(name, user_type, is_group, policy).await
// TODO: notification
}
pub async fn policy_db_get(&self, name: &str, groups: &Option<Vec<String>>) -> Result<Vec<String>> {
self.store.policy_db_get(name, groups).await
}
pub async fn is_allowed_sts(&self, args: &Args<'_>, parent_user: &str) -> bool {
let is_owner = parent_user == get_global_action_cred().unwrap().access_key;
let role_arn = args.get_role_arn();
let policies = {
if is_owner {
Vec::new()
} else if role_arn.is_some() {
let Ok(arn) = ARN::parse(role_arn.unwrap_or_default()) else { return false };
MappedPolicy::new(self.roles_map.get(&arn).map_or_else(String::default, |v| v.clone()).as_str()).to_slice()
} else {
let Ok(p) = self.policy_db_get(parent_user, args.groups).await else { return false };
p
//TODO: FROM JWT
}
};
if policies.is_empty() {
return false;
}
let combined_policy = {
if is_owner {
Policy::default()
} else {
let (a, c) = self.store.merge_policies(&policies.join(",")).await;
if a.is_empty() {
return false;
}
c
}
};
let (has_session_policy, is_allowed_sp) = is_allowed_by_session_policy(args);
if has_session_policy {
return is_allowed_sp && (is_owner || combined_policy.is_allowed(args));
}
is_owner || combined_policy.is_allowed(args)
}
pub async fn is_allowed_service_account(&self, args: &Args<'_>, parent_user: &str) -> bool {
let Some(p) = args.claims.get("parent") else {
return false;
};
if p.as_str() != Some(parent_user) {
return false;
}
let is_owner = parent_user == get_global_action_cred().unwrap().access_key;
let role_arn = args.get_role_arn();
let svc_policies = {
if is_owner {
Vec::new()
} else if role_arn.is_some() {
let Ok(arn) = ARN::parse(role_arn.unwrap_or_default()) else { return false };
MappedPolicy::new(self.roles_map.get(&arn).map_or_else(String::default, |v| v.clone()).as_str()).to_slice()
} else {
let Ok(p) = self.policy_db_get(parent_user, args.groups).await else { return false };
p
}
};
if !is_owner && svc_policies.is_empty() {
return false;
}
let combined_policy = {
if is_owner {
Policy::default()
} else {
let (a, c) = self.store.merge_policies(&svc_policies.join(",")).await;
if a.is_empty() {
return false;
}
c
}
};
let mut parent_args = args.clone();
parent_args.account = parent_user;
let Some(sa) = args.claims.get(&iam_policy_claim_name_sa()) else {
return false;
};
let Some(sa_str) = sa.as_str() else {
return false;
};
if sa_str == INHERITED_POLICY_TYPE {
return is_owner || combined_policy.is_allowed(&parent_args);
}
let (has_session_policy, is_allowed_sp) = is_allowed_by_session_policy_for_service_account(args);
if has_session_policy {
return is_allowed_sp && (is_owner || combined_policy.is_allowed(&parent_args));
}
is_owner || combined_policy.is_allowed(&parent_args)
}
pub async fn get_combined_policy(&self, policies: &[String]) -> Policy {
self.store.merge_policies(&policies.join(",")).await.1
}
pub async fn is_allowed(&self, args: &Args<'_>) -> bool {
if args.is_owner {
return true;
}
let Ok((is_temp, parent_user)) = self.is_temp_user(args.account).await else { return false };
if is_temp {
return self.is_allowed_sts(args, &parent_user).await;
}
let Ok((is_svc, parent_user)) = self.is_service_account(args.account).await else { return false };
if is_svc {
return self.is_allowed_service_account(args, &parent_user).await;
}
let Ok(policies) = self.policy_db_get(args.account, args.groups).await else { return false };
if policies.is_empty() {
return false;
}
self.get_combined_policy(&policies).await.is_allowed(args)
}
}
fn is_allowed_by_session_policy(args: &Args<'_>) -> (bool, bool) {
let Some(policy) = args.claims.get(SESSION_POLICY_NAME_EXTRACTED) else {
return (false, false);
};
let has_session_policy = true;
let Some(policy_str) = policy.as_str() else {
return (has_session_policy, false);
};
let Ok(sub_policy) = Policy::parse_config(policy_str.as_bytes()) else {
return (has_session_policy, false);
};
if sub_policy.version.is_empty() {
return (has_session_policy, false);
}
let mut session_policy_args = args.clone();
session_policy_args.is_owner = false;
(has_session_policy, sub_policy.is_allowed(&session_policy_args))
}
fn is_allowed_by_session_policy_for_service_account(args: &Args<'_>) -> (bool, bool) {
let Some(policy) = args.claims.get(SESSION_POLICY_NAME_EXTRACTED) else {
return (false, false);
};
let mut has_session_policy = true;
let Some(policy_str) = policy.as_str() else {
return (has_session_policy, false);
};
let Ok(sub_policy) = Policy::parse_config(policy_str.as_bytes()) else {
return (has_session_policy, false);
};
if sub_policy.version.is_empty() && sub_policy.statements.is_empty() && sub_policy.id.is_empty() {
has_session_policy = false;
return (has_session_policy, false);
}
let mut session_policy_args = args.clone();
session_policy_args.is_owner = false;
(has_session_policy, sub_policy.is_allowed(&session_policy_args))
}
#[derive(Debug, Clone, Default)]
pub struct NewServiceAccountOpts {
pub session_policy: Option<Policy>,
pub access_key: String,
pub secret_key: String,
pub name: Option<String>,
pub description: Option<String>,
pub expiration: Option<OffsetDateTime>,
pub allow_site_replicator_account: bool,
pub claims: Option<HashMap<String, Value>>,
}
pub struct UpdateServiceAccountOpts {
pub session_policy: Option<Policy>,
pub secret_key: Option<String>,
pub name: Option<String>,
pub description: Option<String>,
pub expiration: Option<OffsetDateTime>,
pub status: Option<String>,
}
+384
View File
@@ -0,0 +1,384 @@
// 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 jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header};
use rand::{Rng, RngCore};
use serde::{Serialize, de::DeserializeOwned};
use std::io::{Error, Result};
/// Generates a random access key of the specified length.
///
/// # Arguments
///
/// * `length` - The length of the access key to be generated.
///
/// # Returns
///
/// * `Result<String>` - A result containing the generated access key or an error if the length is invalid.
///
/// # Errors
///
/// * Returns an error if the length is less than 3.
///
pub fn gen_access_key(length: usize) -> Result<String> {
const ALPHA_NUMERIC_TABLE: [char; 36] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
];
if length < 3 {
return Err(Error::other("access key length is too short"));
}
let mut result = String::with_capacity(length);
let mut rng = rand::rng();
for _ in 0..length {
result.push(ALPHA_NUMERIC_TABLE[rng.random_range(0..ALPHA_NUMERIC_TABLE.len())]);
}
Ok(result)
}
/// Generates a random secret key of the specified length.
///
/// # Arguments
///
/// * `length` - The length of the secret key to be generated.
///
/// # Returns
///
/// * `Result<String>` - A result containing the generated secret key or an error if the length is invalid.
///
/// # Errors
///
/// * Returns an error if the length is less than 8.
///
pub fn gen_secret_key(length: usize) -> Result<String> {
use base64_simd::URL_SAFE_NO_PAD;
if length < 8 {
return Err(Error::other("secret key length is too short"));
}
let mut rng = rand::rng();
let mut key = vec![0u8; URL_SAFE_NO_PAD.estimated_decoded_length(length)];
rng.fill_bytes(&mut key);
let encoded = URL_SAFE_NO_PAD.encode_to_string(&key);
let key_str = encoded.replace("/", "+");
Ok(key_str)
}
pub fn generate_jwt<T: Serialize>(claims: &T, secret: &str) -> std::result::Result<String, jsonwebtoken::errors::Error> {
let header = Header::new(Algorithm::HS512);
jsonwebtoken::encode(&header, &claims, &EncodingKey::from_secret(secret.as_bytes()))
}
pub fn extract_claims<T: DeserializeOwned>(
token: &str,
secret: &str,
) -> std::result::Result<jsonwebtoken::TokenData<T>, jsonwebtoken::errors::Error> {
jsonwebtoken::decode::<T>(
token,
&DecodingKey::from_secret(secret.as_bytes()),
&jsonwebtoken::Validation::new(Algorithm::HS512),
)
}
#[cfg(test)]
mod tests {
use super::{extract_claims, gen_access_key, gen_secret_key, generate_jwt};
use serde::{Deserialize, Serialize};
#[test]
fn test_gen_access_key_valid_length() {
// Test valid access key generation
let key = gen_access_key(10).unwrap();
assert_eq!(key.len(), 10);
// Test different lengths
let key_20 = gen_access_key(20).unwrap();
assert_eq!(key_20.len(), 20);
let key_3 = gen_access_key(3).unwrap();
assert_eq!(key_3.len(), 3);
}
#[test]
fn test_gen_access_key_uniqueness() {
// Test that generated keys are unique
let key1 = gen_access_key(16).unwrap();
let key2 = gen_access_key(16).unwrap();
assert_ne!(key1, key2, "Generated access keys should be unique");
}
#[test]
fn test_gen_access_key_character_set() {
// Test that generated keys only contain valid characters
let key = gen_access_key(100).unwrap();
for ch in key.chars() {
assert!(ch.is_ascii_alphanumeric(), "Access key should only contain alphanumeric characters");
assert!(
ch.is_ascii_uppercase() || ch.is_ascii_digit(),
"Access key should only contain uppercase letters and digits"
);
}
}
#[test]
fn test_gen_access_key_invalid_length() {
// Test error cases for invalid lengths
assert!(gen_access_key(0).is_err(), "Should fail for length 0");
assert!(gen_access_key(1).is_err(), "Should fail for length 1");
assert!(gen_access_key(2).is_err(), "Should fail for length 2");
// Verify error message
let error = gen_access_key(2).unwrap_err();
assert_eq!(error.to_string(), "access key length is too short");
}
#[test]
fn test_gen_secret_key_valid_length() {
// Test valid secret key generation
let key = gen_secret_key(10).unwrap();
assert!(!key.is_empty(), "Secret key should not be empty");
let key_20 = gen_secret_key(20).unwrap();
assert!(!key_20.is_empty(), "Secret key should not be empty");
}
#[test]
fn test_gen_secret_key_uniqueness() {
// Test that generated secret keys are unique
let key1 = gen_secret_key(16).unwrap();
let key2 = gen_secret_key(16).unwrap();
assert_ne!(key1, key2, "Generated secret keys should be unique");
}
#[test]
fn test_gen_secret_key_base64_format() {
// Test that secret key is valid base64-like format
let key = gen_secret_key(32).unwrap();
// Should not contain invalid characters for URL-safe base64
for ch in key.chars() {
assert!(
ch.is_ascii_alphanumeric() || ch == '+' || ch == '-' || ch == '_',
"Secret key should be URL-safe base64 compatible"
);
}
}
#[test]
fn test_gen_secret_key_invalid_length() {
// Test error cases for invalid lengths
assert!(gen_secret_key(0).is_err(), "Should fail for length 0");
assert!(gen_secret_key(7).is_err(), "Should fail for length 7");
// Verify error message
let error = gen_secret_key(5).unwrap_err();
assert_eq!(error.to_string(), "secret key length is too short");
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Claims {
sub: String,
company: String,
exp: usize, // Expiration time (as UTC timestamp)
}
#[test]
fn test_generate_jwt_valid_token() {
// Test JWT generation with valid claims
let claims = Claims {
sub: "user1".to_string(),
company: "example".to_string(),
exp: 9999999999, // Far future timestamp for testing
};
let secret = "my_secret";
let token = generate_jwt(&claims, secret).unwrap();
assert!(!token.is_empty(), "JWT token should not be empty");
// JWT should have 3 parts separated by dots
let parts: Vec<&str> = token.split('.').collect();
assert_eq!(parts.len(), 3, "JWT should have 3 parts (header.payload.signature)");
// Each part should be non-empty
for part in parts {
assert!(!part.is_empty(), "JWT parts should not be empty");
}
}
#[test]
fn test_generate_jwt_different_secrets() {
// Test that different secrets produce different tokens
let claims = Claims {
sub: "user1".to_string(),
company: "example".to_string(),
exp: 9999999999, // Far future timestamp for testing
};
let token1 = generate_jwt(&claims, "secret1").unwrap();
let token2 = generate_jwt(&claims, "secret2").unwrap();
assert_ne!(token1, token2, "Different secrets should produce different tokens");
}
#[test]
fn test_generate_jwt_different_claims() {
// Test that different claims produce different tokens
let claims1 = Claims {
sub: "user1".to_string(),
company: "example".to_string(),
exp: 9999999999, // Far future timestamp for testing
};
let claims2 = Claims {
sub: "user2".to_string(),
company: "example".to_string(),
exp: 9999999999, // Far future timestamp for testing
};
let secret = "my_secret";
let token1 = generate_jwt(&claims1, secret).unwrap();
let token2 = generate_jwt(&claims2, secret).unwrap();
assert_ne!(token1, token2, "Different claims should produce different tokens");
}
#[test]
fn test_extract_claims_valid_token() {
// Test JWT claims extraction with valid token
let original_claims = Claims {
sub: "user1".to_string(),
company: "example".to_string(),
exp: 9999999999, // Far future timestamp for testing
};
let secret = "my_secret";
let token = generate_jwt(&original_claims, secret).unwrap();
let decoded = extract_claims::<Claims>(&token, secret).unwrap();
assert_eq!(decoded.claims, original_claims, "Decoded claims should match original claims");
}
#[test]
fn test_extract_claims_invalid_secret() {
// Test JWT claims extraction with wrong secret
let claims = Claims {
sub: "user1".to_string(),
company: "example".to_string(),
exp: 9999999999, // Far future timestamp for testing
};
let token = generate_jwt(&claims, "correct_secret").unwrap();
let result = extract_claims::<Claims>(&token, "wrong_secret");
assert!(result.is_err(), "Should fail with wrong secret");
}
#[test]
fn test_extract_claims_invalid_token() {
// Test JWT claims extraction with invalid token format
let invalid_tokens = [
"invalid.token",
"not.a.jwt.token",
"",
"header.payload", // Missing signature
"invalid_base64.invalid_base64.invalid_base64",
];
for invalid_token in &invalid_tokens {
let result = extract_claims::<Claims>(invalid_token, "secret");
assert!(result.is_err(), "Should fail with invalid token: {invalid_token}");
}
}
#[test]
fn test_jwt_round_trip_consistency() {
// Test complete round-trip: generate -> extract -> verify
let original_claims = Claims {
sub: "test_user".to_string(),
company: "test_company".to_string(),
exp: 9999999999, // Far future timestamp for testing
};
let secret = "test_secret_key";
// Generate token
let token = generate_jwt(&original_claims, secret).unwrap();
// Extract claims
let decoded = extract_claims::<Claims>(&token, secret).unwrap();
// Verify claims match
assert_eq!(decoded.claims, original_claims);
// Verify token data structure
assert!(matches!(decoded.header.alg, jsonwebtoken::Algorithm::HS512));
}
#[test]
fn test_jwt_with_empty_claims() {
// Test JWT with minimal claims
let empty_claims = Claims {
sub: String::new(),
company: String::new(),
exp: 9999999999, // Far future timestamp for testing
};
let secret = "secret";
let token = generate_jwt(&empty_claims, secret).unwrap();
let decoded = extract_claims::<Claims>(&token, secret).unwrap();
assert_eq!(decoded.claims, empty_claims);
}
#[test]
fn test_jwt_with_special_characters() {
// Test JWT with special characters in claims
let special_claims = Claims {
sub: "user@example.com".to_string(),
company: "Company & Co. (Ltd.)".to_string(),
exp: 9999999999, // Far future timestamp for testing
};
let secret = "secret_with_special_chars!@#$%";
let token = generate_jwt(&special_claims, secret).unwrap();
let decoded = extract_claims::<Claims>(&token, secret).unwrap();
assert_eq!(decoded.claims, special_claims);
}
#[test]
fn test_access_key_length_boundaries() {
// Test boundary conditions for access key length
assert!(gen_access_key(3).is_ok(), "Length 3 should be valid (minimum)");
assert!(gen_access_key(1000).is_ok(), "Large length should be valid");
// Test that minimum length is enforced
let min_key = gen_access_key(3).unwrap();
assert_eq!(min_key.len(), 3);
}
#[test]
fn test_secret_key_length_boundaries() {
// Test boundary conditions for secret key length
assert!(gen_secret_key(8).is_ok(), "Length 8 should be valid (minimum)");
assert!(gen_secret_key(1000).is_ok(), "Large length should be valid");
// Test that minimum length is enforced
let result = gen_secret_key(8);
assert!(result.is_ok(), "Minimum valid length should work");
}
}