extract_claims

This commit is contained in:
weisd
2025-01-12 01:41:22 +08:00
parent c3dd28c510
commit 86b4cae95d
15 changed files with 380 additions and 66 deletions

5
Cargo.lock generated
View File

@@ -1243,6 +1243,7 @@ dependencies = [
"itertools 0.14.0",
"jsonwebtoken",
"log",
"madmin",
"rand",
"serde",
"serde_json",
@@ -2508,7 +2509,7 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "s3s"
version = "0.11.0-dev"
source = "git+https://github.com/Nugine/s3s.git?rev=3291c0ca0284971569499cbe75bd69ef7bde8321#3291c0ca0284971569499cbe75bd69ef7bde8321"
source = "git+https://github.com/Nugine/s3s.git?rev=05efc3f87e9f5d1a6c9760c79cf1f70dcdd100c8#05efc3f87e9f5d1a6c9760c79cf1f70dcdd100c8"
dependencies = [
"arrayvec",
"async-trait",
@@ -2555,7 +2556,7 @@ dependencies = [
[[package]]
name = "s3s-policy"
version = "0.11.0-dev"
source = "git+https://github.com/Nugine/s3s.git?rev=3291c0ca0284971569499cbe75bd69ef7bde8321#3291c0ca0284971569499cbe75bd69ef7bde8321"
source = "git+https://github.com/Nugine/s3s.git?rev=05efc3f87e9f5d1a6c9760c79cf1f70dcdd100c8#05efc3f87e9f5d1a6c9760c79cf1f70dcdd100c8"
dependencies = [
"indexmap 2.6.0",
"serde",

View File

@@ -65,10 +65,10 @@ protos = { path = "./common/protos" }
rand = "0.8.5"
rmp = "0.8.14"
rmp-serde = "1.3.0"
s3s = { git = "https://github.com/Nugine/s3s.git", rev = "3291c0ca0284971569499cbe75bd69ef7bde8321", default-features = true, features = [
s3s = { git = "https://github.com/Nugine/s3s.git", rev = "05efc3f87e9f5d1a6c9760c79cf1f70dcdd100c8", default-features = true, features = [
"tower",
] }
s3s-policy = { git = "https://github.com/Nugine/s3s.git", rev = "3291c0ca0284971569499cbe75bd69ef7bde8321" }
s3s-policy = { git = "https://github.com/Nugine/s3s.git", rev = "05efc3f87e9f5d1a6c9760c79cf1f70dcdd100c8" }
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.134"
tempfile = "3.13.0"

View File

@@ -7,9 +7,13 @@ pub fn decode_tags(tags: &str) -> Vec<Tag> {
let mut list = Vec::new();
for (k, v) in values {
if k.is_empty() || v.is_empty() {
continue;
}
list.push(Tag {
key: k.to_string(),
value: v.to_string(),
key: Some(k.to_string()),
value: Some(v.to_string()),
});
}
@@ -20,7 +24,9 @@ pub fn encode_tags(tags: Vec<Tag>) -> String {
let mut encoded = form_urlencoded::Serializer::new(String::new());
for tag in tags.iter() {
encoded.append_pair(tag.key.as_str(), tag.value.as_str());
if let (Some(k), Some(v)) = (tag.key.as_ref(), tag.value.as_ref()) {
encoded.append_pair(k.as_str(), v.as_str());
}
}
encoded.finish()

View File

@@ -28,6 +28,7 @@ rand.workspace = true
base64-simd = "0.8.0"
jsonwebtoken = "9.3.0"
tracing.workspace = true
madmin.workspace = true
[dev-dependencies]
test-case.workspace = true

View File

@@ -1,15 +1,25 @@
mod credentials;
pub use credentials::Credentials;
pub use credentials::CredentialsBuilder;
pub use credentials::*;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct UserIdentity {
pub version: i64,
pub credentials: Credentials,
pub update_at: OffsetDateTime,
pub update_at: Option<OffsetDateTime>,
}
impl UserIdentity {
pub fn new(credentials: Credentials) -> Self {
UserIdentity {
version: 1,
credentials,
update_at: Some(OffsetDateTime::now_utc()),
}
}
}
impl From<Credentials> for UserIdentity {
@@ -17,7 +27,7 @@ impl From<Credentials> for UserIdentity {
UserIdentity {
version: 1,
credentials: value,
update_at: OffsetDateTime::now_utc(),
update_at: Some(OffsetDateTime::now_utc()),
}
}
}

View File

@@ -1,6 +1,8 @@
use crate::policy::{Policy, Validator};
use crate::service_type::ServiceType;
use crate::utils::extract_claims;
use crate::{utils, Error};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::cell::LazyCell;
@@ -13,8 +15,8 @@ const ACCESS_KEY_MAX_LEN: usize = 20;
const SECRET_KEY_MIN_LEN: usize = 8;
const SECRET_KEY_MAX_LEN: usize = 40;
const ACCOUNT_ON: &str = "on";
const ACCOUNT_OFF: &str = "off";
pub const ACCOUNT_ON: &str = "on";
pub const ACCOUNT_OFF: &str = "off";
#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
struct CredentialHeader {
@@ -89,7 +91,7 @@ impl TryFrom<&str> for CredentialHeader {
}
}
#[derive(Serialize, Deserialize, Clone, Default)]
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
pub struct Credentials {
pub access_key: String,
pub secret_key: String,
@@ -109,44 +111,6 @@ impl Credentials {
Self::check_key_value(header)
}
pub fn get_new_credentials_with_metadata<T: Serialize>(
claims: &T,
token_secret: &str,
exp: Option<usize>,
) -> crate::Result<Self> {
let ak = utils::gen_access_key(20).unwrap_or_default();
let sk = utils::gen_secret_key(32).unwrap_or_default();
Self::create_new_credentials_with_metadata(&ak, &sk, claims, token_secret, exp)
}
pub fn create_new_credentials_with_metadata<T: Serialize>(
ak: &str,
sk: &str,
claims: &T,
token_secret: &str,
exp: Option<usize>,
) -> crate::Result<Self> {
if ak.len() < ACCESS_KEY_MIN_LEN || ak.len() > ACCESS_KEY_MAX_LEN {
return Err(Error::InvalidAccessKeyLength);
}
if sk.len() < SECRET_KEY_MIN_LEN || sk.len() > SECRET_KEY_MAX_LEN {
return Err(Error::InvalidAccessKeyLength);
}
let token = utils::generate_jwt(claims, token_secret).map_err(Error::JWTError)?;
Ok(Self {
access_key: ak.to_owned(),
secret_key: sk.to_owned(),
session_token: token,
status: ACCOUNT_ON.to_owned(),
expiration: exp.map(|v| OffsetDateTime::now_utc().saturating_add(Duration::seconds(v as i64))),
..Default::default()
})
}
pub fn check_key_value(_header: CredentialHeader) -> crate::Result<Self> {
todo!()
}
@@ -186,6 +150,50 @@ impl Credentials {
}
}
pub fn get_new_credentials_with_metadata<T: Serialize>(
claims: &T,
token_secret: &str,
exp: Option<usize>,
) -> crate::Result<Credentials> {
let ak = utils::gen_access_key(20).unwrap_or_default();
let sk = utils::gen_secret_key(32).unwrap_or_default();
create_new_credentials_with_metadata(&ak, &sk, claims, token_secret, exp)
}
pub fn create_new_credentials_with_metadata<T: Serialize>(
ak: &str,
sk: &str,
claims: &T,
token_secret: &str,
exp: Option<usize>,
) -> crate::Result<Credentials> {
if ak.len() < ACCESS_KEY_MIN_LEN || ak.len() > ACCESS_KEY_MAX_LEN {
return Err(Error::InvalidAccessKeyLength);
}
if sk.len() < SECRET_KEY_MIN_LEN || sk.len() > SECRET_KEY_MAX_LEN {
return Err(Error::InvalidAccessKeyLength);
}
let token = utils::generate_jwt(claims, token_secret).map_err(Error::JWTError)?;
Ok(Credentials {
access_key: ak.to_owned(),
secret_key: sk.to_owned(),
session_token: token,
status: ACCOUNT_ON.to_owned(),
expiration: exp.map(|v| OffsetDateTime::now_utc().saturating_add(Duration::seconds(v as i64))),
..Default::default()
})
}
pub fn get_claims_from_token_with_secret<T: DeserializeOwned>(token: &str, secret: &str) -> crate::Result<T> {
let ms = extract_claims::<T>(token, secret).map_err(Error::JWTError)?;
// TODO SessionPolicyName
Ok(ms.claims)
}
#[derive(Default)]
pub struct CredentialsBuilder {
session_policy: Option<Policy>,

View File

@@ -37,6 +37,15 @@ pub enum Error {
#[error("jwt err {0}")]
JWTError(jsonwebtoken::errors::Error),
#[error("no access key")]
NoAccessKey,
#[error("invalid token")]
InvalidToken,
#[error("invalid access_key")]
InvalidAccessKey,
}
pub type Result<T> = std::result::Result<T, Error>;

View File

@@ -3,7 +3,10 @@ use ecstore::store::ECStore;
use log::debug;
use manager::IamCache;
use policy::{Args, Policy};
use std::sync::{Arc, OnceLock};
use std::{
collections::HashMap,
sync::{Arc, OnceLock},
};
use store::object::ObjectStore;
use time::OffsetDateTime;
@@ -85,6 +88,19 @@ pub async fn add_service_account(cred: Credentials) -> crate::Result<OffsetDateT
get()?.add_service_account(cred).await
}
pub async fn check_key(ak: &str) -> crate::Result<Option<UserIdentity>> {
pub async fn check_key(ak: &str) -> crate::Result<(Option<UserIdentity>, bool)> {
if let Some(sys_cred) = get_global_action_cred() {
if sys_cred.access_key == ak {
return Ok((Some(UserIdentity::new(sys_cred)), true));
}
}
get()?.check_key(ak).await
}
pub async fn list_users() -> crate::Result<HashMap<String, madmin::UserInfo>> {
get()?.get_users().await
}
pub async fn get_user(ak: &str) -> crate::Result<(Option<UserIdentity>, bool)> {
get()?.check_key(ak).await
}

View File

@@ -8,7 +8,7 @@ use std::{
};
use ecstore::store_err::is_err_object_not_found;
use log::debug;
use log::{debug, warn};
use time::OffsetDateTime;
use tokio::{
select,
@@ -188,7 +188,7 @@ where
OffsetDateTime::now_utc(),
);
Ok(user_entiry.update_at)
Ok(user_entiry.update_at.unwrap_or(OffsetDateTime::now_utc()))
}
pub async fn is_allowed<'a>(&self, args: Args<'a>) -> bool {
@@ -209,7 +209,7 @@ where
Ok((u.clone(), None))
}
pub async fn check_key(&self, ak: &str) -> crate::Result<Option<UserIdentity>> {
pub async fn check_key(&self, ak: &str) -> crate::Result<(Option<UserIdentity>, bool)> {
let user = self
.cache
.users
@@ -219,8 +219,14 @@ where
.or_else(|| self.cache.sts_accounts.load().get(ak).cloned());
match user {
Some(u) if u.credentials.is_valid() => Ok(Some(u)),
_ => Ok(None),
Some(u) => {
if u.credentials.is_valid() {
Ok((Some(u), true))
} else {
Ok((Some(u), false))
}
}
_ => Ok((None, false)),
}
}
pub async fn policy_db_get(&self, name: &str, _groups: Option<Vec<String>>) -> crate::Result<Vec<String>> {
@@ -263,4 +269,44 @@ where
Ok(())
}
// returns all users (not STS or service accounts)
pub async fn get_users(&self) -> crate::Result<HashMap<String, madmin::UserInfo>> {
let mut m = HashMap::new();
let users = self.cache.users.load();
let policies = self.cache.user_policies.load();
let group_members = self.cache.user_group_memeberships.load();
for (k, v) in users.iter() {
warn!("k: {}, v: {:?}", k, v.credentials);
if v.credentials.is_temp() || v.credentials.is_service_account() {
continue;
}
let mut u = madmin::UserInfo {
status: if v.credentials.is_valid() {
madmin::AccountStatus::Enabled
} else {
madmin::AccountStatus::Disabled
},
updated_at: v.update_at,
..Default::default()
};
if let Some(p) = policies.get(k) {
u.policy_name = Some(p.policies.clone());
u.updated_at = Some(p.update_at);
}
if let Some(members) = group_members.get(k) {
u.member_of = Some(members.iter().cloned().collect());
}
m.insert(k.clone(), u);
}
Ok(m)
}
}

View File

@@ -1,7 +1,7 @@
use crate::Error;
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use jsonwebtoken::{encode, Algorithm, DecodingKey, EncodingKey, Header};
use rand::{Rng, RngCore};
use serde::Serialize;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
pub fn gen_access_key(length: usize) -> crate::Result<String> {
const ALPHA_NUMERIC_TABLE: [char; 36] = [
@@ -45,6 +45,17 @@ pub fn generate_jwt<T: Serialize>(claims: &T, secret: &str) -> Result<String, js
encode(&header, &claims, &EncodingKey::from_secret(secret.as_bytes()))
}
pub fn extract_claims<T: DeserializeOwned>(
token: &str,
secret: &str,
) -> 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::{gen_access_key, gen_secret_key};

View File

@@ -5,6 +5,8 @@ pub mod metrics;
pub mod net;
pub mod service_commands;
pub mod trace;
pub mod user;
pub mod utils;
pub use info_commands::*;
pub use user::*;

52
madmin/src/user.rs Normal file
View File

@@ -0,0 +1,52 @@
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
#[derive(Debug, Serialize, Deserialize, Default)]
pub enum AccountStatus {
#[serde(rename = "enabled")]
Enabled,
#[serde(rename = "disabled")]
#[default]
Disabled,
}
#[derive(Debug, Serialize, Deserialize)]
pub enum UserAuthType {
#[serde(rename = "builtin")]
Builtin,
#[serde(rename = "ldap")]
Ldap,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UserAuthInfo {
#[serde(rename = "type")]
pub auth_type: UserAuthType,
#[serde(rename = "authServer", skip_serializing_if = "Option::is_none")]
pub auth_server: Option<String>,
#[serde(rename = "authServerUserID", skip_serializing_if = "Option::is_none")]
pub auth_server_user_id: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct UserInfo {
#[serde(rename = "userAuthInfo", skip_serializing_if = "Option::is_none")]
pub auth_info: Option<UserAuthInfo>,
#[serde(rename = "secretKey", skip_serializing_if = "Option::is_none")]
pub secret_key: Option<String>,
#[serde(rename = "policyName", skip_serializing_if = "Option::is_none")]
pub policy_name: Option<String>,
#[serde(rename = "status")]
pub status: AccountStatus,
#[serde(rename = "memberOf", skip_serializing_if = "Option::is_none")]
pub member_of: Option<Vec<String>>,
#[serde(rename = "updatedAt")]
pub updated_at: Option<OffsetDateTime>,
}

View File

@@ -22,7 +22,8 @@ use ecstore::GLOBAL_Endpoints;
use futures::{Stream, StreamExt};
use http::Uri;
use hyper::StatusCode;
use iam::get_global_action_cred;
use iam::auth::create_new_credentials_with_metadata;
use iam::{auth, get_global_action_cred};
use madmin::metrics::RealtimeMetrics;
use madmin::utils::parse_duration;
use matchit::Params;
@@ -107,6 +108,63 @@ fn get_token_signing_key() -> Option<String> {
}
}
pub async fn check_key_valid(st: &str, ak: &str) -> S3Result<(auth::Credentials, bool)> {
let Some(mut cred) = get_global_action_cred() else {
return Err(s3_error!(InternalError, "action cred not init"));
};
if cred.access_key != ak {
let Ok(iam_store) = iam::get() else { return Err(s3_error!(InternalError, "iam not init")) };
match iam_store
.check_key(ak)
.await
.map_err(|_e| s3_error!(InternalError, "check key failed"))?
{
(Some(u), true) => {
cred = u.credentials;
}
(Some(u), false) => {
if u.credentials.status == "off" {
return Err(s3_error!(InvalidRequest, "ErrAccessKeyDisabled"));
}
return Err(s3_error!(InvalidRequest, "check key failed"));
}
_ => {
return Err(s3_error!(InvalidRequest, "check key failed"));
}
}
return Ok((cred, true));
}
unimplemented!()
}
pub fn check_claims_from_token(token: &str, cred: &auth::Credentials) -> crate::Result<HashMap<String, Vec<String>>> {
// if !token.is_empty() && cred.access_key.is_empty() {
// return Err(Error::NoAccessKey);
// }
// if token.is_empty() && cred.is_temp() && !cred.is_service_account() {
// return Err(Error::InvalidToken);
// }
// if !token.is_empty() && !cred.is_temp() {
// return Err(Error::InvalidToken);
// }
// if !cred.is_service_account() && cred.is_temp() && token != cred.session_token {
// return Err(Error::InvalidToken);
// }
// if cred.is_temp() || cred.is_expired() {
// return Err(Error::InvalidAccessKey);
// }
unimplemented!()
}
pub struct AssumeRoleHandle {}
#[async_trait::async_trait]
impl Operation for AssumeRoleHandle {
@@ -164,7 +222,7 @@ impl Operation for AssumeRoleHandle {
claims.access_key = ak.clone();
let mut cred = match iam::auth::Credentials::create_new_credentials_with_metadata(&ak, &sk, &claims, &secret, Some(exp)) {
let mut cred = match create_new_credentials_with_metadata(&ak, &sk, &claims, &secret, Some(exp)) {
Ok(res) => res,
Err(_er) => return Err(s3_error!(InvalidRequest, "")),
};

View File

@@ -1,17 +1,60 @@
use futures::TryFutureExt;
use http::StatusCode;
use iam::get_global_action_cred;
use matchit::Params;
use s3s::{s3_error, Body, S3Request, S3Response, S3Result};
use s3s::{s3_error, Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result};
use serde::Deserialize;
use serde_urlencoded::from_bytes;
use tracing::warn;
use crate::admin::router::Operation;
#[derive(Debug, Deserialize, Default)]
pub struct AddUserQuery {
#[serde(rename = "accessKey")]
pub access_key: Option<String>,
}
pub struct AddUser {}
#[async_trait::async_trait]
impl Operation for AddUser {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let query = {
if let Some(query) = req.uri.query() {
let input: AddUserQuery =
from_bytes(query.as_bytes()).map_err(|_e| s3_error!(InvalidArgument, "get body failed"))?;
input
} else {
AddUserQuery::default()
}
};
let ak = query.access_key.as_deref().unwrap_or_default();
if let Some(sys_cred) = get_global_action_cred() {
if sys_cred.access_key == ak {
return Err(s3_error!(InvalidArgument, "can't create user with system access key"));
}
}
if let (Some(user), true) = iam::get_user(ak)
.await
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("marshal users err {}", e)))?
{
if user.credentials.is_temp() || user.credentials.is_service_account() {
return Err(s3_error!(InvalidArgument, "can't create user with service account access key"));
}
}
warn!("handle AddUser");
return Err(s3_error!(NotImplemented));
}
}
fn check_claims_from_token(_token: &str) -> bool {
true
}
pub struct SetUserStatus {}
#[async_trait::async_trait]
impl Operation for SetUserStatus {
@@ -19,3 +62,35 @@ impl Operation for SetUserStatus {
return Err(s3_error!(NotImplemented));
}
}
pub struct ListUsers {}
#[async_trait::async_trait]
impl Operation for ListUsers {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle ListUsers");
let users = iam::list_users()
.await
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
let body = serde_json::to_string(&users)
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("marshal users err {}", e)))?;
Ok(S3Response::new((StatusCode::OK, Body::from(body))))
}
}
pub struct RemoveUser {}
#[async_trait::async_trait]
impl Operation for RemoveUser {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
warn!("handle RemoveUser");
return Err(s3_error!(NotImplemented));
}
}
pub struct GetUserInfo {}
#[async_trait::async_trait]
impl Operation for GetUserInfo {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
return Err(s3_error!(NotImplemented));
}
}

View File

@@ -126,13 +126,32 @@ fn regist_user_route(r: &mut S3Router<AdminOperation>) -> Result<()> {
format!("{}{}", ADMIN_PREFIX, "/v3/accountinfo").as_str(),
AdminOperation(&handlers::AccountInfoHandler {}),
)?;
r.insert(
Method::GET,
format!("{}{}", ADMIN_PREFIX, "/v3/list-users").as_str(),
AdminOperation(&user::ListUsers {}),
)?;
r.insert(
Method::GET,
format!("{}{}", ADMIN_PREFIX, "/v3/user-info").as_str(),
AdminOperation(&user::GetUserInfo {}),
)?;
r.insert(
Method::DELETE,
format!("{}{}", ADMIN_PREFIX, "/v3/remove-user").as_str(),
AdminOperation(&user::RemoveUser {}),
)?;
r.insert(
Method::PUT,
format!("{}{}", ADMIN_PREFIX, "/v3/add-user").as_str(),
AdminOperation(&user::AddUser {}),
)?;
r.insert(
Method::GET,
Method::PUT,
format!("{}{}", ADMIN_PREFIX, "/v3/set-user-status").as_str(),
AdminOperation(&user::SetUserStatus {}),
)?;