mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-15 01:23:12 +00:00
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:
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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>,
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user