use crate::error::Error as IamError;
use crate::error::{Error, Result};
use crate::policy::{INHERITED_POLICY_TYPE, Policy, Validator, iam_policy_claim_name_sa};
use crate::utils;
use crate::utils::extract_claims;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::collections::HashMap;
use time::OffsetDateTime;
use time::macros::offset;
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 = "=,";
pub fn contains_reserved_chars(s: &str) -> bool {
s.contains(RESERVED_CHARS)
}
pub fn is_access_key_valid(access_key: &str) -> bool {
access_key.len() >= ACCESS_KEY_MIN_LEN
}
pub fn is_secret_key_valid(secret_key: &str) -> bool {
secret_key.len() >= SECRET_KEY_MIN_LEN
}
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
pub struct Credentials {
pub access_key: String,
pub secret_key: String,
pub session_token: String,
pub expiration: Option<OffsetDateTime>,
pub status: String,
pub parent_user: String,
pub groups: Option<Vec<String>>,
pub claims: Option<HashMap<String, Value>>,
pub name: Option<String>,
pub description: Option<String>,
}
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)
.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).is_some_and(|_| !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()).is_some_and(|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<String, Value>, token_secret: &str) -> Result<Credentials> {
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<String, Value>,
token_secret: &str,
) -> Result<Credentials> {
if ak.len() < ACCESS_KEY_MIN_LEN || ak.len() > ACCESS_KEY_MAX_LEN {
return Err(IamError::InvalidAccessKeyLength);
}
if sk.len() < SECRET_KEY_MIN_LEN || sk.len() > SECRET_KEY_MAX_LEN {
return Err(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<T: DeserializeOwned>(token: &str, secret: &str) -> Result<T> {
let ms = extract_claims::<T>(token, secret)?;
Ok(ms.claims)
}
pub fn jwt_sign<T: Serialize>(claims: &T, token_secret: &str) -> Result<String> {
let token = utils::generate_jwt(claims, token_secret)?;
Ok(token)
}
#[derive(Default)]
pub struct CredentialsBuilder {
session_policy: Option<Policy>,
access_key: String,
secret_key: String,
name: Option<String>,
description: Option<String>,
expiration: Option<OffsetDateTime>,
allow_site_replicator_account: bool,
claims: Option<serde_json::Value>,
parent_user: String,
groups: Option<Vec<String>>,
}
impl CredentialsBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn session_policy(mut self, policy: Option<Policy>) -> 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<OffsetDateTime>) -> 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<String>) -> Self {
self.groups = Some(groups);
self
}
pub fn try_build(self) -> Result<Credentials> {
self.try_into()
}
}
impl TryFrom<CredentialsBuilder> for Credentials {
type Error = Error;
fn try_from(mut value: CredentialsBuilder) -> std::result::Result<Self, Self::Error> {
if value.parent_user.is_empty() {
return Err(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::other("Either ak or sk is empty"));
}
if value.parent_user == value.access_key.as_str() {
return Err(IamError::InvalidArgument);
}
if value.access_key == "site-replicator-0" && !value.allow_site_replicator_account {
return Err(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(|_| IamError::InvalidArgument)?;
if policy_buf.len() > 4096 {
return Err(Error::other("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 = rustfs_crypto::jwt_encode(value.access_key.as_bytes(), &claim)
.map_err(|_| Error::other("session policy is too large"))?;
cred.session_token = session_token;
} else {
}
cred.expiration = value.expiration;
cred.access_key = value.access_key;
cred.secret_key = value.secret_key;
Ok(cred)
}
}