mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-13 16:46:55 +00:00
feat: add admin user api
This commit is contained in:
@@ -18,6 +18,23 @@ const SECRET_KEY_MAX_LEN: usize = 40;
|
||||
pub const ACCOUNT_ON: &str = "on";
|
||||
pub const ACCOUNT_OFF: &str = "off";
|
||||
|
||||
const RESERVED_CHARS: &str = "=,";
|
||||
|
||||
// ContainsReservedChars - returns whether the input string contains reserved characters.
|
||||
pub fn contains_reserved_chars(s: &str) -> bool {
|
||||
s.contains(RESERVED_CHARS)
|
||||
}
|
||||
|
||||
// IsAccessKeyValid - validate access key for right length.
|
||||
pub fn is_access_key_valid(access_key: &str) -> bool {
|
||||
access_key.len() >= ACCESS_KEY_MIN_LEN
|
||||
}
|
||||
|
||||
// IsSecretKeyValid - validate secret key for right length.
|
||||
pub fn is_secret_key_valid(secret_key: &str) -> bool {
|
||||
secret_key.len() >= SECRET_KEY_MIN_LEN
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
|
||||
struct CredentialHeader {
|
||||
access_key: String,
|
||||
@@ -116,6 +133,10 @@ impl Credentials {
|
||||
}
|
||||
|
||||
pub fn is_expired(&self) -> bool {
|
||||
if self.expiration.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.expiration
|
||||
.as_ref()
|
||||
.map(|e| time::OffsetDateTime::now_utc() > *e)
|
||||
|
||||
+7
-1
@@ -32,9 +32,15 @@ pub enum Error {
|
||||
#[error("CredNotInitialized")]
|
||||
CredNotInitialized,
|
||||
|
||||
#[error("invalid key length")]
|
||||
#[error("invalid access key length")]
|
||||
InvalidAccessKeyLength,
|
||||
|
||||
#[error("invalid secret key length")]
|
||||
InvalidSecretKeyLength,
|
||||
|
||||
#[error("access key contains reserved characters =,")]
|
||||
ContainsReservedChars,
|
||||
|
||||
#[error("jwt err {0}")]
|
||||
JWTError(jsonwebtoken::errors::Error),
|
||||
|
||||
|
||||
+40
-3
@@ -1,8 +1,9 @@
|
||||
use auth::{Credentials, UserIdentity};
|
||||
use auth::{contains_reserved_chars, is_access_key_valid, is_secret_key_valid, Credentials, UserIdentity};
|
||||
use ecstore::store::ECStore;
|
||||
use log::debug;
|
||||
use madmin::AccountStatus;
|
||||
use manager::IamCache;
|
||||
use policy::{Args, Policy};
|
||||
use policy::{Args, Policy, UserType};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, OnceLock},
|
||||
@@ -71,7 +72,7 @@ pub fn get() -> crate::Result<Arc<IamCache<ObjectStore>>> {
|
||||
IAM_SYS.get().map(Arc::clone).ok_or(Error::IamSysNotInitialized)
|
||||
}
|
||||
|
||||
pub async fn is_allowed<'a>(args: Args<'a>) -> crate::Result<bool> {
|
||||
pub async fn is_allowed(args: Args<'_>) -> crate::Result<bool> {
|
||||
Ok(get()?.is_allowed(args).await)
|
||||
}
|
||||
|
||||
@@ -106,6 +107,42 @@ pub async fn get_user(ak: &str) -> crate::Result<(Option<UserIdentity>, bool)> {
|
||||
}
|
||||
|
||||
pub async fn create_user(ak: &str, sk: &str, status: &str) -> crate::Result<OffsetDateTime> {
|
||||
if !is_access_key_valid(ak) {
|
||||
return Err(Error::InvalidAccessKeyLength);
|
||||
}
|
||||
|
||||
if contains_reserved_chars(ak) {
|
||||
return Err(Error::ContainsReservedChars);
|
||||
}
|
||||
|
||||
if !is_secret_key_valid(sk) {
|
||||
return Err(Error::InvalidSecretKeyLength);
|
||||
}
|
||||
get()?.add_user(ak, sk, status).await
|
||||
// notify
|
||||
}
|
||||
|
||||
pub async fn delete_user(ak: &str, _notify: bool) -> crate::Result<()> {
|
||||
get()?.delete_user(ak, UserType::Reg).await
|
||||
// TODO NOTIFY
|
||||
}
|
||||
|
||||
pub async fn is_temp_user(ak: &str) -> crate::Result<(bool, String)> {
|
||||
if let Some(user) = get()?.get_user(ak).await? {
|
||||
Ok((user.credentials.is_temp(), user.credentials.parent_user))
|
||||
} else {
|
||||
Err(Error::NoSuchUser(ak.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_user_info(ak: &str) -> crate::Result<madmin::UserInfo> {
|
||||
get()?.get_user_info(ak).await
|
||||
}
|
||||
|
||||
pub async fn set_user_status(ak: &str, status: AccountStatus) -> crate::Result<OffsetDateTime> {
|
||||
get()?.set_user_status(ak, status).await
|
||||
}
|
||||
|
||||
pub async fn list_service_accounts(ak: &str) -> crate::Result<Vec<Credentials>> {
|
||||
get()?.list_service_accounts(ak).await
|
||||
}
|
||||
|
||||
+120
-2
@@ -9,6 +9,7 @@ use std::{
|
||||
|
||||
use ecstore::store_err::is_err_object_not_found;
|
||||
use log::{debug, warn};
|
||||
use madmin::AccountStatus;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::{
|
||||
select,
|
||||
@@ -295,6 +296,8 @@ where
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
warn!("uinfo {:?}", u);
|
||||
|
||||
if let Some(p) = policies.get(k) {
|
||||
u.policy_name = Some(p.policies.clone());
|
||||
u.updated_at = Some(p.update_at);
|
||||
@@ -313,7 +316,7 @@ where
|
||||
pub async fn add_user(&self, access_key: &str, secret_key: &str, status: &str) -> crate::Result<OffsetDateTime> {
|
||||
let status = {
|
||||
match status {
|
||||
"disabled" => auth::ACCOUNT_ON,
|
||||
val if val == AccountStatus::Enabled.as_ref() => auth::ACCOUNT_ON,
|
||||
auth::ACCOUNT_ON => auth::ACCOUNT_ON,
|
||||
_ => auth::ACCOUNT_OFF,
|
||||
}
|
||||
@@ -321,6 +324,7 @@ where
|
||||
|
||||
let users = self.cache.users.load();
|
||||
if let Some(x) = users.get(access_key) {
|
||||
warn!("user already exists: {:?}", x);
|
||||
if x.credentials.is_temp() {
|
||||
return Err(crate::Error::IAMActionNotAllowed);
|
||||
}
|
||||
@@ -329,7 +333,121 @@ where
|
||||
let user_entiry = UserIdentity::from(Credentials {
|
||||
access_key: access_key.to_string(),
|
||||
secret_key: secret_key.to_string(),
|
||||
status: status.to_string(),
|
||||
status: status.to_owned(),
|
||||
..Default::default()
|
||||
});
|
||||
let path = format!(
|
||||
"config/iam/{}{}/identity.json",
|
||||
UserType::Reg.prefix(),
|
||||
user_entiry.credentials.access_key
|
||||
);
|
||||
debug!("save object: {path:?}");
|
||||
self.api.save_iam_config(&user_entiry, path).await?;
|
||||
|
||||
Cache::add_or_update(
|
||||
&self.cache.users,
|
||||
&user_entiry.credentials.access_key,
|
||||
&user_entiry,
|
||||
OffsetDateTime::now_utc(),
|
||||
);
|
||||
|
||||
Ok(user_entiry.update_at.unwrap_or(OffsetDateTime::now_utc()))
|
||||
}
|
||||
|
||||
pub async fn delete_user(&self, access_key: &str, utype: UserType) -> crate::Result<()> {
|
||||
let users = self.cache.users.load();
|
||||
if let Some(x) = users.get(access_key) {
|
||||
if x.credentials.is_temp() {
|
||||
return Err(crate::Error::IAMActionNotAllowed);
|
||||
}
|
||||
}
|
||||
|
||||
// if utype == UserType::Reg {}
|
||||
|
||||
let path = format!("config/iam/{}{}/identity.json", UserType::Reg.prefix(), access_key);
|
||||
debug!("delete object: {path:?}");
|
||||
self.api.delete_iam_config(path).await?;
|
||||
|
||||
// delete cache
|
||||
Cache::delete(&self.cache.users, access_key, OffsetDateTime::now_utc());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_user(&self, access_key: &str) -> crate::Result<Option<UserIdentity>> {
|
||||
let u = self
|
||||
.cache
|
||||
.users
|
||||
.load()
|
||||
.get(access_key)
|
||||
.cloned()
|
||||
.or_else(|| self.cache.sts_accounts.load().get(access_key).cloned());
|
||||
|
||||
Ok(u)
|
||||
}
|
||||
|
||||
pub async fn get_user_info(&self, access_key: &str) -> crate::Result<madmin::UserInfo> {
|
||||
let users = self.cache.users.load();
|
||||
let policies = self.cache.user_policies.load();
|
||||
let group_members = self.cache.user_group_memeberships.load();
|
||||
|
||||
let u = match users.get(access_key) {
|
||||
Some(u) => u,
|
||||
None => return Err(Error::NoSuchUser(access_key.to_string())),
|
||||
};
|
||||
|
||||
if u.credentials.is_temp() || u.credentials.is_service_account() {
|
||||
return Err(Error::IAMActionNotAllowed);
|
||||
}
|
||||
|
||||
let mut uinfo = madmin::UserInfo {
|
||||
status: if u.credentials.is_valid() {
|
||||
madmin::AccountStatus::Enabled
|
||||
} else {
|
||||
madmin::AccountStatus::Disabled
|
||||
},
|
||||
updated_at: u.update_at,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Some(p) = policies.get(access_key) {
|
||||
uinfo.policy_name = Some(p.policies.clone());
|
||||
uinfo.updated_at = Some(p.update_at);
|
||||
}
|
||||
|
||||
if let Some(members) = group_members.get(access_key) {
|
||||
uinfo.member_of = Some(members.iter().cloned().collect());
|
||||
}
|
||||
|
||||
Ok(uinfo)
|
||||
}
|
||||
|
||||
pub async fn set_user_status(&self, access_key: &str, status: AccountStatus) -> crate::Result<OffsetDateTime> {
|
||||
if access_key.is_empty() {
|
||||
return Err(Error::InvalidArgument);
|
||||
}
|
||||
|
||||
let users = self.cache.users.load();
|
||||
let u = match users.get(access_key) {
|
||||
Some(u) => u,
|
||||
None => return Err(Error::NoSuchUser(access_key.to_string())),
|
||||
};
|
||||
|
||||
if u.credentials.is_temp() || u.credentials.is_service_account() {
|
||||
return Err(Error::IAMActionNotAllowed);
|
||||
}
|
||||
|
||||
let status = {
|
||||
match status {
|
||||
AccountStatus::Enabled => auth::ACCOUNT_ON,
|
||||
_ => auth::ACCOUNT_OFF,
|
||||
}
|
||||
};
|
||||
|
||||
let user_entiry = UserIdentity::from(Credentials {
|
||||
access_key: access_key.to_string(),
|
||||
secret_key: u.credentials.secret_key.clone(),
|
||||
status: status.to_owned(),
|
||||
..Default::default()
|
||||
});
|
||||
let path = format!(
|
||||
|
||||
+2
-1
@@ -16,7 +16,7 @@ pub use function::Functions;
|
||||
pub use id::ID;
|
||||
pub use policy::{default::DEFAULT_POLICIES, Policy};
|
||||
pub use resource::ResourceSet;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{de, Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
pub use statement::Statement;
|
||||
use std::collections::HashMap;
|
||||
@@ -93,6 +93,7 @@ pub trait Validator {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum UserType {
|
||||
Svc,
|
||||
Sts,
|
||||
|
||||
@@ -18,6 +18,7 @@ pub trait Store: Clone + Send + Sync + 'static {
|
||||
Item: DeserializeOwned;
|
||||
|
||||
async fn save_iam_config<Item: Serialize + Send>(&self, item: Item, path: impl AsRef<str> + Send) -> crate::Result<()>;
|
||||
async fn delete_iam_config(&self, path: impl AsRef<str> + Send) -> crate::Result<()>;
|
||||
|
||||
async fn load_all(&self, cache: &Cache) -> crate::Result<()>;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use ecstore::{
|
||||
store_api::{ObjectIO, ObjectInfo, ObjectOptions, PutObjReader},
|
||||
store_list_objects::{ObjectInfoOrErr, WalkOptions},
|
||||
utils::path::{dir, SLASH_SEPARATOR},
|
||||
StorageAPI,
|
||||
};
|
||||
use futures::future::try_join_all;
|
||||
use log::{debug, warn};
|
||||
@@ -121,6 +122,22 @@ impl ObjectStore {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Store for ObjectStore {
|
||||
async fn delete_iam_config(&self, path: impl AsRef<str> + Send) -> crate::Result<()> {
|
||||
self.object_api
|
||||
.delete_object(
|
||||
Self::BUCKET_NAME,
|
||||
path.as_ref(),
|
||||
ObjectOptions {
|
||||
delete_prefix: true,
|
||||
delete_prefix_object: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(crate::Error::EcstoreError)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
async fn load_iam_config<Item>(&self, path: impl AsRef<str> + Send) -> crate::Result<(Item, ObjectInfo)>
|
||||
where
|
||||
Item: DeserializeOwned,
|
||||
|
||||
Reference in New Issue
Block a user