use crate::error::Error as IamError; use crate::policy::Policy; use crate::sys::{iam_policy_claim_name_sa, Validator, INHERITED_POLICY_TYPE}; use crate::utils; use crate::utils::extract_claims; use ecstore::error::{Error, Result}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::collections::HashMap; use time::macros::offset; use time::OffsetDateTime; const ACCESS_KEY_MIN_LEN: usize = 3; const ACCESS_KEY_MAX_LEN: usize = 20; const SECRET_KEY_MIN_LEN: usize = 8; 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, // scop: CredentialHeaderScope, // } // #[cfg_attr(test, derive(PartialEq, Eq, Debug))] // struct CredentialHeaderScope { // date: Date, // region: String, // service: ServiceType, // request: String, // } // impl TryFrom<&str> for CredentialHeader { // type Error = Error; // fn try_from(value: &str) -> Result { // let mut elem = value.trim().splitn(2, '='); // let (Some(h), Some(cred_elems)) = (elem.next(), elem.next()) else { // return Err(Error::new(IamError::ErrCredMalformed)); // }; // if h != "Credential" { // return Err(Error::new(IamError::ErrCredMalformed)); // } // let mut cred_elems = cred_elems.trim().rsplitn(5, '/'); // let Some(request) = cred_elems.next() else { // return Err(Error::new(IamError::ErrCredMalformed)); // }; // let Some(service) = cred_elems.next() else { // return Err(Error::new(IamError::ErrCredMalformed)); // }; // let Some(region) = cred_elems.next() else { // return Err(Error::new(IamError::ErrCredMalformed)); // }; // let Some(date) = cred_elems.next() else { // return Err(Error::new(IamError::ErrCredMalformed)); // }; // let Some(ak) = cred_elems.next() else { // return Err(Error::new(IamError::ErrCredMalformed)); // }; // if ak.len() < 3 { // return Err(Error::new(IamError::ErrCredMalformed)); // } // if request != "aws4_request" { // return Err(Error::new(IamError::ErrCredMalformed)); // } // Ok(CredentialHeader { // access_key: ak.to_owned(), // scop: CredentialHeaderScope { // date: { // const FORMATTER: LazyCell>> = // LazyCell::new(|| time::format_description::parse("[year][month][day]").unwrap()); // Date::parse(date, &FORMATTER).map_err(|_| Error::new(IamError::ErrCredMalformed))? // }, // region: region.to_owned(), // service: service.try_into()?, // request: request.to_owned(), // }, // }) // } // } #[derive(Serialize, Deserialize, Clone, Default, Debug)] pub struct Credentials { pub access_key: String, pub secret_key: String, pub session_token: String, pub expiration: Option, pub status: String, pub parent_user: String, pub groups: Option>, pub claims: Option>, pub name: Option, pub description: Option, } impl Credentials { // pub fn new(elem: &str) -> Result { // let header: CredentialHeader = elem.try_into()?; // Self::check_key_value(header) // } // pub fn check_key_value(_header: CredentialHeader) -> Result { // todo!() // } pub fn is_expired(&self) -> bool { if self.expiration.is_none() { return false; } self.expiration .as_ref() .map(|e| time::OffsetDateTime::now_utc() > *e) .unwrap_or(false) } pub fn is_temp(&self) -> bool { !self.session_token.is_empty() && !self.is_expired() } pub fn is_service_account(&self) -> bool { const IAM_POLICY_CLAIM_NAME_SA: &str = "sa-policy"; self.claims .as_ref() .map(|x| { x.get(IAM_POLICY_CLAIM_NAME_SA) .map_or(false, |_| !self.parent_user.is_empty()) }) .unwrap_or_default() } pub fn is_implied_policy(&self) -> bool { if self.is_service_account() { return self .claims .as_ref() .map(|x| { x.get(&iam_policy_claim_name_sa()) .map_or(false, |v| v == INHERITED_POLICY_TYPE) }) .unwrap_or_default(); } false } pub fn is_valid(&self) -> bool { if self.status == "off" { return false; } self.access_key.len() >= 3 && self.secret_key.len() >= 8 && !self.is_expired() } pub fn is_owner(&self) -> bool { false } } pub fn generate_credentials() -> Result<(String, String)> { let ak = utils::gen_access_key(20)?; let sk = utils::gen_secret_key(40)?; Ok((ak, sk)) } pub fn get_new_credentials_with_metadata(claims: &HashMap, token_secret: &str) -> Result { let (ak, sk) = generate_credentials()?; create_new_credentials_with_metadata(&ak, &sk, claims, token_secret) } pub fn create_new_credentials_with_metadata( ak: &str, sk: &str, claims: &HashMap, token_secret: &str, ) -> Result { if ak.len() < ACCESS_KEY_MIN_LEN || ak.len() > ACCESS_KEY_MAX_LEN { return Err(Error::new(IamError::InvalidAccessKeyLength)); } if sk.len() < SECRET_KEY_MIN_LEN || sk.len() > SECRET_KEY_MAX_LEN { return Err(Error::new(IamError::InvalidAccessKeyLength)); } if token_secret.is_empty() { return Ok(Credentials { access_key: ak.to_owned(), secret_key: sk.to_owned(), status: ACCOUNT_OFF.to_owned(), ..Default::default() }); } let expiration = { if let Some(v) = claims.get("exp") { if let Some(expiry) = v.as_i64() { Some(OffsetDateTime::from_unix_timestamp(expiry)?.to_offset(offset!(+8))) } else { None } } else { None } }; let token = utils::generate_jwt(&claims, token_secret)?; Ok(Credentials { access_key: ak.to_owned(), secret_key: sk.to_owned(), session_token: token, status: ACCOUNT_ON.to_owned(), expiration, ..Default::default() }) } pub fn get_claims_from_token_with_secret(token: &str, secret: &str) -> Result { let ms = extract_claims::(token, secret)?; // TODO SessionPolicyName Ok(ms.claims) } pub fn jwt_sign(claims: &T, token_secret: &str) -> Result { let token = utils::generate_jwt(claims, token_secret)?; Ok(token) } #[derive(Default)] pub struct CredentialsBuilder { session_policy: Option, access_key: String, secret_key: String, name: Option, description: Option, expiration: Option, allow_site_replicator_account: bool, claims: Option, parent_user: String, groups: Option>, } impl CredentialsBuilder { pub fn new() -> Self { Self::default() } pub fn session_policy(mut self, policy: Option) -> Self { self.session_policy = policy; self } pub fn access_key(mut self, access_key: String) -> Self { self.access_key = access_key; self } pub fn secret_key(mut self, secret_key: String) -> Self { self.secret_key = secret_key; self } pub fn name(mut self, name: String) -> Self { self.name = Some(name); self } pub fn description(mut self, description: String) -> Self { self.description = Some(description); self } pub fn expiration(mut self, expiration: Option) -> Self { self.expiration = expiration; self } pub fn allow_site_replicator_account(mut self, allow_site_replicator_account: bool) -> Self { self.allow_site_replicator_account = allow_site_replicator_account; self } pub fn claims(mut self, claims: serde_json::Value) -> Self { self.claims = Some(claims); self } pub fn parent_user(mut self, parent_user: String) -> Self { self.parent_user = parent_user; self } pub fn groups(mut self, groups: Vec) -> Self { self.groups = Some(groups); self } pub fn try_build(self) -> Result { self.try_into() } } impl TryFrom for Credentials { type Error = Error; fn try_from(mut value: CredentialsBuilder) -> Result { if value.parent_user.is_empty() { return Err(Error::new(IamError::InvalidArgument)); } if (value.access_key.is_empty() && !value.secret_key.is_empty()) || (!value.access_key.is_empty() && value.secret_key.is_empty()) { return Err(Error::msg("Either ak or sk is empty")); } if value.parent_user == value.access_key.as_str() { return Err(Error::new(IamError::InvalidArgument)); } if value.access_key == "site-replicator-0" && !value.allow_site_replicator_account { return Err(Error::new(IamError::InvalidArgument)); } let mut claim = serde_json::json!({ "parent": value.parent_user }); if let Some(p) = value.session_policy { p.is_valid()?; let policy_buf = serde_json::to_vec(&p).map_err(|_| Error::new(IamError::InvalidArgument))?; if policy_buf.len() > 4096 { return Err(Error::msg("session policy is too large")); } claim["sessionPolicy"] = serde_json::json!(base64_simd::STANDARD.encode_to_string(&policy_buf)); claim["sa-policy"] = serde_json::json!("embedded-policy"); } else { claim["sa-policy"] = serde_json::json!("inherited-policy"); } if let Some(Value::Object(obj)) = value.claims { for (key, value) in obj { if claim.get(&key).is_some() { continue; } claim[key] = value; } } if value.access_key.is_empty() { value.access_key = utils::gen_access_key(20)?; } if value.secret_key.is_empty() { value.access_key = utils::gen_secret_key(40)?; } claim["accessKey"] = json!(&value.access_key); let mut cred = Credentials { status: "on".into(), parent_user: value.parent_user, groups: value.groups, name: value.name, description: value.description, ..Default::default() }; if !value.secret_key.is_empty() { let session_token = crypto::jwt_encode(value.access_key.as_bytes(), &claim).map_err(|_| Error::msg("session policy is too large"))?; cred.session_token = session_token; // cred.expiration = Some( // OffsetDateTime::from_unix_timestamp( // claim // .get("exp") // .and_then(|x| x.as_i64()) // .ok_or(Error::StringError("invalid exp".into()))?, // ) // .map_err(|_| Error::StringError("invalie timestamp".into()))?, // ); } else { // cred.expiration = // Some(OffsetDateTime::from_unix_timestamp(0).map_err(|_| Error::StringError("invalie timestamp".into()))?); } cred.expiration = value.expiration; cred.access_key = value.access_key; cred.secret_key = value.secret_key; Ok(cred) } } // #[cfg(test)] // #[allow(non_snake_case)] // mod tests { // use test_case::test_case; // use time::Date; // use super::CredentialHeader; // use super::CredentialHeaderScope; // use crate::service_type::ServiceType; // #[test_case( // "Credential=aaaaaaaaaaaaaaaaaaaa/20241127/us-east-1/s3/aws4_request" => // CredentialHeader{ // access_key: "aaaaaaaaaaaaaaaaaaaa".into(), // scop: CredentialHeaderScope { // date: Date::from_calendar_date(2024, time::Month::November, 27).unwrap(), // region: "us-east-1".to_owned(), // service: ServiceType::S3, // request: "aws4_request".into(), // } // }; // "1")] // #[test_case( // "Credential=aaaaaaaaaaa/aaaaaaaaa/20241127/us-east-1/s3/aws4_request" => // CredentialHeader{ // access_key: "aaaaaaaaaaa/aaaaaaaaa".into(), // scop: CredentialHeaderScope { // date: Date::from_calendar_date(2024, time::Month::November, 27).unwrap(), // region: "us-east-1".to_owned(), // service: ServiceType::S3, // request: "aws4_request".into(), // } // }; // "2")] // #[test_case( // "Credential=aaaaaaaaaaa/aaaaaaaaa/20241127/us-east-1/sts/aws4_request" => // CredentialHeader{ // access_key: "aaaaaaaaaaa/aaaaaaaaa".into(), // scop: CredentialHeaderScope { // date: Date::from_calendar_date(2024, time::Month::November, 27).unwrap(), // region: "us-east-1".to_owned(), // service: ServiceType::STS, // request: "aws4_request".into(), // } // }; // "3")] // fn test_CredentialHeader_from_str_successful(input: &str) -> CredentialHeader { // CredentialHeader::try_from(input).unwrap() // } // #[test_case("Credential")] // #[test_case("Cred=")] // #[test_case("Credential=abc")] // #[test_case("Credential=a/20241127/us-east-1/s3/aws4_request")] // #[test_case("Credential=aa/20241127/us-east-1/s3/aws4_request")] // #[test_case("Credential=aaaa/20241127/us-east-1/asa/aws4_request")] // #[test_case("Credential=aaaa/20241127/us-east-1/sts/aws4a_request")] // fn test_credential_header_from_str_failed(input: &str) { // if CredentialHeader::try_from(input).is_ok() { // unreachable!() // } // } // }