mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-06 21:33:14 +00:00
Merge branch 'main' of github.com:rustfs/s3-rustfs into feature/observability
# Conflicts: # rustfs/src/storage/ecfs.rs
This commit is contained in:
@@ -1,90 +0,0 @@
|
||||
use ecstore::error::{Error, Result};
|
||||
use regex::Regex;
|
||||
|
||||
const ARN_PREFIX_ARN: &str = "arn";
|
||||
const ARN_PARTITION_RUSTFS: &str = "rustfs";
|
||||
const ARN_SERVICE_IAM: &str = "iam";
|
||||
const ARN_RESOURCE_TYPE_ROLE: &str = "role";
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ARN {
|
||||
pub partition: String,
|
||||
pub service: String,
|
||||
pub region: String,
|
||||
pub resource_type: String,
|
||||
pub resource_id: String,
|
||||
}
|
||||
|
||||
impl ARN {
|
||||
pub fn new_iam_role_arn(resource_id: &str, server_region: &str) -> Result<Self> {
|
||||
let valid_resource_id_regex = Regex::new(r"^[A-Za-z0-9_/\.-]+$").unwrap();
|
||||
if !valid_resource_id_regex.is_match(resource_id) {
|
||||
return Err(Error::msg("ARN resource ID invalid"));
|
||||
}
|
||||
Ok(ARN {
|
||||
partition: ARN_PARTITION_RUSTFS.to_string(),
|
||||
service: ARN_SERVICE_IAM.to_string(),
|
||||
region: server_region.to_string(),
|
||||
resource_type: ARN_RESOURCE_TYPE_ROLE.to_string(),
|
||||
resource_id: resource_id.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse(arn_str: &str) -> Result<Self> {
|
||||
let ps: Vec<&str> = arn_str.split(':').collect();
|
||||
if ps.len() != 6 || ps[0] != ARN_PREFIX_ARN {
|
||||
return Err(Error::msg("ARN format invalid"));
|
||||
}
|
||||
|
||||
if ps[1] != ARN_PARTITION_RUSTFS {
|
||||
return Err(Error::msg("ARN partition invalid"));
|
||||
}
|
||||
|
||||
if ps[2] != ARN_SERVICE_IAM {
|
||||
return Err(Error::msg("ARN service invalid"));
|
||||
}
|
||||
|
||||
if !ps[4].is_empty() {
|
||||
return Err(Error::msg("ARN account-id invalid"));
|
||||
}
|
||||
|
||||
let res: Vec<&str> = ps[5].splitn(2, '/').collect();
|
||||
if res.len() != 2 {
|
||||
return Err(Error::msg("ARN resource invalid"));
|
||||
}
|
||||
|
||||
if res[0] != ARN_RESOURCE_TYPE_ROLE {
|
||||
return Err(Error::msg("ARN resource type invalid"));
|
||||
}
|
||||
|
||||
let valid_resource_id_regex = Regex::new(r"^[A-Za-z0-9_/\.-]+$").unwrap();
|
||||
if !valid_resource_id_regex.is_match(res[1]) {
|
||||
return Err(Error::msg("ARN resource ID invalid"));
|
||||
}
|
||||
|
||||
Ok(ARN {
|
||||
partition: ARN_PARTITION_RUSTFS.to_string(),
|
||||
service: ARN_SERVICE_IAM.to_string(),
|
||||
region: ps[3].to_string(),
|
||||
resource_type: ARN_RESOURCE_TYPE_ROLE.to_string(),
|
||||
resource_id: res[1].to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ARN {
|
||||
#[allow(clippy::write_literal)]
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}:{}:{}:{}:{}:{}/{}",
|
||||
ARN_PREFIX_ARN,
|
||||
self.partition,
|
||||
self.service,
|
||||
self.region,
|
||||
"", // account-id is always empty in this implementation
|
||||
self.resource_type,
|
||||
self.resource_id
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
mod credentials;
|
||||
|
||||
pub use credentials::Credentials;
|
||||
pub use credentials::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct UserIdentity {
|
||||
pub version: i64,
|
||||
pub credentials: Credentials,
|
||||
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 {
|
||||
fn from(value: Credentials) -> Self {
|
||||
UserIdentity {
|
||||
version: 1,
|
||||
credentials: value,
|
||||
update_at: Some(OffsetDateTime::now_utc()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,481 +0,0 @@
|
||||
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<Self, Self::Error> {
|
||||
// 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<Vec<BorrowedFormatItem<'static>>> =
|
||||
// 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<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 new(elem: &str) -> Result<Self> {
|
||||
// let header: CredentialHeader = elem.try_into()?;
|
||||
// Self::check_key_value(header)
|
||||
// }
|
||||
|
||||
// pub fn check_key_value(_header: CredentialHeader) -> Result<Self> {
|
||||
// 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).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(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<T: DeserializeOwned>(token: &str, secret: &str) -> Result<T> {
|
||||
let ms = extract_claims::<T>(token, secret)?;
|
||||
// TODO SessionPolicyName
|
||||
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) -> Result<Self, Self::Error> {
|
||||
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!()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
+5
-6
@@ -7,14 +7,13 @@ use std::{
|
||||
|
||||
use arc_swap::{ArcSwap, AsRaw, Guard};
|
||||
use log::warn;
|
||||
use policy::{
|
||||
auth::UserIdentity,
|
||||
policy::{Args, PolicyDoc},
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::{
|
||||
auth::UserIdentity,
|
||||
policy::PolicyDoc,
|
||||
store::{GroupInfo, MappedPolicy},
|
||||
sys::Args,
|
||||
};
|
||||
use crate::store::{GroupInfo, MappedPolicy};
|
||||
|
||||
pub struct Cache {
|
||||
pub policy_docs: ArcSwap<CacheEntity<PolicyDoc>>,
|
||||
|
||||
+26
-9
@@ -1,12 +1,14 @@
|
||||
use crate::policy;
|
||||
use ecstore::disk::error::clone_disk_err;
|
||||
use ecstore::disk::error::DiskError;
|
||||
use policy::policy::Error as PolicyError;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
PolicyError(#[from] policy::Error),
|
||||
PolicyError(#[from] PolicyError),
|
||||
|
||||
#[error("ecsotre error: {0}")]
|
||||
EcstoreError(ecstore::error::Error),
|
||||
EcstoreError(common::error::Error),
|
||||
|
||||
#[error("{0}")]
|
||||
StringError(String),
|
||||
@@ -96,7 +98,7 @@ pub enum Error {
|
||||
// matches!(e, Error::NoSuchUser(_))
|
||||
// }
|
||||
|
||||
pub fn is_err_no_such_policy(err: &ecstore::error::Error) -> bool {
|
||||
pub fn is_err_no_such_policy(err: &common::error::Error) -> bool {
|
||||
if let Some(e) = err.downcast_ref::<Error>() {
|
||||
matches!(e, Error::NoSuchPolicy)
|
||||
} else {
|
||||
@@ -104,7 +106,7 @@ pub fn is_err_no_such_policy(err: &ecstore::error::Error) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_err_no_such_user(err: &ecstore::error::Error) -> bool {
|
||||
pub fn is_err_no_such_user(err: &common::error::Error) -> bool {
|
||||
if let Some(e) = err.downcast_ref::<Error>() {
|
||||
matches!(e, Error::NoSuchUser(_))
|
||||
} else {
|
||||
@@ -112,7 +114,7 @@ pub fn is_err_no_such_user(err: &ecstore::error::Error) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_err_no_such_account(err: &ecstore::error::Error) -> bool {
|
||||
pub fn is_err_no_such_account(err: &common::error::Error) -> bool {
|
||||
if let Some(e) = err.downcast_ref::<Error>() {
|
||||
matches!(e, Error::NoSuchAccount(_))
|
||||
} else {
|
||||
@@ -120,7 +122,7 @@ pub fn is_err_no_such_account(err: &ecstore::error::Error) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_err_no_such_temp_account(err: &ecstore::error::Error) -> bool {
|
||||
pub fn is_err_no_such_temp_account(err: &common::error::Error) -> bool {
|
||||
if let Some(e) = err.downcast_ref::<Error>() {
|
||||
matches!(e, Error::NoSuchTempAccount(_))
|
||||
} else {
|
||||
@@ -128,7 +130,7 @@ pub fn is_err_no_such_temp_account(err: &ecstore::error::Error) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_err_no_such_group(err: &ecstore::error::Error) -> bool {
|
||||
pub fn is_err_no_such_group(err: &common::error::Error) -> bool {
|
||||
if let Some(e) = err.downcast_ref::<Error>() {
|
||||
matches!(e, Error::NoSuchGroup(_))
|
||||
} else {
|
||||
@@ -136,10 +138,25 @@ pub fn is_err_no_such_group(err: &ecstore::error::Error) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_err_no_such_service_account(err: &ecstore::error::Error) -> bool {
|
||||
pub fn is_err_no_such_service_account(err: &common::error::Error) -> bool {
|
||||
if let Some(e) = err.downcast_ref::<Error>() {
|
||||
matches!(e, Error::NoSuchServiceAccount(_))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clone_err(e: &common::error::Error) -> common::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() {
|
||||
common::error::Error::new(std::io::Error::from_raw_os_error(code))
|
||||
} else {
|
||||
common::error::Error::new(std::io::Error::new(e.kind(), e.to_string()))
|
||||
}
|
||||
} else {
|
||||
//TODO: 优化其他类型
|
||||
common::error::Error::msg(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Serialize, Default)]
|
||||
pub struct Format {
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
// impl Format {
|
||||
// pub const PATH: &str = "config/iam/config/format.json";
|
||||
// pub const DEFAULT_VERSION: i32 = 1;
|
||||
|
||||
// pub fn new() -> Self {
|
||||
// Self {
|
||||
// version: Self::DEFAULT_VERSION,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -1,154 +0,0 @@
|
||||
// use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
// use log::{info, warn};
|
||||
|
||||
// use crate::{
|
||||
// arn::ARN,
|
||||
// auth::UserIdentity,
|
||||
// cache::CacheInner,
|
||||
// policy::{utils::get_values_from_claims, Args, Policy},
|
||||
// store::Store,
|
||||
// Error,
|
||||
// };
|
||||
|
||||
// pub(crate) struct Handler<'m, T> {
|
||||
// cache: CacheInner,
|
||||
// api: &'m T,
|
||||
// roles: &'m HashMap<ARN, Vec<String>>,
|
||||
// }
|
||||
|
||||
// impl<'m, T> Handler<'m, T> {
|
||||
// pub fn new(cache: CacheInner, api: &'m T, roles: &'m HashMap<ARN, Vec<String>>) -> Self {
|
||||
// Self { cache, api, roles }
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl<'m, T> Handler<'m, T>
|
||||
// where
|
||||
// T: Store,
|
||||
// {
|
||||
// #[inline]
|
||||
// fn get_user<'a>(&self, user_name: &'a str) -> Option<&UserIdentity> {
|
||||
// self.cache
|
||||
// .users
|
||||
// .get(user_name)
|
||||
// .or_else(|| self.cache.sts_accounts.get(user_name))
|
||||
// }
|
||||
|
||||
// async fn get_policy(&self, name: &str, _groups: &[String]) -> crate::Result<Vec<String>> {
|
||||
// if name.is_empty() {
|
||||
// return Err(Error::InvalidArgument);
|
||||
// }
|
||||
|
||||
// todo!()
|
||||
// // self.api.policy_db_get(name, groups)
|
||||
// }
|
||||
|
||||
// /// 如果是临时用户,返回Ok(Some(partent_name)))
|
||||
// /// 如果不是临时用户,返回Ok(None)
|
||||
// fn is_temp_user<'a>(&self, user_name: &'a 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<'a>(&self, user_name: &'a 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 async fn is_allowed_service_account<'a>(&self, args: &Args<'a>, parent: &str) -> bool {
|
||||
// let Some(p) = args.claims.get(parent) else {
|
||||
// return false;
|
||||
// };
|
||||
|
||||
// if let Some(parent_in_chaim) = p.as_str() {
|
||||
// if parent_in_chaim != parent {
|
||||
// return false;
|
||||
// }
|
||||
// } else {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// let is_owner_derived = parent == "rustfsadmin"; // todo ,使用全局变量
|
||||
// let role_arn = args.get_role_arn();
|
||||
// let mut svc_policies = None;
|
||||
|
||||
// if is_owner_derived {
|
||||
// } else if let Some(x) = role_arn {
|
||||
// let Ok(arn) = x.parse::<ARN>() else {
|
||||
// info!("error parsing role ARN {x}");
|
||||
// return false;
|
||||
// };
|
||||
|
||||
// svc_policies = self.roles.get(&arn).map(|x| Cow::from(x));
|
||||
// } else {
|
||||
// let Ok(mut p) = self.get_policy(parent, &args.groups[..]).await else { return false };
|
||||
// if p.is_empty() {
|
||||
// // todo iamPolicyClaimNameOpenID
|
||||
// let (p1, _) = get_values_from_claims(&args.claims, "");
|
||||
// p = p1;
|
||||
// }
|
||||
// svc_policies = Some(Cow::Owned(p));
|
||||
// }
|
||||
|
||||
// if is_owner_derived && svc_policies.as_ref().map(|x| x.as_ref().len()).unwrap_or_default() == 0 {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// false
|
||||
// }
|
||||
|
||||
// pub async fn get_combined_policy(&self, _policies: &[String]) -> Policy {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// pub async fn is_allowed<'a>(&self, args: Args<'a>) -> bool {
|
||||
// if args.is_owner {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// match self.is_temp_user(&args.account) {
|
||||
// Ok(Some(parent)) => return self.is_allowed_sts(&args, parent),
|
||||
// Err(_) => return false,
|
||||
// _ => {}
|
||||
// }
|
||||
|
||||
// match self.is_service_account(&args.account) {
|
||||
// Ok(Some(parent)) => return self.is_allowed_service_account(&args, parent).await,
|
||||
// Err(_) => return false,
|
||||
// _ => {}
|
||||
// }
|
||||
|
||||
// let Ok(policies) = self.get_policy(&args.account, &args.groups).await else { return false };
|
||||
|
||||
// if policies.is_empty() {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// let policy = self.get_combined_policy(&policies[..]).await;
|
||||
// policy.is_allowed(&args)
|
||||
// }
|
||||
// }
|
||||
+2
-9
@@ -1,23 +1,16 @@
|
||||
use auth::Credentials;
|
||||
use ecstore::error::{Error, Result};
|
||||
use common::error::{Error, Result};
|
||||
use ecstore::store::ECStore;
|
||||
use error::Error as IamError;
|
||||
use manager::IamCache;
|
||||
use policy::auth::Credentials;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use store::object::ObjectStore;
|
||||
use sys::IamSys;
|
||||
use tracing::{debug, instrument};
|
||||
|
||||
pub mod cache;
|
||||
mod format;
|
||||
mod handler;
|
||||
|
||||
pub mod arn;
|
||||
pub mod auth;
|
||||
pub mod error;
|
||||
pub mod manager;
|
||||
pub mod policy;
|
||||
pub mod service_type;
|
||||
pub mod store;
|
||||
pub mod utils;
|
||||
|
||||
|
||||
+12
-11
@@ -1,24 +1,26 @@
|
||||
use crate::{
|
||||
arn::ARN,
|
||||
auth::{self, get_claims_from_token_with_secret, is_secret_key_valid, jwt_sign, Credentials, UserIdentity},
|
||||
cache::{Cache, CacheEntity},
|
||||
error::{is_err_no_such_group, is_err_no_such_policy, is_err_no_such_user, Error as IamError},
|
||||
format::Format,
|
||||
get_global_action_cred,
|
||||
policy::{Policy, PolicyDoc, DEFAULT_POLICIES},
|
||||
store::{object::IAM_CONFIG_PREFIX, GroupInfo, MappedPolicy, Store, UserType},
|
||||
sys::{
|
||||
iam_policy_claim_name_sa, UpdateServiceAccountOpts, EMBEDDED_POLICY_TYPE, INHERITED_POLICY_TYPE,
|
||||
MAX_SVCSESSION_POLICY_SIZE, SESSION_POLICY_NAME, SESSION_POLICY_NAME_EXTRACTED, STATUS_DISABLED, STATUS_ENABLED,
|
||||
UpdateServiceAccountOpts, MAX_SVCSESSION_POLICY_SIZE, SESSION_POLICY_NAME, SESSION_POLICY_NAME_EXTRACTED,
|
||||
STATUS_DISABLED, STATUS_ENABLED,
|
||||
},
|
||||
};
|
||||
use common::error::{Error, Result};
|
||||
use ecstore::config::error::is_err_config_not_found;
|
||||
use ecstore::utils::{crypto::base64_encode, path::path_join_buf};
|
||||
use ecstore::{
|
||||
config::error::is_err_config_not_found,
|
||||
error::{Error, Result},
|
||||
};
|
||||
use log::{debug, warn};
|
||||
use madmin::{AccountStatus, AddOrUpdateUserReq, GroupDesc};
|
||||
use policy::{
|
||||
arn::ARN,
|
||||
auth::{self, get_claims_from_token_with_secret, is_secret_key_valid, jwt_sign, Credentials, UserIdentity},
|
||||
format::Format,
|
||||
policy::{
|
||||
default::DEFAULT_POLICIES, iam_policy_claim_name_sa, Policy, PolicyDoc, EMBEDDED_POLICY_TYPE, INHERITED_POLICY_TYPE,
|
||||
},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
@@ -488,7 +490,6 @@ where
|
||||
if !is_secret_key_valid(&secret) {
|
||||
return Err(IamError::InvalidSecretKeyLength.into());
|
||||
}
|
||||
|
||||
cr.secret_key = secret;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
pub mod action;
|
||||
mod doc;
|
||||
mod effect;
|
||||
mod function;
|
||||
mod id;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod policy;
|
||||
pub mod resource;
|
||||
pub mod statement;
|
||||
pub(crate) mod utils;
|
||||
|
||||
pub use action::ActionSet;
|
||||
pub use doc::PolicyDoc;
|
||||
|
||||
pub use effect::Effect;
|
||||
pub use function::Functions;
|
||||
pub use id::ID;
|
||||
pub use policy::{default::DEFAULT_POLICIES, Policy};
|
||||
pub use resource::ResourceSet;
|
||||
|
||||
pub use statement::Statement;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
||||
pub enum Error {
|
||||
#[error("invalid Version '{0}'")]
|
||||
InvalidVersion(String),
|
||||
|
||||
#[error("invalid Effect '{0}'")]
|
||||
InvalidEffect(String),
|
||||
|
||||
#[error("both 'Action' and 'NotAction' are empty")]
|
||||
NonAction,
|
||||
|
||||
#[error("'Resource' is empty")]
|
||||
NonResource,
|
||||
|
||||
#[error("invalid key name: '{0}'")]
|
||||
InvalidKeyName(String),
|
||||
|
||||
#[error("invalid key: '{0}'")]
|
||||
InvalidKey(String),
|
||||
|
||||
#[error("invalid action: '{0}'")]
|
||||
InvalidAction(String),
|
||||
|
||||
#[error("invalid resource, type: '{0}', pattern: '{1}'")]
|
||||
InvalidResource(String, String),
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
use ecstore::error::{Error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashSet, ops::Deref};
|
||||
use strum::{EnumString, IntoStaticStr};
|
||||
|
||||
use crate::sys::Validator;
|
||||
|
||||
use super::{utils::wildcard, Error as IamError};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
|
||||
pub struct ActionSet(pub HashSet<Action>);
|
||||
|
||||
impl ActionSet {
|
||||
pub fn is_match(&self, action: &Action) -> bool {
|
||||
for act in self.0.iter() {
|
||||
if act.is_match(action) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if matches!(act, Action::S3Action(S3Action::GetObjectVersionAction))
|
||||
&& matches!(action, Action::S3Action(S3Action::GetObjectAction))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ActionSet {
|
||||
type Target = HashSet<Action>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Validator for ActionSet {
|
||||
type Error = Error;
|
||||
fn is_valid(&self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ActionSet {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.len() == other.len() && self.0.iter().all(|x| other.0.contains(x))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, Debug)]
|
||||
#[serde(try_from = "&str", untagged)]
|
||||
pub enum Action {
|
||||
S3Action(S3Action),
|
||||
AdminAction(AdminAction),
|
||||
StsAction(StsAction),
|
||||
KmsAction(KmsAction),
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub fn is_match(&self, action: &Action) -> bool {
|
||||
wildcard::is_match::<&str, &str>(self.into(), action.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Action> for &str {
|
||||
fn from(value: &Action) -> &'static str {
|
||||
match value {
|
||||
Action::S3Action(s) => s.into(),
|
||||
Action::AdminAction(s) => s.into(),
|
||||
Action::StsAction(s) => s.into(),
|
||||
Action::KmsAction(s) => s.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Action {
|
||||
const S3_PREFIX: &'static str = "s3:";
|
||||
const ADMIN_PREFIX: &'static str = "admin:";
|
||||
const STS_PREFIX: &'static str = "sts:";
|
||||
const KMS_PREFIX: &'static str = "kms:";
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Action {
|
||||
type Error = Error;
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
if value.starts_with(Self::S3_PREFIX) {
|
||||
Ok(Self::S3Action(
|
||||
S3Action::try_from(value).map_err(|_| IamError::InvalidAction(value.into()))?,
|
||||
))
|
||||
} else if value.starts_with(Self::ADMIN_PREFIX) {
|
||||
Ok(Self::AdminAction(
|
||||
AdminAction::try_from(value).map_err(|_| IamError::InvalidAction(value.into()))?,
|
||||
))
|
||||
} else if value.starts_with(Self::STS_PREFIX) {
|
||||
Ok(Self::StsAction(
|
||||
StsAction::try_from(value).map_err(|_| IamError::InvalidAction(value.into()))?,
|
||||
))
|
||||
} else if value.starts_with(Self::KMS_PREFIX) {
|
||||
Ok(Self::KmsAction(
|
||||
KmsAction::try_from(value).map_err(|_| IamError::InvalidAction(value.into()))?,
|
||||
))
|
||||
} else {
|
||||
Err(IamError::InvalidAction(value.into()).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, EnumString, IntoStaticStr, Debug)]
|
||||
#[cfg_attr(test, derive(Default))]
|
||||
#[serde(try_from = "&str", into = "&str")]
|
||||
pub enum S3Action {
|
||||
#[cfg_attr(test, default)]
|
||||
#[strum(serialize = "s3:*")]
|
||||
AllActions,
|
||||
#[strum(serialize = "s3:AbortMultipartUpload")]
|
||||
AbortMultipartUploadAction,
|
||||
#[strum(serialize = "s3:CreateBucket")]
|
||||
CreateBucketAction,
|
||||
#[strum(serialize = "s3:DeleteBucket")]
|
||||
DeleteBucketAction,
|
||||
#[strum(serialize = "s3:ForceDeleteBucket")]
|
||||
ForceDeleteBucketAction,
|
||||
#[strum(serialize = "s3:DeleteBucketPolicy")]
|
||||
DeleteBucketPolicyAction,
|
||||
#[strum(serialize = "s3:DeleteBucketCors")]
|
||||
DeleteBucketCorsAction,
|
||||
#[strum(serialize = "s3:DeleteObject")]
|
||||
DeleteObjectAction,
|
||||
#[strum(serialize = "s3:GetBucketLocation")]
|
||||
GetBucketLocationAction,
|
||||
#[strum(serialize = "s3:GetBucketNotification")]
|
||||
GetBucketNotificationAction,
|
||||
#[strum(serialize = "s3:GetBucketPolicy")]
|
||||
GetBucketPolicyAction,
|
||||
#[strum(serialize = "s3:GetBucketCors")]
|
||||
GetBucketCorsAction,
|
||||
#[strum(serialize = "s3:GetObject")]
|
||||
GetObjectAction,
|
||||
#[strum(serialize = "s3:GetObjectAttributes")]
|
||||
GetObjectAttributesAction,
|
||||
#[strum(serialize = "s3:HeadBucket")]
|
||||
HeadBucketAction,
|
||||
#[strum(serialize = "s3:ListAllMyBuckets")]
|
||||
ListAllMyBucketsAction,
|
||||
#[strum(serialize = "s3:ListBucket")]
|
||||
ListBucketAction,
|
||||
#[strum(serialize = "s3:GetBucketPolicyStatus")]
|
||||
GetBucketPolicyStatusAction,
|
||||
#[strum(serialize = "s3:ListBucketVersions")]
|
||||
ListBucketVersionsAction,
|
||||
#[strum(serialize = "s3:ListBucketMultipartUploads")]
|
||||
ListBucketMultipartUploadsAction,
|
||||
#[strum(serialize = "s3:ListenNotification")]
|
||||
ListenNotificationAction,
|
||||
#[strum(serialize = "s3:ListenBucketNotification")]
|
||||
ListenBucketNotificationAction,
|
||||
#[strum(serialize = "s3:ListMultipartUploadParts")]
|
||||
ListMultipartUploadPartsAction,
|
||||
#[strum(serialize = "s3:PutBucketLifecycle")]
|
||||
PutBucketLifecycleAction,
|
||||
#[strum(serialize = "s3:GetBucketLifecycle")]
|
||||
GetBucketLifecycleAction,
|
||||
#[strum(serialize = "s3:PutBucketNotification")]
|
||||
PutBucketNotificationAction,
|
||||
#[strum(serialize = "s3:PutBucketPolicy")]
|
||||
PutBucketPolicyAction,
|
||||
#[strum(serialize = "s3:PutBucketCors")]
|
||||
PutBucketCorsAction,
|
||||
#[strum(serialize = "s3:PutObject")]
|
||||
PutObjectAction,
|
||||
#[strum(serialize = "s3:DeleteObjectVersion")]
|
||||
DeleteObjectVersionAction,
|
||||
#[strum(serialize = "s3:DeleteObjectVersionTagging")]
|
||||
DeleteObjectVersionTaggingAction,
|
||||
#[strum(serialize = "s3:GetObjectVersion")]
|
||||
GetObjectVersionAction,
|
||||
#[strum(serialize = "s3:GetObjectVersionAttributes")]
|
||||
GetObjectVersionAttributesAction,
|
||||
#[strum(serialize = "s3:GetObjectVersionTagging")]
|
||||
GetObjectVersionTaggingAction,
|
||||
#[strum(serialize = "s3:PutObjectVersionTagging")]
|
||||
PutObjectVersionTaggingAction,
|
||||
#[strum(serialize = "s3:BypassGovernanceRetention")]
|
||||
BypassGovernanceRetentionAction,
|
||||
#[strum(serialize = "s3:PutObjectRetention")]
|
||||
PutObjectRetentionAction,
|
||||
#[strum(serialize = "s3:GetObjectRetention")]
|
||||
GetObjectRetentionAction,
|
||||
#[strum(serialize = "s3:GetObjectLegalHold")]
|
||||
GetObjectLegalHoldAction,
|
||||
#[strum(serialize = "s3:PutObjectLegalHold")]
|
||||
PutObjectLegalHoldAction,
|
||||
#[strum(serialize = "s3:GetBucketObjectLockConfiguration")]
|
||||
GetBucketObjectLockConfigurationAction,
|
||||
#[strum(serialize = "s3:PutBucketObjectLockConfiguration")]
|
||||
PutBucketObjectLockConfigurationAction,
|
||||
#[strum(serialize = "s3:GetBucketTagging")]
|
||||
GetBucketTaggingAction,
|
||||
#[strum(serialize = "s3:PutBucketTagging")]
|
||||
PutBucketTaggingAction,
|
||||
#[strum(serialize = "s3:GetObjectTagging")]
|
||||
GetObjectTaggingAction,
|
||||
#[strum(serialize = "s3:PutObjectTagging")]
|
||||
PutObjectTaggingAction,
|
||||
#[strum(serialize = "s3:DeleteObjectTagging")]
|
||||
DeleteObjectTaggingAction,
|
||||
#[strum(serialize = "s3:PutBucketEncryption")]
|
||||
PutBucketEncryptionAction,
|
||||
#[strum(serialize = "s3:GetBucketEncryption")]
|
||||
GetBucketEncryptionAction,
|
||||
#[strum(serialize = "s3:PutBucketVersioning")]
|
||||
PutBucketVersioningAction,
|
||||
#[strum(serialize = "s3:GetBucketVersioning")]
|
||||
GetBucketVersioningAction,
|
||||
#[strum(serialize = "s3:GetReplicationConfiguration")]
|
||||
GetReplicationConfigurationAction,
|
||||
#[strum(serialize = "s3:PutReplicationConfiguration")]
|
||||
PutReplicationConfigurationAction,
|
||||
#[strum(serialize = "s3:ReplicateObject")]
|
||||
ReplicateObjectAction,
|
||||
#[strum(serialize = "s3:ReplicateDelete")]
|
||||
ReplicateDeleteAction,
|
||||
#[strum(serialize = "s3:ReplicateTags")]
|
||||
ReplicateTagsAction,
|
||||
#[strum(serialize = "s3:GetObjectVersionForReplication")]
|
||||
GetObjectVersionForReplicationAction,
|
||||
#[strum(serialize = "s3:RestoreObject")]
|
||||
RestoreObjectAction,
|
||||
#[strum(serialize = "s3:ResetBucketReplicationState")]
|
||||
ResetBucketReplicationStateAction,
|
||||
#[strum(serialize = "s3:PutObjectFanOut")]
|
||||
PutObjectFanOutAction,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, EnumString, IntoStaticStr, Debug)]
|
||||
#[serde(try_from = "&str", into = "&str")]
|
||||
pub enum AdminAction {
|
||||
#[strum(serialize = "admin:*")]
|
||||
AllActions,
|
||||
#[strum(serialize = "admin:Profiling")]
|
||||
ProfilingAdminAction,
|
||||
#[strum(serialize = "admin:ServerTrace")]
|
||||
TraceAdminAction,
|
||||
#[strum(serialize = "admin:ConsoleLog")]
|
||||
ConsoleLogAdminAction,
|
||||
#[strum(serialize = "admin:ServerInfo")]
|
||||
ServerInfoAdminAction,
|
||||
#[strum(serialize = "admin:OBDInfo")]
|
||||
HealthInfoAdminAction,
|
||||
#[strum(serialize = "admin:TopLocksInfo")]
|
||||
TopLocksAdminAction,
|
||||
#[strum(serialize = "admin:LicenseInfo")]
|
||||
LicenseInfoAdminAction,
|
||||
#[strum(serialize = "admin:BandwidthMonitor")]
|
||||
BandwidthMonitorAction,
|
||||
#[strum(serialize = "admin:InspectData")]
|
||||
InspectDataAction,
|
||||
#[strum(serialize = "admin:Prometheus")]
|
||||
PrometheusAdminAction,
|
||||
#[strum(serialize = "admin:ListServiceAccounts")]
|
||||
ListServiceAccountsAdminAction,
|
||||
#[strum(serialize = "admin:CreateServiceAccount")]
|
||||
CreateServiceAccountAdminAction,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, EnumString, IntoStaticStr, Debug)]
|
||||
#[serde(try_from = "&str", into = "&str")]
|
||||
pub enum StsAction {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, EnumString, IntoStaticStr, Debug)]
|
||||
#[serde(try_from = "&str", into = "&str")]
|
||||
pub enum KmsAction {
|
||||
#[strum(serialize = "kms:*")]
|
||||
AllActions,
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use super::Policy;
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Clone)]
|
||||
pub struct PolicyDoc {
|
||||
pub version: i64,
|
||||
pub policy: Policy,
|
||||
pub create_date: Option<OffsetDateTime>,
|
||||
pub update_date: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
impl PolicyDoc {
|
||||
pub fn new(policy: Policy) -> Self {
|
||||
Self {
|
||||
version: 1,
|
||||
policy,
|
||||
create_date: Some(OffsetDateTime::now_utc()),
|
||||
update_date: Some(OffsetDateTime::now_utc()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, policy: Policy) {
|
||||
self.version += 1;
|
||||
self.policy = policy;
|
||||
self.update_date = Some(OffsetDateTime::now_utc());
|
||||
|
||||
if self.create_date.is_none() {
|
||||
self.create_date = self.update_date;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_policy(policy: Policy) -> Self {
|
||||
Self {
|
||||
version: 1,
|
||||
policy,
|
||||
create_date: None,
|
||||
update_date: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for PolicyDoc {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
match serde_json::from_slice::<PolicyDoc>(&value) {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => match serde_json::from_slice::<Policy>(&value) {
|
||||
Ok(res2) => Ok(Self {
|
||||
policy: res2,
|
||||
..Default::default()
|
||||
}),
|
||||
Err(_) => Err(err),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
use ecstore::error::{Error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{EnumString, IntoStaticStr};
|
||||
|
||||
use crate::sys::Validator;
|
||||
|
||||
#[derive(Serialize, Clone, Deserialize, EnumString, IntoStaticStr, Default, Debug, PartialEq)]
|
||||
#[serde(try_from = "&str", into = "&str")]
|
||||
pub enum Effect {
|
||||
#[default]
|
||||
#[strum(serialize = "Allow")]
|
||||
Allow,
|
||||
#[strum(serialize = "Deny")]
|
||||
Deny,
|
||||
}
|
||||
|
||||
impl Effect {
|
||||
pub fn is_allowed(&self, allowed: bool) -> bool {
|
||||
if matches!(self, Self::Allow) {
|
||||
return allowed;
|
||||
}
|
||||
|
||||
!allowed
|
||||
}
|
||||
}
|
||||
|
||||
impl Validator for Effect {
|
||||
type Error = Error;
|
||||
fn is_valid(&self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,393 +0,0 @@
|
||||
use crate::policy::function::condition::Condition;
|
||||
use serde::ser::SerializeMap;
|
||||
use serde::{de, Deserialize, Serialize, Serializer};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub mod addr;
|
||||
pub mod binary;
|
||||
pub mod bool_null;
|
||||
pub mod condition;
|
||||
pub mod date;
|
||||
pub mod func;
|
||||
pub mod key;
|
||||
pub mod key_name;
|
||||
pub mod number;
|
||||
pub mod string;
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct Functions {
|
||||
for_any_value: Vec<Condition>,
|
||||
for_all_values: Vec<Condition>,
|
||||
for_normal: Vec<Condition>,
|
||||
}
|
||||
|
||||
impl Functions {
|
||||
pub fn evaluate(&self, values: &HashMap<String, Vec<String>>) -> bool {
|
||||
for c in self.for_any_value.iter() {
|
||||
if !c.evaluate(false, values) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for c in self.for_all_values.iter() {
|
||||
if !c.evaluate(true, values) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for c in self.for_normal.iter() {
|
||||
if !c.evaluate(false, values) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.for_all_values.is_empty() && self.for_any_value.is_empty() && self.for_normal.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Functions {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut se =
|
||||
serializer.serialize_map(Some(self.for_any_value.len() + self.for_all_values.len() + self.for_normal.len()))?;
|
||||
|
||||
for conditions in self.for_all_values.iter() {
|
||||
se.serialize_key(format!("ForAllValues:{}", conditions.to_key()).as_str())?;
|
||||
conditions.serialize_map(&mut se)?;
|
||||
}
|
||||
|
||||
for conditions in self.for_any_value.iter() {
|
||||
se.serialize_key(format!("ForAnyValue:{}", conditions.to_key()).as_str())?;
|
||||
conditions.serialize_map(&mut se)?;
|
||||
}
|
||||
|
||||
for conditions in self.for_normal.iter() {
|
||||
se.serialize_key(conditions.to_key())?;
|
||||
conditions.serialize_map(&mut se)?;
|
||||
}
|
||||
|
||||
se.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Functions {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct FuncVisitor;
|
||||
use serde::de::Visitor;
|
||||
|
||||
impl<'de> Visitor<'de> for FuncVisitor {
|
||||
type Value = Functions;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("Functions")
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: de::MapAccess<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
|
||||
let mut hash = HashSet::with_capacity(map.size_hint().unwrap_or_default());
|
||||
|
||||
let mut inner_data = Functions::default();
|
||||
while let Some(key) = map.next_key::<&str>()? {
|
||||
if hash.contains(&key) {
|
||||
return Err(Error::custom(format!("duplicate condition operator `{}`", key)));
|
||||
}
|
||||
|
||||
hash.insert(key);
|
||||
|
||||
let mut tokens = key.split(":");
|
||||
let mut qualifier = tokens.next();
|
||||
let mut name = tokens.next();
|
||||
if name.is_none() {
|
||||
name = qualifier;
|
||||
qualifier = None;
|
||||
}
|
||||
|
||||
if tokens.next().is_some() {
|
||||
return Err(Error::custom("invalid condition operator"));
|
||||
}
|
||||
|
||||
let Some(name) = name else { return Err(Error::custom("has no condition operator")) };
|
||||
|
||||
let condition = Condition::from_deserializer(name, &mut map)?;
|
||||
match qualifier {
|
||||
Some("ForAnyValue") => inner_data.for_any_value.push(condition),
|
||||
Some("ForAllValues") => inner_data.for_all_values.push(condition),
|
||||
Some(q) => return Err(Error::custom(format!("invalid qualifier `{q}`"))),
|
||||
None => inner_data.for_normal.push(condition),
|
||||
}
|
||||
}
|
||||
|
||||
/* if inner_data.is_empty() {
|
||||
return Err(Error::custom("has no condition element"));
|
||||
} */
|
||||
|
||||
Ok(inner_data)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_map(FuncVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Functions {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
if !(self.for_all_values.len() == other.for_all_values.len()
|
||||
&& self.for_any_value.len() == other.for_any_value.len()
|
||||
&& self.for_normal.len() == other.for_normal.len())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
self.for_any_value.iter().all(|x| other.for_any_value.contains(x))
|
||||
&& self.for_all_values.iter().all(|x| other.for_all_values.contains(x))
|
||||
&& self.for_normal.iter().all(|x| other.for_normal.contains(x))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Value;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::policy::function::condition::Condition::*;
|
||||
use crate::policy::function::func::FuncKeyValue;
|
||||
use crate::policy::function::key::Key;
|
||||
use crate::policy::function::string::StringFunc;
|
||||
use crate::policy::function::string::StringFuncValue;
|
||||
use crate::policy::Functions;
|
||||
use test_case::test_case;
|
||||
|
||||
#[test_case(
|
||||
r#"{
|
||||
"Null": {
|
||||
"s3:x-amz-server-side-encryption-customer-algorithm": true
|
||||
},
|
||||
"Null": {
|
||||
"s3:x-amz-server-side-encryption-customer-algorithm": "true"
|
||||
}
|
||||
}"# => false; "1")]
|
||||
#[test_case(r#"{}"# => true; "2")]
|
||||
#[test_case(
|
||||
r#"{
|
||||
"StringLike": {
|
||||
"s3:x-amz-metadata-directive": "REPL*"
|
||||
},
|
||||
"StringEquals": {
|
||||
"s3:x-amz-copy-source": "mybucket/myobject"
|
||||
},
|
||||
"StringNotEquals": {
|
||||
"s3:x-amz-server-side-encryption": "AES256"
|
||||
},
|
||||
"NotIpAddress": {
|
||||
"aws:SourceIp": [
|
||||
"10.1.10.0/24",
|
||||
"10.10.1.0/24"
|
||||
]
|
||||
},
|
||||
"StringNotLike": {
|
||||
"s3:x-amz-storage-class": "STANDARD",
|
||||
"s3:x-amz-server-side-encryption": "AES256"
|
||||
},
|
||||
"Null": {
|
||||
"s3:x-amz-server-side-encryption-customer-algorithm": true
|
||||
},
|
||||
"IpAddress": {
|
||||
"aws:SourceIp": [
|
||||
"192.168.1.0/24",
|
||||
"192.168.2.0/24"
|
||||
]
|
||||
}
|
||||
}"# => true; "3"
|
||||
)]
|
||||
#[test_case(
|
||||
r#"{
|
||||
"StringLike": {
|
||||
"s3:x-amz-metadata-directive": "REPL*"
|
||||
},
|
||||
"StringEquals": {
|
||||
"s3:x-amz-copy-source": "mybucket/myobject",
|
||||
"s3:prefix": [
|
||||
"",
|
||||
"home/"
|
||||
],
|
||||
"s3:delimiter": [
|
||||
"/"
|
||||
]
|
||||
},
|
||||
"StringNotEquals": {
|
||||
"s3:x-amz-server-side-encryption": "AES256"
|
||||
},
|
||||
"NotIpAddress": {
|
||||
"aws:SourceIp": [
|
||||
"10.1.10.0/24",
|
||||
"10.10.1.0/24"
|
||||
]
|
||||
},
|
||||
"StringNotLike": {
|
||||
"s3:x-amz-storage-class": "STANDARD"
|
||||
},
|
||||
"Null": {
|
||||
"s3:x-amz-server-side-encryption-customer-algorithm": true
|
||||
},
|
||||
"IpAddress": {
|
||||
"aws:SourceIp": [
|
||||
"192.168.1.0/24",
|
||||
"192.168.2.0/24"
|
||||
]
|
||||
}
|
||||
}"# => true; "4"
|
||||
)]
|
||||
#[test_case(
|
||||
r#"{
|
||||
"IpAddress": {
|
||||
"aws:SourceIp": [
|
||||
"192.168.1.0/24"
|
||||
]
|
||||
},
|
||||
"NotIpAddress": {
|
||||
"aws:SourceIp": [
|
||||
"10.1.10.0/24"
|
||||
]
|
||||
},
|
||||
"Null": {
|
||||
"s3:x-amz-server-side-encryption-customer-algorithm": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"StringEquals": {
|
||||
"s3:x-amz-copy-source": [
|
||||
"mybucket/myobject"
|
||||
]
|
||||
},
|
||||
"StringLike": {
|
||||
"s3:x-amz-metadata-directive": [
|
||||
"REPL*"
|
||||
]
|
||||
},
|
||||
"StringNotEquals": {
|
||||
"s3:x-amz-server-side-encryption": [
|
||||
"AES256"
|
||||
]
|
||||
},
|
||||
"StringNotLike": {
|
||||
"s3:x-amz-storage-class": [
|
||||
"STANDARD"
|
||||
]
|
||||
}
|
||||
}"# => true;
|
||||
"5"
|
||||
)]
|
||||
#[test_case(
|
||||
r#"{
|
||||
"IpAddress": {
|
||||
"aws:SourceIp": [
|
||||
"192.168.1.0/24"
|
||||
]
|
||||
},
|
||||
"NotIpAddress": {
|
||||
"aws:SourceIp": [
|
||||
"10.1.10.0/24"
|
||||
]
|
||||
},
|
||||
"Null": {
|
||||
"s3:x-amz-server-side-encryption-customer-algorithm": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"StringEquals": {
|
||||
"s3:x-amz-copy-source": [
|
||||
"mybucket/myobject"
|
||||
]
|
||||
},
|
||||
"StringLike": {
|
||||
"s3:x-amz-metadata-directive": [
|
||||
"REPL*"
|
||||
]
|
||||
},
|
||||
"StringNotEquals": {
|
||||
"s3:x-amz-server-side-encryption": [
|
||||
"aws:kms"
|
||||
]
|
||||
},
|
||||
"StringNotLike": {
|
||||
"s3:x-amz-storage-class": [
|
||||
"STANDARD"
|
||||
]
|
||||
}
|
||||
}"# => true;
|
||||
"6"
|
||||
)]
|
||||
fn test_de(input: &str) -> bool {
|
||||
serde_json::from_str::<Functions>(input)
|
||||
.map_err(|e| eprintln!("{e:?}"))
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
#[test_case(
|
||||
Functions {
|
||||
for_normal: vec![StringNotLike(StringFunc {
|
||||
0: vec![FuncKeyValue {
|
||||
key: Key::try_from("s3:LocationConstraint").unwrap(),
|
||||
values: StringFuncValue(vec!["us-east-1"].into_iter().map(ToOwned::to_owned).collect()),
|
||||
}],
|
||||
})],
|
||||
..Default::default()
|
||||
},
|
||||
r#"{"StringNotLike":{"s3:LocationConstraint":"us-east-1"}}"#;
|
||||
"1"
|
||||
)]
|
||||
#[test_case(
|
||||
Functions {
|
||||
for_all_values: vec![StringNotLike(StringFunc {
|
||||
0: vec![FuncKeyValue {
|
||||
key: Key::try_from("s3:LocationConstraint").unwrap(),
|
||||
values: StringFuncValue(vec!["us-east-1"].into_iter().map(ToOwned::to_owned).collect()),
|
||||
}],
|
||||
})],
|
||||
..Default::default()
|
||||
},
|
||||
r#"{"ForAllValues:StringNotLike":{"s3:LocationConstraint":"us-east-1"}}"#;
|
||||
"2"
|
||||
)]
|
||||
#[test_case(
|
||||
Functions {
|
||||
for_any_value: vec![StringNotLike(StringFunc {
|
||||
0: vec![FuncKeyValue {
|
||||
key: Key::try_from("s3:LocationConstraint").unwrap(),
|
||||
values: StringFuncValue(vec!["us-east-1", "us-east-2"].into_iter().map(ToOwned::to_owned).collect()),
|
||||
}],
|
||||
})],
|
||||
for_all_values: vec![StringNotLike(StringFunc {
|
||||
0: vec![FuncKeyValue {
|
||||
key: Key::try_from("s3:LocationConstraint").unwrap(),
|
||||
values: StringFuncValue(vec!["us-east-1"].into_iter().map(ToOwned::to_owned).collect()),
|
||||
}],
|
||||
})],
|
||||
for_normal: vec![StringNotLike(StringFunc {
|
||||
0: vec![FuncKeyValue {
|
||||
key: Key::try_from("s3:LocationConstraint").unwrap(),
|
||||
values: StringFuncValue(vec!["us-east-1"].into_iter().map(ToOwned::to_owned).collect()),
|
||||
}],
|
||||
})],
|
||||
},
|
||||
r#"{"ForAllValues:StringNotLike":{"s3:LocationConstraint":"us-east-1"},"ForAnyValue:StringNotLike":{"s3:LocationConstraint":["us-east-1","us-east-2"]},"StringNotLike":{"s3:LocationConstraint":"us-east-1"}}"#;
|
||||
"3"
|
||||
)]
|
||||
fn test_ser(input: Functions, expect: &str) {
|
||||
assert_eq!(serde_json::to_string(&input).unwrap(), expect);
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
use super::func::InnerFunc;
|
||||
use ipnetwork::IpNetwork;
|
||||
use serde::{de::Visitor, Deserialize, Serialize};
|
||||
use std::{borrow::Cow, collections::HashMap, net::IpAddr};
|
||||
|
||||
pub type AddrFunc = InnerFunc<AddrFuncValue>;
|
||||
|
||||
impl AddrFunc {
|
||||
pub(crate) fn evaluate(&self, values: &HashMap<String, Vec<String>>) -> bool {
|
||||
for inner in self.0.iter() {
|
||||
let rvalues = values.get(inner.key.name().as_str()).map(|t| t.iter()).unwrap_or_default();
|
||||
|
||||
for r in rvalues {
|
||||
let Ok(ip) = r.parse::<IpAddr>() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
for ip_net in inner.values.0.iter() {
|
||||
if ip_net.contains(ip) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, PartialEq, Eq, Debug)]
|
||||
#[serde(transparent)]
|
||||
pub struct AddrFuncValue(Vec<IpNetwork>);
|
||||
|
||||
impl<'de> Deserialize<'de> for AddrFuncValue {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct AddrFuncValueVisitor;
|
||||
impl<'d> Visitor<'d> for AddrFuncValueVisitor {
|
||||
type Value = AddrFuncValue;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("cidr string")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(AddrFuncValue(vec![Self::cidr::<E>(v)?]))
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::SeqAccess<'d>,
|
||||
{
|
||||
Ok(AddrFuncValue({
|
||||
let mut data = Vec::with_capacity(seq.size_hint().unwrap_or_default());
|
||||
while let Some(v) = seq.next_element::<&str>()? {
|
||||
data.push(Self::cidr::<A::Error>(v)?)
|
||||
}
|
||||
data
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl AddrFuncValueVisitor {
|
||||
fn cidr<E: serde::de::Error>(v: &str) -> Result<IpNetwork, E> {
|
||||
let mut cidr_str = Cow::from(v);
|
||||
if v.find('/').is_none() {
|
||||
cidr_str.to_mut().push_str("/32");
|
||||
}
|
||||
|
||||
cidr_str
|
||||
.parse::<IpNetwork>()
|
||||
.map_err(|_| E::custom(format!("{v} can not be parsed to CIDR")))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(AddrFuncValueVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{AddrFunc, AddrFuncValue};
|
||||
use crate::policy::function::func::FuncKeyValue;
|
||||
use crate::policy::function::{
|
||||
key::Key,
|
||||
key_name::AwsKeyName::*,
|
||||
key_name::KeyName::{self, *},
|
||||
};
|
||||
use test_case::test_case;
|
||||
|
||||
fn new_func(name: KeyName, variable: Option<String>, value: Vec<&str>) -> AddrFunc {
|
||||
AddrFunc {
|
||||
0: vec![FuncKeyValue {
|
||||
key: Key { name, variable },
|
||||
values: AddrFuncValue(value.into_iter().filter_map(|x| x.parse().ok()).collect()),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
#[test_case(r#"{"aws:SourceIp": "203.0.113.0/24"}"#, new_func(Aws(AWSSourceIP), None, vec!["203.0.113.0/24"]); "1")]
|
||||
#[test_case(r#"{"aws:SourceIp": "203.0.113.0"}"#, new_func(Aws(AWSSourceIP), None, vec!["203.0.113.0/32"]); "2")]
|
||||
#[test_case(r#"{"aws:SourceIp": "2001:DB8:1234:5678::/64"}"#, new_func(Aws(AWSSourceIP),None, vec!["2001:DB8:1234:5678::/64"]); "3")]
|
||||
#[test_case(r#"{"aws:SourceIp": "2001:DB8:1234:5678::"}"#, new_func(Aws(AWSSourceIP), None, vec!["2001:DB8:1234:5678::/32"]); "4")]
|
||||
#[test_case(r#"{"aws:SourceIp": ["203.0.113.0/24","203.0.113.0"]}"#, new_func(Aws(AWSSourceIP), None, vec!["203.0.113.0/24", "203.0.113.0/32"]); "5")]
|
||||
#[test_case(r#"{"aws:SourceIp": ["2001:DB8:1234:5678::/64","203.0.113.0/24"]}"#, new_func(Aws(AWSSourceIP), None, vec!["2001:DB8:1234:5678::/64", "203.0.113.0/24"]); "6")]
|
||||
#[test_case(r#"{"aws:SourceIp": ["2001:DB8:1234:5678::/64", "2001:DB8:1234:5678::"]}"#, new_func(Aws(AWSSourceIP),None, vec!["2001:DB8:1234:5678::/64", "2001:DB8:1234:5678::/32"]); "7")]
|
||||
#[test_case(r#"{"aws:SourceIp": ["2001:DB8:1234:5678::", "203.0.113.0"]}"#, new_func(Aws(AWSSourceIP), None, vec!["2001:DB8:1234:5678::/32", "203.0.113.0/32"]); "8")]
|
||||
#[test_case(r#"{"aws:SourceIp/a": "203.0.113.0/24"}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["203.0.113.0/24"]); "9")]
|
||||
#[test_case(r#"{"aws:SourceIp/a": "203.0.113.0/24"}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["203.0.113.0/24"]); "10")]
|
||||
#[test_case(r#"{"aws:SourceIp/a": "203.0.113.0"}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["203.0.113.0/32"]); "11")]
|
||||
#[test_case(r#"{"aws:SourceIp/a": "2001:DB8:1234:5678::/64"}"#, new_func(Aws(AWSSourceIP),Some("a".into()), vec!["2001:DB8:1234:5678::/64"]); "12")]
|
||||
#[test_case(r#"{"aws:SourceIp/a": "2001:DB8:1234:5678::"}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["2001:DB8:1234:5678::/32"]); "13")]
|
||||
#[test_case(r#"{"aws:SourceIp/a": ["203.0.113.0/24", "203.0.113.0"]}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["203.0.113.0/24", "203.0.113.0/32"]); "14")]
|
||||
#[test_case(r#"{"aws:SourceIp/a": ["2001:DB8:1234:5678::/64", "203.0.113.0/24"]}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["2001:DB8:1234:5678::/64", "203.0.113.0/24"]); "15")]
|
||||
#[test_case(r#"{"aws:SourceIp/a": ["2001:DB8:1234:5678::/64", "2001:DB8:1234:5678::"]}"#, new_func(Aws(AWSSourceIP),Some("a".into()), vec!["2001:DB8:1234:5678::/64", "2001:DB8:1234:5678::/32"]); "16")]
|
||||
#[test_case(r#"{"aws:SourceIp/a": ["2001:DB8:1234:5678::", "203.0.113.0"]}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["2001:DB8:1234:5678::/32", "203.0.113.0/32"]); "17")]
|
||||
fn test_deser(input: &str, expect: AddrFunc) -> Result<(), serde_json::Error> {
|
||||
let v: AddrFunc = serde_json::from_str(input)?;
|
||||
assert_eq!(v, expect);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(r#"{"aws:SourceIp":["203.0.113.0/24"]}"#, new_func(Aws(AWSSourceIP), None, vec!["203.0.113.0/24"]); "1")]
|
||||
#[test_case(r#"{"aws:SourceIp":["203.0.113.0/32"]}"#, new_func(Aws(AWSSourceIP), None, vec!["203.0.113.0/32"]); "2")]
|
||||
#[test_case(r#"{"aws:SourceIp":["2001:db8:1234:5678::/64"]}"#, new_func(Aws(AWSSourceIP),None, vec!["2001:DB8:1234:5678::/64"]); "3")]
|
||||
#[test_case(r#"{"aws:SourceIp":["2001:db8:1234:5678::/32"]}"#, new_func(Aws(AWSSourceIP), None, vec!["2001:DB8:1234:5678::/32"]); "4")]
|
||||
#[test_case(r#"{"aws:SourceIp":["203.0.113.0/24","203.0.113.0/32"]}"#, new_func(Aws(AWSSourceIP), None, vec!["203.0.113.0/24", "203.0.113.0/32"]); "5")]
|
||||
#[test_case(r#"{"aws:SourceIp":["2001:db8:1234:5678::/64","203.0.113.0/24"]}"#, new_func(Aws(AWSSourceIP), None, vec!["2001:DB8:1234:5678::/64", "203.0.113.0/24"]); "6")]
|
||||
#[test_case(r#"{"aws:SourceIp":["2001:db8:1234:5678::/64","2001:db8:1234:5678::/32"]}"#, new_func(Aws(AWSSourceIP),None, vec!["2001:DB8:1234:5678::/64", "2001:DB8:1234:5678::/32"]); "7")]
|
||||
#[test_case(r#"{"aws:SourceIp":["2001:db8:1234:5678::/32","203.0.113.0/32"]}"#, new_func(Aws(AWSSourceIP), None, vec!["2001:DB8:1234:5678::/32", "203.0.113.0/32"]); "8")]
|
||||
#[test_case(r#"{"aws:SourceIp/a":["203.0.113.0/24"]}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["203.0.113.0/24"]); "9")]
|
||||
#[test_case(r#"{"aws:SourceIp/a":["203.0.113.0/24"]}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["203.0.113.0/24"]); "10")]
|
||||
#[test_case(r#"{"aws:SourceIp/a":["203.0.113.0/32"]}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["203.0.113.0/32"]); "11")]
|
||||
#[test_case(r#"{"aws:SourceIp/a":["2001:db8:1234:5678::/64"]}"#, new_func(Aws(AWSSourceIP),Some("a".into()), vec!["2001:DB8:1234:5678::/64"]); "12")]
|
||||
#[test_case(r#"{"aws:SourceIp/a":["2001:db8:1234:5678::/32"]}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["2001:DB8:1234:5678::/32"]); "13")]
|
||||
#[test_case(r#"{"aws:SourceIp/a":["203.0.113.0/24","203.0.113.0/32"]}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["203.0.113.0/24", "203.0.113.0/32"]); "14")]
|
||||
#[test_case(r#"{"aws:SourceIp/a":["2001:db8:1234:5678::/64","203.0.113.0/24"]}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["2001:DB8:1234:5678::/64", "203.0.113.0/24"]); "15")]
|
||||
#[test_case(r#"{"aws:SourceIp/a":["2001:db8:1234:5678::/64","2001:db8:1234:5678::/32"]}"#, new_func(Aws(AWSSourceIP),Some("a".into()), vec!["2001:DB8:1234:5678::/64", "2001:DB8:1234:5678::/32"]); "16")]
|
||||
#[test_case(r#"{"aws:SourceIp/a":["2001:db8:1234:5678::/32","203.0.113.0/32"]}"#, new_func(Aws(AWSSourceIP), Some("a".into()), vec!["2001:DB8:1234:5678::/32", "203.0.113.0/32"]); "17")]
|
||||
fn test_ser(expect: &str, input: AddrFunc) -> Result<(), serde_json::Error> {
|
||||
let v = serde_json::to_string(&input)?;
|
||||
assert_eq!(v, expect);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::func::InnerFunc;
|
||||
|
||||
pub type BinaryFunc = InnerFunc<BinaryFuncValue>;
|
||||
|
||||
// todo implement it
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
|
||||
#[serde(transparent)]
|
||||
pub struct BinaryFuncValue(String);
|
||||
|
||||
impl BinaryFunc {
|
||||
pub fn evaluate(&self, _values: &HashMap<String, Vec<String>>) -> bool {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
use super::func::InnerFunc;
|
||||
use serde::de::{Error, IgnoredAny, SeqAccess};
|
||||
use serde::{de, Deserialize, Deserializer, Serialize};
|
||||
use std::{collections::HashMap, fmt};
|
||||
|
||||
pub type BoolFunc = InnerFunc<BoolFuncValue>;
|
||||
impl BoolFunc {
|
||||
pub fn evaluate_bool(&self, values: &HashMap<String, Vec<String>>) -> bool {
|
||||
for inner in self.0.iter() {
|
||||
if !match values.get(inner.key.name().as_str()).and_then(|x| x.first()) {
|
||||
Some(x) => inner.values.0.to_string().as_str() == x,
|
||||
None => false,
|
||||
} {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn evaluate_null(&self, values: &HashMap<String, Vec<String>>) -> bool {
|
||||
for inner in self.0.iter() {
|
||||
let len = values.get(inner.key.name().as_str()).map(Vec::len).unwrap_or(0);
|
||||
let r = if inner.values.0 { len == 0 } else { len != 0 };
|
||||
|
||||
if !r {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct BoolFuncValue(bool);
|
||||
|
||||
impl Serialize for BoolFuncValue {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.0.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for BoolFuncValue {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct BoolOrStringVisitor;
|
||||
|
||||
impl<'de> de::Visitor<'de> for BoolOrStringVisitor {
|
||||
type Value = BoolFuncValue;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a boolean or a string representing 'true' or 'false'")
|
||||
}
|
||||
|
||||
fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
Ok(BoolFuncValue(value))
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
Ok(BoolFuncValue(value.parse::<bool>().map_err(|e| E::custom(format!("{e:?}")))?))
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
let Some(v) = seq.next_element::<BoolFuncValue>()? else {
|
||||
return Err(Error::custom("no value for boolean"));
|
||||
};
|
||||
|
||||
if seq.next_element::<IgnoredAny>()?.is_some() {
|
||||
return Err(Error::custom("only allow one boolean value"));
|
||||
}
|
||||
|
||||
Ok(v)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(BoolOrStringVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{BoolFunc, BoolFuncValue};
|
||||
use crate::policy::function::func::FuncKeyValue;
|
||||
use crate::policy::function::{
|
||||
key::Key,
|
||||
key_name::AwsKeyName::*,
|
||||
key_name::KeyName::{self, *},
|
||||
};
|
||||
use test_case::test_case;
|
||||
|
||||
fn new_func(name: KeyName, variable: Option<String>, value: bool) -> BoolFunc {
|
||||
BoolFunc {
|
||||
0: vec![FuncKeyValue {
|
||||
key: Key { name, variable },
|
||||
values: BoolFuncValue(value),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
#[test_case(r#"{"aws:SecureTransport": "true"}"#, new_func(Aws(AWSSecureTransport), None, true); "1")]
|
||||
#[test_case(r#"{"aws:SecureTransport": "false"}"#, new_func(Aws(AWSSecureTransport), None, false); "2")]
|
||||
#[test_case(r#"{"aws:SecureTransport": true}"#, new_func(Aws(AWSSecureTransport), None, true); "3")]
|
||||
#[test_case(r#"{"aws:SecureTransport": false}"#, new_func(Aws(AWSSecureTransport), None, false); "4")]
|
||||
#[test_case(r#"{"aws:SecureTransport/a": "true"}"#, new_func(Aws(AWSSecureTransport), Some("a".into()), true); "9")]
|
||||
#[test_case(r#"{"aws:SecureTransport/a": "false"}"#, new_func(Aws(AWSSecureTransport), Some("a".into()), false); "10")]
|
||||
#[test_case(r#"{"aws:SecureTransport/a": true}"#, new_func(Aws(AWSSecureTransport), Some("a".into()), true); "11")]
|
||||
#[test_case(r#"{"aws:SecureTransport/a": false}"#, new_func(Aws(AWSSecureTransport), Some("a".into()), false); "12")]
|
||||
#[test_case(r#"{"aws:SecureTransport/a": [true]}"#, new_func(Aws(AWSSecureTransport), Some("a".into()), true); "13")]
|
||||
#[test_case(r#"{"aws:SecureTransport/a": ["false"]}"#, new_func(Aws(AWSSecureTransport), Some("a".into()), false); "14")]
|
||||
fn test_deser(input: &str, expect: BoolFunc) -> Result<(), serde_json::Error> {
|
||||
let v: BoolFunc = serde_json::from_str(input)?;
|
||||
assert_eq!(v, expect);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(r#"{"aws:usernamea":"johndoe"}"#)]
|
||||
#[test_case(r#"{"aws:username":[]}"#)] // 空
|
||||
#[test_case(r#"{"aws:usernamea/value":"johndoe"}"#)]
|
||||
#[test_case(r#"{"aws:usernamea/value":["johndoe", "aaa"]}"#)]
|
||||
#[test_case(r#""aaa""#)]
|
||||
#[test_case(r#"{"aws:SecureTransport/a": ["false", "true"]}"#)]
|
||||
#[test_case(r#"{"aws:SecureTransport/a": [true, false]}"#)]
|
||||
#[test_case(r#"{"aws:SecureTransport/a": ["aa"]}"#)]
|
||||
fn test_deser_failed(input: &str) {
|
||||
assert!(serde_json::from_str::<BoolFunc>(input).is_err());
|
||||
}
|
||||
|
||||
#[test_case(r#"{"aws:SecureTransport":"true"}"#, new_func(Aws(AWSSecureTransport), None, true); "1")]
|
||||
#[test_case(r#"{"aws:SecureTransport":"false"}"#, new_func(Aws(AWSSecureTransport), None, false);"2")]
|
||||
#[test_case(r#"{"aws:SecureTransport/aa":"true"}"#, new_func(Aws(AWSSecureTransport),Some("aa".into()), true);"3")]
|
||||
#[test_case(r#"{"aws:SecureTransport/aa":"false"}"#, new_func(Aws(AWSSecureTransport), Some("aa".into()), false);"4")]
|
||||
# [test_case(r#"{"aws:SecureTransport/aa":"false"}"#, new_func(Aws(AWSSecureTransport), Some("aa".into()), false); "5")]
|
||||
fn test_ser(expect: &str, input: BoolFunc) -> Result<(), serde_json::Error> {
|
||||
let v = serde_json::to_string(&input)?;
|
||||
assert_eq!(v.as_str(), expect);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
use serde::de::{Error, MapAccess};
|
||||
use serde::ser::SerializeMap;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use super::{addr::AddrFunc, binary::BinaryFunc, bool_null::BoolFunc, date::DateFunc, number::NumberFunc, string::StringFunc};
|
||||
|
||||
#[derive(Clone, Deserialize, Debug)]
|
||||
pub enum Condition {
|
||||
StringEquals(StringFunc),
|
||||
StringNotEquals(StringFunc),
|
||||
StringEqualsIgnoreCase(StringFunc),
|
||||
StringNotEqualsIgnoreCase(StringFunc),
|
||||
StringLike(StringFunc),
|
||||
StringNotLike(StringFunc),
|
||||
BinaryEquals(BinaryFunc),
|
||||
IpAddress(AddrFunc),
|
||||
NotIpAddress(AddrFunc),
|
||||
Null(BoolFunc),
|
||||
Bool(BoolFunc),
|
||||
NumericEquals(NumberFunc),
|
||||
NumericNotEquals(NumberFunc),
|
||||
NumericLessThan(NumberFunc),
|
||||
NumericLessThanEquals(NumberFunc),
|
||||
NumericGreaterThan(NumberFunc),
|
||||
NumericGreaterThanIfExists(NumberFunc),
|
||||
NumericGreaterThanEquals(NumberFunc),
|
||||
DateEquals(DateFunc),
|
||||
DateNotEquals(DateFunc),
|
||||
DateLessThan(DateFunc),
|
||||
DateLessThanEquals(DateFunc),
|
||||
DateGreaterThan(DateFunc),
|
||||
DateGreaterThanEquals(DateFunc),
|
||||
}
|
||||
|
||||
impl Condition {
|
||||
pub fn from_deserializer<'a, D: MapAccess<'a>>(key: &str, d: &mut D) -> Result<Self, D::Error> {
|
||||
Ok(match key {
|
||||
"StringEquals" => Self::StringEquals(d.next_value()?),
|
||||
"StringNotEquals" => Self::StringNotEquals(d.next_value()?),
|
||||
"StringEqualsIgnoreCase" => Self::StringEqualsIgnoreCase(d.next_value()?),
|
||||
"StringNotEqualsIgnoreCase" => Self::StringNotEqualsIgnoreCase(d.next_value()?),
|
||||
"StringLike" => Self::StringLike(d.next_value()?),
|
||||
"StringNotLike" => Self::StringNotLike(d.next_value()?),
|
||||
"BinaryEquals" => Self::BinaryEquals(d.next_value()?),
|
||||
"IpAddress" => Self::IpAddress(d.next_value()?),
|
||||
"NotIpAddress" => Self::NotIpAddress(d.next_value()?),
|
||||
"Null" => Self::Null(d.next_value()?),
|
||||
"Bool" => Self::Bool(d.next_value()?),
|
||||
"NumericEquals" => Self::NumericEquals(d.next_value()?),
|
||||
"NumericNotEquals" => Self::NumericNotEquals(d.next_value()?),
|
||||
"NumericLessThan" => Self::NumericLessThan(d.next_value()?),
|
||||
"NumericGreaterThan" => Self::NumericGreaterThan(d.next_value()?),
|
||||
"NumericGreaterThanIfExists" => Self::NumericGreaterThanIfExists(d.next_value()?),
|
||||
"NumericGreaterThanEquals" => Self::NumericGreaterThanEquals(d.next_value()?),
|
||||
"DateEquals" => Self::DateEquals(d.next_value()?),
|
||||
"DateNotEquals" => Self::DateNotEquals(d.next_value()?),
|
||||
"DateLessThanEquals" => Self::DateLessThanEquals(d.next_value()?),
|
||||
"DateGreaterThan" => Self::DateGreaterThan(d.next_value()?),
|
||||
"DateGreaterThanEquals" => Self::DateGreaterThanEquals(d.next_value()?),
|
||||
_ => Err(Error::custom(format!("unknown key: {key}")))?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_key(&self) -> &'static str {
|
||||
match self {
|
||||
Condition::StringEquals(_) => "StringEquals",
|
||||
Condition::StringNotEquals(_) => "StringNotEquals",
|
||||
Condition::StringEqualsIgnoreCase(_) => "StringEqualsIgnoreCase",
|
||||
Condition::StringNotEqualsIgnoreCase(_) => "StringNotEqualsIgnoreCase",
|
||||
Condition::StringLike(_) => "StringLike",
|
||||
Condition::StringNotLike(_) => "StringNotLike",
|
||||
Condition::BinaryEquals(_) => "BinaryEquals",
|
||||
Condition::IpAddress(_) => "IpAddress",
|
||||
Condition::NotIpAddress(_) => "NotIpAddress",
|
||||
Condition::Null(_) => "Null",
|
||||
Condition::Bool(_) => "Bool",
|
||||
Condition::NumericEquals(_) => "NumericEquals",
|
||||
Condition::NumericNotEquals(_) => "NumericNotEquals",
|
||||
Condition::NumericLessThan(_) => "NumericLessThan",
|
||||
Condition::NumericLessThanEquals(_) => "NumericLessThanEquals",
|
||||
Condition::NumericGreaterThan(_) => "NumericGreaterThan",
|
||||
Condition::NumericGreaterThanIfExists(_) => "NumericGreaterThanIfExists",
|
||||
Condition::NumericGreaterThanEquals(_) => "NumericGreaterThanEquals",
|
||||
Condition::DateEquals(_) => "DateEquals",
|
||||
Condition::DateNotEquals(_) => "DateNotEquals",
|
||||
Condition::DateLessThan(_) => "DateLessThan",
|
||||
Condition::DateLessThanEquals(_) => "DateLessThanEquals",
|
||||
Condition::DateGreaterThan(_) => "DateGreaterThan",
|
||||
Condition::DateGreaterThanEquals(_) => "DateGreaterThanEquals",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn evaluate(&self, for_all: bool, values: &HashMap<String, Vec<String>>) -> bool {
|
||||
use Condition::*;
|
||||
|
||||
let r = match self {
|
||||
StringEquals(s) => s.evaluate(for_all, false, false, false, values),
|
||||
StringNotEquals(s) => s.evaluate(for_all, false, false, true, values),
|
||||
StringEqualsIgnoreCase(s) => s.evaluate(for_all, true, false, false, values),
|
||||
StringNotEqualsIgnoreCase(s) => s.evaluate(for_all, true, false, true, values),
|
||||
StringLike(s) => s.evaluate(for_all, false, true, false, values),
|
||||
StringNotLike(s) => s.evaluate(for_all, false, true, true, values),
|
||||
BinaryEquals(s) => s.evaluate(values),
|
||||
IpAddress(s) => s.evaluate(values),
|
||||
NotIpAddress(s) => s.evaluate(values),
|
||||
Null(s) => s.evaluate_null(values),
|
||||
Bool(s) => s.evaluate_bool(values),
|
||||
NumericEquals(s) => s.evaluate(i64::eq, false, values),
|
||||
NumericNotEquals(s) => s.evaluate(i64::ne, false, values),
|
||||
NumericLessThan(s) => s.evaluate(i64::lt, false, values),
|
||||
NumericLessThanEquals(s) => s.evaluate(i64::le, false, values),
|
||||
NumericGreaterThan(s) => s.evaluate(i64::gt, false, values),
|
||||
NumericGreaterThanIfExists(s) => s.evaluate(i64::ge, true, values),
|
||||
NumericGreaterThanEquals(s) => s.evaluate(i64::ge, false, values),
|
||||
DateEquals(s) => s.evaluate(OffsetDateTime::eq, values),
|
||||
DateNotEquals(s) => s.evaluate(OffsetDateTime::ne, values),
|
||||
DateLessThan(s) => s.evaluate(OffsetDateTime::lt, values),
|
||||
DateLessThanEquals(s) => s.evaluate(OffsetDateTime::le, values),
|
||||
DateGreaterThan(s) => s.evaluate(OffsetDateTime::gt, values),
|
||||
DateGreaterThanEquals(s) => s.evaluate(OffsetDateTime::ge, values),
|
||||
};
|
||||
|
||||
if self.is_negate() {
|
||||
!r
|
||||
} else {
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_negate(&self) -> bool {
|
||||
use Condition::*;
|
||||
matches!(self, StringNotEquals(_) | StringNotEqualsIgnoreCase(_) | NotIpAddress(_))
|
||||
}
|
||||
|
||||
pub fn serialize_map<T: SerializeMap>(&self, se: &mut T) -> Result<(), T::Error> {
|
||||
match self {
|
||||
Condition::StringEquals(s) => se.serialize_value(s),
|
||||
Condition::StringNotEquals(s) => se.serialize_value(s),
|
||||
Condition::StringEqualsIgnoreCase(s) => se.serialize_value(s),
|
||||
Condition::StringNotEqualsIgnoreCase(s) => se.serialize_value(s),
|
||||
Condition::StringLike(s) => se.serialize_value(s),
|
||||
Condition::StringNotLike(s) => se.serialize_value(s),
|
||||
Condition::BinaryEquals(s) => se.serialize_value(s),
|
||||
Condition::IpAddress(s) => se.serialize_value(s),
|
||||
Condition::NotIpAddress(s) => se.serialize_value(s),
|
||||
Condition::Null(s) => se.serialize_value(s),
|
||||
Condition::Bool(s) => se.serialize_value(s),
|
||||
Condition::NumericEquals(s) => se.serialize_value(s),
|
||||
Condition::NumericNotEquals(s) => se.serialize_value(s),
|
||||
Condition::NumericLessThan(s) => se.serialize_value(s),
|
||||
Condition::NumericLessThanEquals(s) => se.serialize_value(s),
|
||||
Condition::NumericGreaterThan(s) => se.serialize_value(s),
|
||||
Condition::NumericGreaterThanIfExists(s) => se.serialize_value(s),
|
||||
Condition::NumericGreaterThanEquals(s) => se.serialize_value(s),
|
||||
Condition::DateEquals(s) => se.serialize_value(s),
|
||||
Condition::DateNotEquals(s) => se.serialize_value(s),
|
||||
Condition::DateLessThan(s) => se.serialize_value(s),
|
||||
Condition::DateLessThanEquals(s) => se.serialize_value(s),
|
||||
Condition::DateGreaterThan(s) => se.serialize_value(s),
|
||||
Condition::DateGreaterThanEquals(s) => se.serialize_value(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Condition {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::StringEquals(l0), Self::StringEquals(r0)) => l0 == r0,
|
||||
(Self::StringNotEquals(l0), Self::StringNotEquals(r0)) => l0 == r0,
|
||||
(Self::StringEqualsIgnoreCase(l0), Self::StringEqualsIgnoreCase(r0)) => l0 == r0,
|
||||
(Self::StringNotEqualsIgnoreCase(l0), Self::StringNotEqualsIgnoreCase(r0)) => l0 == r0,
|
||||
(Self::StringLike(l0), Self::StringLike(r0)) => l0 == r0,
|
||||
(Self::StringNotLike(l0), Self::StringNotLike(r0)) => l0 == r0,
|
||||
(Self::BinaryEquals(l0), Self::BinaryEquals(r0)) => l0 == r0,
|
||||
(Self::IpAddress(l0), Self::IpAddress(r0)) => l0 == r0,
|
||||
(Self::NotIpAddress(l0), Self::NotIpAddress(r0)) => l0 == r0,
|
||||
(Self::Null(l0), Self::Null(r0)) => l0 == r0,
|
||||
(Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
|
||||
(Self::NumericEquals(l0), Self::NumericEquals(r0)) => l0 == r0,
|
||||
(Self::NumericNotEquals(l0), Self::NumericNotEquals(r0)) => l0 == r0,
|
||||
(Self::NumericLessThan(l0), Self::NumericLessThan(r0)) => l0 == r0,
|
||||
(Self::NumericLessThanEquals(l0), Self::NumericLessThanEquals(r0)) => l0 == r0,
|
||||
(Self::NumericGreaterThan(l0), Self::NumericGreaterThan(r0)) => l0 == r0,
|
||||
(Self::NumericGreaterThanIfExists(l0), Self::NumericGreaterThanIfExists(r0)) => l0 == r0,
|
||||
(Self::NumericGreaterThanEquals(l0), Self::NumericGreaterThanEquals(r0)) => l0 == r0,
|
||||
(Self::DateEquals(l0), Self::DateEquals(r0)) => l0 == r0,
|
||||
(Self::DateNotEquals(l0), Self::DateNotEquals(r0)) => l0 == r0,
|
||||
(Self::DateLessThan(l0), Self::DateLessThan(r0)) => l0 == r0,
|
||||
(Self::DateLessThanEquals(l0), Self::DateLessThanEquals(r0)) => l0 == r0,
|
||||
(Self::DateGreaterThan(l0), Self::DateGreaterThan(r0)) => l0 == r0,
|
||||
(Self::DateGreaterThanEquals(l0), Self::DateGreaterThanEquals(r0)) => l0 == r0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
use super::func::InnerFunc;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize};
|
||||
use std::{collections::HashMap, fmt};
|
||||
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
|
||||
|
||||
pub type DateFunc = InnerFunc<DateFuncValue>;
|
||||
|
||||
impl DateFunc {
|
||||
pub fn evaluate(&self, op: impl Fn(&OffsetDateTime, &OffsetDateTime) -> bool, values: &HashMap<String, Vec<String>>) -> bool {
|
||||
for inner in self.0.iter() {
|
||||
let v = match values.get(inner.key.name().as_str()).and_then(|x| x.first()) {
|
||||
Some(x) => x,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
let Ok(rv) = OffsetDateTime::parse(v, &Rfc3339) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if !op(&inner.values.0, &rv) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct DateFuncValue(OffsetDateTime);
|
||||
|
||||
impl Serialize for DateFuncValue {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
use serde::ser::Error;
|
||||
serializer.serialize_str(
|
||||
&self
|
||||
.0
|
||||
.format(&Rfc3339)
|
||||
.map_err(|e| S::Error::custom(format!("format datetime failed: {e:?}")))?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for DateFuncValue {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct DateVisitor;
|
||||
|
||||
impl de::Visitor<'_> for DateVisitor {
|
||||
type Value = DateFuncValue;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a data string that is representable in RFC 3339 format.")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(DateFuncValue(
|
||||
OffsetDateTime::parse(value, &Rfc3339).map_err(|e| E::custom(format!("{e:?}")))?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(DateVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{DateFunc, DateFuncValue};
|
||||
use crate::policy::function::func::FuncKeyValue;
|
||||
use crate::policy::function::{
|
||||
key::Key,
|
||||
key_name::KeyName::{self, *},
|
||||
key_name::S3KeyName::*,
|
||||
};
|
||||
use test_case::test_case;
|
||||
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
|
||||
|
||||
fn new_func(name: KeyName, variable: Option<String>, value: &str) -> DateFunc {
|
||||
DateFunc {
|
||||
0: vec![FuncKeyValue {
|
||||
key: Key { name, variable },
|
||||
values: DateFuncValue(OffsetDateTime::parse(value, &Rfc3339).unwrap()),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
#[test_case(r#"{"s3:object-lock-retain-until-date": "2009-11-10T15:00:00Z"}"#, new_func(S3(S3ObjectLockRetainUntilDate), None, "2009-11-10T15:00:00Z"); "1")]
|
||||
#[test_case(r#"{"s3:object-lock-retain-until-date/a": "2009-11-10T15:00:00Z"}"#, new_func(S3(S3ObjectLockRetainUntilDate), Some("a".into()), "2009-11-10T15:00:00Z"); "2")]
|
||||
fn test_deser(input: &str, expect: DateFunc) -> Result<(), serde_json::Error> {
|
||||
let v: DateFunc = serde_json::from_str(input)?;
|
||||
assert_eq!(v, expect);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(r#"{"s3:object-lock-retain-until-date":"2009-11-10T15:00:00Z"}"#, new_func(S3(S3ObjectLockRetainUntilDate), None, "2009-11-10T15:00:00Z"); "1")]
|
||||
#[test_case(r#"{"s3:object-lock-retain-until-date/a":"2009-11-10T15:00:00Z"}"#, new_func(S3(S3ObjectLockRetainUntilDate), Some("a".into()), "2009-11-10T15:00:00Z"); "2")]
|
||||
fn test_ser(expect: &str, input: DateFunc) -> Result<(), serde_json::Error> {
|
||||
let v = serde_json::to_string(&input)?;
|
||||
assert_eq!(v, expect);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use serde::{
|
||||
de::{self, Visitor},
|
||||
Deserialize, Deserializer, Serialize,
|
||||
};
|
||||
|
||||
use super::key::Key;
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct InnerFunc<T>(pub(crate) Vec<FuncKeyValue<T>>);
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct FuncKeyValue<T> {
|
||||
pub key: Key,
|
||||
pub values: T,
|
||||
}
|
||||
|
||||
impl<T: Clone> Clone for FuncKeyValue<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
key: self.key.clone(),
|
||||
values: self.values.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Clone for InnerFunc<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize> Serialize for InnerFunc<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
use serde::ser::SerializeMap;
|
||||
|
||||
let mut map = serializer.serialize_map(Some(self.0.len()))?;
|
||||
|
||||
for kv in self.0.iter() {
|
||||
map.serialize_key(&kv.key)?;
|
||||
map.serialize_value(&kv.values)?;
|
||||
}
|
||||
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> Deserialize<'de> for InnerFunc<T>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct FuncVisitor<T>(PhantomData<T>);
|
||||
impl<'v, T> Visitor<'v> for FuncVisitor<T>
|
||||
where
|
||||
T: Deserialize<'v>,
|
||||
{
|
||||
type Value = InnerFunc<T>;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("struct StringFunc")
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: de::MapAccess<'v>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
|
||||
let mut inner = Vec::with_capacity(map.size_hint().unwrap_or(0));
|
||||
while let Some((key, values)) = map.next_entry::<Key, T>()? {
|
||||
inner.push(FuncKeyValue { key, values });
|
||||
}
|
||||
|
||||
if inner.is_empty() {
|
||||
return Err(Error::custom("has no condition key"));
|
||||
}
|
||||
|
||||
Ok(InnerFunc(inner))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_map(FuncVisitor::<T>(PhantomData))
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
use super::key_name::KeyName;
|
||||
use crate::{policy::Error as PolicyError, sys::Validator};
|
||||
use ecstore::error::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(into = "String")]
|
||||
#[serde(try_from = "&str")]
|
||||
pub struct Key {
|
||||
pub name: KeyName,
|
||||
pub variable: Option<String>,
|
||||
}
|
||||
|
||||
impl Validator for Key {
|
||||
type Error = Error;
|
||||
}
|
||||
|
||||
impl Key {
|
||||
pub fn is(&self, other: &KeyName) -> bool {
|
||||
self.name.eq(other)
|
||||
}
|
||||
|
||||
pub fn var_name(&self) -> String {
|
||||
self.name.var_name()
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
if let Some(ref x) = self.variable {
|
||||
format!("{}/{}", self.name.name(), x)
|
||||
} else {
|
||||
self.name.name().to_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Key> for String {
|
||||
fn from(value: Key) -> Self {
|
||||
let mut data = String::from(Into::<&str>::into(&value.name));
|
||||
if let Some(x) = value.variable.as_ref() {
|
||||
data.push('/');
|
||||
data.push_str(x);
|
||||
}
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Key {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
let mut iter = value.splitn(2, '/');
|
||||
let name = iter.next().ok_or_else(|| PolicyError::InvalidKey(value.to_string()))?;
|
||||
let variable = iter.next().map(Into::into);
|
||||
|
||||
Ok(Self {
|
||||
name: KeyName::try_from(name)?,
|
||||
variable,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Key;
|
||||
use test_case::test_case;
|
||||
|
||||
fn new_key(name: &str, value: Option<&str>) -> Key {
|
||||
Key {
|
||||
name: name.try_into().unwrap(),
|
||||
variable: value.map(ToString::to_string),
|
||||
}
|
||||
}
|
||||
|
||||
#[test_case(new_key("s3:x-amz-copy-source", Some("aaa")), r#""s3:x-amz-copy-source/aaa""#)]
|
||||
#[test_case(new_key("s3:x-amz-copy-source", None), r#""s3:x-amz-copy-source""#)]
|
||||
#[test_case(new_key("aws:Referer", Some("bbb")), r#""aws:Referer/bbb""#)]
|
||||
#[test_case(new_key("aws:Referer", None), r#""aws:Referer""#)]
|
||||
#[test_case(new_key("jwt:website", None), r#""jwt:website""#)]
|
||||
#[test_case(new_key("jwt:website", Some("aaa")), r#""jwt:website/aaa""#)]
|
||||
#[test_case(new_key("svc:DurationSeconds", None), r#""svc:DurationSeconds""#)]
|
||||
#[test_case(new_key("svc:DurationSeconds", Some("aaa")), r#""svc:DurationSeconds/aaa""#)]
|
||||
fn test_serialize_successful(key: Key, except: &str) -> Result<(), serde_json::Error> {
|
||||
let val = serde_json::to_string(&key)?;
|
||||
assert_eq!(val.as_str(), except);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case("s3:x-amz-copy-source1/aaa")]
|
||||
#[test_case("s33:x-amz-copy-source")]
|
||||
#[test_case("aw2s:Referer/bbb")]
|
||||
#[test_case("aws:Referera")]
|
||||
#[test_case("jwdt:website")]
|
||||
#[test_case("jwt:dwebsite/aaa")]
|
||||
#[test_case("sfvc:DuratdionSeconds")]
|
||||
#[test_case("svc:DursationSeconds/aaa")]
|
||||
fn test_deserialize_falied(key: &str) {
|
||||
let val = serde_json::from_str::<Key>(key);
|
||||
assert!(val.is_err());
|
||||
}
|
||||
|
||||
#[test_case(new_key("s3:x-amz-copy-source", Some("aaa")), r#""s3:x-amz-copy-source/aaa""#)]
|
||||
#[test_case(new_key("s3:x-amz-copy-source", None), r#""s3:x-amz-copy-source""#)]
|
||||
#[test_case(new_key("aws:Referer", Some("bbb")), r#""aws:Referer/bbb""#)]
|
||||
#[test_case(new_key("aws:Referer", None), r#""aws:Referer""#)]
|
||||
#[test_case(new_key("jwt:website", None), r#""jwt:website""#)]
|
||||
#[test_case(new_key("jwt:website", Some("aaa")), r#""jwt:website/aaa""#)]
|
||||
#[test_case(new_key("svc:DurationSeconds", None), r#""svc:DurationSeconds""#)]
|
||||
#[test_case(new_key("svc:DurationSeconds", Some("aaa")), r#""svc:DurationSeconds/aaa""#)]
|
||||
fn test_deserialize(except: Key, input: &str) -> Result<(), serde_json::Error> {
|
||||
let v = serde_json::from_str::<Key>(input)?;
|
||||
assert_eq!(v.name, except.name);
|
||||
assert_eq!(v.variable, except.variable);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,369 +0,0 @@
|
||||
use crate::policy::Error::{self, InvalidKeyName};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{EnumString, IntoStaticStr};
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
|
||||
#[serde(try_from = "&str", untagged)]
|
||||
pub enum KeyName {
|
||||
Aws(AwsKeyName),
|
||||
Jwt(JwtKeyName),
|
||||
Ldap(LdapKeyName),
|
||||
Sts(StsKeyName),
|
||||
Svc(SvcKeyName),
|
||||
S3(S3KeyName),
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for KeyName {
|
||||
type Error = Error;
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
Ok(if value.starts_with("s3:") {
|
||||
Self::S3(S3KeyName::try_from(value).map_err(|_| InvalidKeyName(value.into()))?)
|
||||
} else if value.starts_with("aws:") {
|
||||
Self::Aws(AwsKeyName::try_from(value).map_err(|_| InvalidKeyName(value.into()))?)
|
||||
} else if value.starts_with("ldap:") {
|
||||
Self::Ldap(LdapKeyName::try_from(value).map_err(|_| InvalidKeyName(value.into()))?)
|
||||
} else if value.starts_with("sts:") {
|
||||
Self::Sts(StsKeyName::try_from(value).map_err(|_| InvalidKeyName(value.into()))?)
|
||||
} else if value.starts_with("jwt:") {
|
||||
Self::Jwt(JwtKeyName::try_from(value).map_err(|_| InvalidKeyName(value.into()))?)
|
||||
} else if value.starts_with("svc:") {
|
||||
Self::Svc(SvcKeyName::try_from(value).map_err(|_| InvalidKeyName(value.into()))?)
|
||||
} else {
|
||||
Err(InvalidKeyName(value.into()))?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyName {
|
||||
pub const COMMON_KEYS: &'static [KeyName] = &[
|
||||
// s3
|
||||
KeyName::S3(S3KeyName::S3SignatureVersion),
|
||||
KeyName::S3(S3KeyName::S3AuthType),
|
||||
KeyName::S3(S3KeyName::S3SignatureAge),
|
||||
KeyName::S3(S3KeyName::S3XAmzContentSha256),
|
||||
KeyName::S3(S3KeyName::S3LocationConstraint),
|
||||
//aws
|
||||
KeyName::Aws(AwsKeyName::AWSReferer),
|
||||
KeyName::Aws(AwsKeyName::AWSSourceIP),
|
||||
KeyName::Aws(AwsKeyName::AWSUserAgent),
|
||||
KeyName::Aws(AwsKeyName::AWSSecureTransport),
|
||||
KeyName::Aws(AwsKeyName::AWSCurrentTime),
|
||||
KeyName::Aws(AwsKeyName::AWSEpochTime),
|
||||
KeyName::Aws(AwsKeyName::AWSPrincipalType),
|
||||
KeyName::Aws(AwsKeyName::AWSUserID),
|
||||
KeyName::Aws(AwsKeyName::AWSUsername),
|
||||
KeyName::Aws(AwsKeyName::AWSGroups),
|
||||
// ldap
|
||||
KeyName::Ldap(LdapKeyName::User),
|
||||
KeyName::Ldap(LdapKeyName::Username),
|
||||
KeyName::Ldap(LdapKeyName::Groups),
|
||||
// jwt
|
||||
KeyName::Jwt(JwtKeyName::JWTSub),
|
||||
KeyName::Jwt(JwtKeyName::JWTIss),
|
||||
KeyName::Jwt(JwtKeyName::JWTAud),
|
||||
KeyName::Jwt(JwtKeyName::JWTJti),
|
||||
KeyName::Jwt(JwtKeyName::JWTName),
|
||||
KeyName::Jwt(JwtKeyName::JWTUpn),
|
||||
KeyName::Jwt(JwtKeyName::JWTGroups),
|
||||
KeyName::Jwt(JwtKeyName::JWTGivenName),
|
||||
KeyName::Jwt(JwtKeyName::JWTFamilyName),
|
||||
KeyName::Jwt(JwtKeyName::JWTMiddleName),
|
||||
KeyName::Jwt(JwtKeyName::JWTNickName),
|
||||
KeyName::Jwt(JwtKeyName::JWTPrefUsername),
|
||||
KeyName::Jwt(JwtKeyName::JWTProfile),
|
||||
KeyName::Jwt(JwtKeyName::JWTPicture),
|
||||
KeyName::Jwt(JwtKeyName::JWTWebsite),
|
||||
KeyName::Jwt(JwtKeyName::JWTEmail),
|
||||
KeyName::Jwt(JwtKeyName::JWTGender),
|
||||
KeyName::Jwt(JwtKeyName::JWTBirthdate),
|
||||
KeyName::Jwt(JwtKeyName::JWTPhoneNumber),
|
||||
KeyName::Jwt(JwtKeyName::JWTAddress),
|
||||
KeyName::Jwt(JwtKeyName::JWTScope),
|
||||
KeyName::Jwt(JwtKeyName::JWTClientID),
|
||||
];
|
||||
|
||||
pub const fn prefix(&self) -> usize {
|
||||
match self {
|
||||
KeyName::Aws(_) => "aws:".len(),
|
||||
KeyName::Jwt(_) => "jwt:".len(),
|
||||
KeyName::Ldap(_) => "ldap:".len(),
|
||||
KeyName::Sts(_) => "sts:".len(),
|
||||
KeyName::Svc(_) => "svc:".len(),
|
||||
KeyName::S3(_) => "s3:".len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&Into::<&str>::into(self)[self.prefix()..]
|
||||
}
|
||||
|
||||
pub fn var_name(&self) -> String {
|
||||
match self {
|
||||
KeyName::Aws(s) => format!("${{aws:{}}}", Into::<&str>::into(s)),
|
||||
KeyName::Jwt(s) => format!("${{jwt:{}}}", Into::<&str>::into(s)),
|
||||
KeyName::Ldap(s) => format!("${{ldap:{}}}", Into::<&str>::into(s)),
|
||||
KeyName::Sts(s) => format!("${{sts:{}}}", Into::<&str>::into(s)),
|
||||
KeyName::Svc(s) => format!("${{svc:{}}}", Into::<&str>::into(s)),
|
||||
KeyName::S3(s) => format!("${{s3:{}}}", Into::<&str>::into(s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&KeyName> for &'static str {
|
||||
fn from(k: &KeyName) -> Self {
|
||||
match k {
|
||||
KeyName::Aws(aws) => aws.into(),
|
||||
KeyName::Jwt(jwt) => jwt.into(),
|
||||
KeyName::Ldap(ldap) => ldap.into(),
|
||||
KeyName::Sts(sts) => sts.into(),
|
||||
KeyName::Svc(svc) => svc.into(),
|
||||
KeyName::S3(s3) => s3.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, EnumString, Debug, IntoStaticStr, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(try_from = "&str", into = "&str")]
|
||||
pub enum S3KeyName {
|
||||
#[strum(serialize = "s3:x-amz-copy-source")]
|
||||
S3XAmzCopySource,
|
||||
|
||||
#[strum(serialize = "s3:x-amz-server-side-encryption")]
|
||||
S3XAmzServerSideEncryption,
|
||||
|
||||
#[strum(serialize = "s3:x-amz-server-side-encryption-customer-algorithm")]
|
||||
S3XAmzServerSideEncryptionCustomerAlgorithm,
|
||||
|
||||
#[strum(serialize = "s3:signatureversion")]
|
||||
S3SignatureVersion,
|
||||
|
||||
#[strum(serialize = "s3:authType")]
|
||||
S3AuthType,
|
||||
|
||||
#[strum(serialize = "s3:signatureAge")]
|
||||
S3SignatureAge,
|
||||
|
||||
#[strum(serialize = "s3:x-amz-content-sha256")]
|
||||
S3XAmzContentSha256,
|
||||
|
||||
#[strum(serialize = "s3:LocationConstraint")]
|
||||
S3LocationConstraint,
|
||||
|
||||
#[strum(serialize = "s3:object-lock-retain-until-date")]
|
||||
S3ObjectLockRetainUntilDate,
|
||||
|
||||
#[strum(serialize = "s3:max-keys")]
|
||||
S3MaxKeys,
|
||||
|
||||
#[strum(serialize = "s3:x-amz-metadata-directive")]
|
||||
S3XAmzMetadataDirective,
|
||||
|
||||
#[strum(serialize = "s3:x-amz-storage-class")]
|
||||
S3XAmzStorageClass,
|
||||
|
||||
#[strum(serialize = "s3:prefix")]
|
||||
S3Prefix,
|
||||
|
||||
#[strum(serialize = "s3:delimiter")]
|
||||
S3Delimiter,
|
||||
|
||||
#[strum(serialize = "s3:ExistingObjectTag")]
|
||||
S3ExistingObjectTag,
|
||||
#[strum(serialize = "s3:RequestObjectTagKeys")]
|
||||
S3RequestObjectTagKeys,
|
||||
#[strum(serialize = "s3:RequestObjectTag")]
|
||||
S3RequestObjectTag,
|
||||
}
|
||||
|
||||
#[derive(Clone, EnumString, Debug, IntoStaticStr, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(try_from = "&str", into = "&str")]
|
||||
pub enum JwtKeyName {
|
||||
#[strum(serialize = "jwt:sub")]
|
||||
JWTSub,
|
||||
|
||||
#[strum(serialize = "jwt:iss")]
|
||||
JWTIss,
|
||||
|
||||
#[strum(serialize = "jwt:aud")]
|
||||
JWTAud,
|
||||
|
||||
#[strum(serialize = "jwt:jti")]
|
||||
JWTJti,
|
||||
|
||||
#[strum(serialize = "jwt:name")]
|
||||
JWTName,
|
||||
|
||||
#[strum(serialize = "jwt:upn")]
|
||||
JWTUpn,
|
||||
|
||||
#[strum(serialize = "jwt:groups")]
|
||||
JWTGroups,
|
||||
|
||||
#[strum(serialize = "jwt:given_name")]
|
||||
JWTGivenName,
|
||||
|
||||
#[strum(serialize = "jwt:family_name")]
|
||||
JWTFamilyName,
|
||||
|
||||
#[strum(serialize = "jwt:middle_name")]
|
||||
JWTMiddleName,
|
||||
|
||||
#[strum(serialize = "jwt:nickname")]
|
||||
JWTNickName,
|
||||
|
||||
#[strum(serialize = "jwt:preferred_username")]
|
||||
JWTPrefUsername,
|
||||
|
||||
#[strum(serialize = "jwt:profile")]
|
||||
JWTProfile,
|
||||
|
||||
#[strum(serialize = "jwt:picture")]
|
||||
JWTPicture,
|
||||
|
||||
#[strum(serialize = "jwt:website")]
|
||||
JWTWebsite,
|
||||
|
||||
#[strum(serialize = "jwt:email")]
|
||||
JWTEmail,
|
||||
|
||||
#[strum(serialize = "jwt:gender")]
|
||||
JWTGender,
|
||||
|
||||
#[strum(serialize = "jwt:birthdate")]
|
||||
JWTBirthdate,
|
||||
|
||||
#[strum(serialize = "jwt:phone_number")]
|
||||
JWTPhoneNumber,
|
||||
|
||||
#[strum(serialize = "jwt:address")]
|
||||
JWTAddress,
|
||||
|
||||
#[strum(serialize = "jwt:scope")]
|
||||
JWTScope,
|
||||
|
||||
#[strum(serialize = "jwt:client_id")]
|
||||
JWTClientID,
|
||||
}
|
||||
|
||||
#[derive(Clone, EnumString, Debug, IntoStaticStr, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(try_from = "&str", into = "&str")]
|
||||
pub enum SvcKeyName {
|
||||
#[strum(serialize = "svc:DurationSeconds")]
|
||||
SVCDurationSeconds,
|
||||
}
|
||||
|
||||
#[derive(Clone, EnumString, Debug, IntoStaticStr, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(try_from = "&str", into = "&str")]
|
||||
pub enum LdapKeyName {
|
||||
#[strum(serialize = "ldap:user")]
|
||||
User,
|
||||
|
||||
#[strum(serialize = "ldap:username")]
|
||||
Username,
|
||||
|
||||
#[strum(serialize = "ldap:groups")]
|
||||
Groups,
|
||||
}
|
||||
|
||||
#[derive(Clone, EnumString, Debug, IntoStaticStr, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(try_from = "&str", into = "&str")]
|
||||
pub enum StsKeyName {
|
||||
#[strum(serialize = "sts:DurationSeconds")]
|
||||
STSDurationSeconds,
|
||||
}
|
||||
|
||||
#[derive(Clone, EnumString, Debug, IntoStaticStr, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(try_from = "&str", into = "&str")]
|
||||
pub enum AwsKeyName {
|
||||
#[strum(serialize = "aws:Referer")]
|
||||
AWSReferer,
|
||||
|
||||
#[strum(serialize = "aws:SourceIp")]
|
||||
AWSSourceIP,
|
||||
|
||||
#[strum(serialize = "aws:UserAgent")]
|
||||
AWSUserAgent,
|
||||
|
||||
#[strum(serialize = "aws:SecureTransport")]
|
||||
AWSSecureTransport,
|
||||
|
||||
#[strum(serialize = "aws:CurrentTime")]
|
||||
AWSCurrentTime,
|
||||
|
||||
#[strum(serialize = "aws:EpochTime")]
|
||||
AWSEpochTime,
|
||||
|
||||
#[strum(serialize = "aws:principaltype")]
|
||||
AWSPrincipalType,
|
||||
|
||||
#[strum(serialize = "aws:userid")]
|
||||
AWSUserID,
|
||||
|
||||
#[strum(serialize = "aws:username")]
|
||||
AWSUsername,
|
||||
|
||||
#[strum(serialize = "aws:groups")]
|
||||
AWSGroups,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::policy::Error;
|
||||
use serde::Deserialize;
|
||||
use test_case::test_case;
|
||||
|
||||
#[test_case("s3:x-amz-copy-source", KeyName::S3(S3KeyName::S3XAmzCopySource))]
|
||||
#[test_case("aws:SecureTransport", KeyName::Aws(AwsKeyName::AWSSecureTransport))]
|
||||
#[test_case("jwt:sub", KeyName::Jwt(JwtKeyName::JWTSub))]
|
||||
#[test_case("ldap:user", KeyName::Ldap(LdapKeyName::User))]
|
||||
#[test_case("sts:DurationSeconds", KeyName::Sts(StsKeyName::STSDurationSeconds))]
|
||||
#[test_case("svc:DurationSeconds", KeyName::Svc(SvcKeyName::SVCDurationSeconds))]
|
||||
fn key_name_from_str_successful(val: &str, except: KeyName) {
|
||||
let key_name = KeyName::try_from(val);
|
||||
assert_eq!(key_name, Ok(except));
|
||||
}
|
||||
|
||||
#[test_case("S3:x-amz-copy-source")]
|
||||
#[test_case("aWs:SecureTransport")]
|
||||
#[test_case("jwt:suB")]
|
||||
#[test_case("ldap:us")]
|
||||
#[test_case("DurationSeconds")]
|
||||
fn key_name_from_str_failed(val: &str) {
|
||||
assert_eq!(KeyName::try_from(val), Err(Error::InvalidKeyName(val.to_string())));
|
||||
}
|
||||
|
||||
#[test_case("s3:x-amz-copy-source", KeyName::S3(S3KeyName::S3XAmzCopySource))]
|
||||
#[test_case("aws:SecureTransport", KeyName::Aws(AwsKeyName::AWSSecureTransport))]
|
||||
#[test_case("jwt:sub", KeyName::Jwt(JwtKeyName::JWTSub))]
|
||||
#[test_case("ldap:user", KeyName::Ldap(LdapKeyName::User))]
|
||||
#[test_case("sts:DurationSeconds", KeyName::Sts(StsKeyName::STSDurationSeconds))]
|
||||
#[test_case("svc:DurationSeconds", KeyName::Svc(SvcKeyName::SVCDurationSeconds))]
|
||||
fn key_name_deserialize(val: &str, except: KeyName) {
|
||||
#[derive(Deserialize)]
|
||||
struct TestCase {
|
||||
data: KeyName,
|
||||
}
|
||||
|
||||
let data = format!("{{\"data\":\"{val}\"}}");
|
||||
let data: TestCase = serde_json::from_str(data.as_str()).expect("unmarshal failed");
|
||||
assert_eq!(data.data, except);
|
||||
}
|
||||
|
||||
#[test_case("s3:x-amz-copy-source", KeyName::S3(S3KeyName::S3XAmzCopySource))]
|
||||
#[test_case("aws:SecureTransport", KeyName::Aws(AwsKeyName::AWSSecureTransport))]
|
||||
#[test_case("jwt:sub", KeyName::Jwt(JwtKeyName::JWTSub))]
|
||||
#[test_case("ldap:user", KeyName::Ldap(LdapKeyName::User))]
|
||||
#[test_case("sts:DurationSeconds", KeyName::Sts(StsKeyName::STSDurationSeconds))]
|
||||
#[test_case("svc:DurationSeconds", KeyName::Svc(SvcKeyName::SVCDurationSeconds))]
|
||||
fn key_name_serialize(except: &str, value: KeyName) {
|
||||
#[derive(Serialize)]
|
||||
struct TestCase {
|
||||
data: KeyName,
|
||||
}
|
||||
|
||||
let except = format!("{{\"data\":\"{except}\"}}");
|
||||
let data = serde_json::to_string(&TestCase { data: value }).expect("marshal failed");
|
||||
assert_eq!(data, except);
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::func::InnerFunc;
|
||||
use serde::{
|
||||
de::{Error, Visitor},
|
||||
Deserialize, Deserializer, Serialize,
|
||||
};
|
||||
|
||||
pub type NumberFunc = InnerFunc<NumberFuncValue>;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct NumberFuncValue(i64);
|
||||
|
||||
impl NumberFunc {
|
||||
pub fn evaluate(&self, op: impl Fn(&i64, &i64) -> bool, if_exists: bool, values: &HashMap<String, Vec<String>>) -> bool {
|
||||
for inner in self.0.iter() {
|
||||
let v = match values.get(inner.key.name().as_str()).and_then(|x| x.first()) {
|
||||
Some(x) => x,
|
||||
None => return if_exists,
|
||||
};
|
||||
|
||||
let Ok(rv) = v.parse::<i64>() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if !op(&rv, &inner.values.0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for NumberFuncValue {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.0.to_string().as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for NumberFuncValue {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct NumberVisitor;
|
||||
|
||||
impl Visitor<'_> for NumberVisitor {
|
||||
type Value = NumberFuncValue;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a number or a string that can be represented as a number.")
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
Ok(NumberFuncValue(value))
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
Ok(NumberFuncValue(value as i64))
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
Ok(NumberFuncValue(value.parse().map_err(|e| E::custom(format!("{e:?}")))?))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(NumberVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{NumberFunc, NumberFuncValue};
|
||||
use crate::policy::function::func::FuncKeyValue;
|
||||
use crate::policy::function::{
|
||||
key::Key,
|
||||
key_name::KeyName::{self, *},
|
||||
key_name::S3KeyName::*,
|
||||
};
|
||||
use test_case::test_case;
|
||||
|
||||
fn new_func(name: KeyName, variable: Option<String>, value: i64) -> NumberFunc {
|
||||
NumberFunc {
|
||||
0: vec![FuncKeyValue {
|
||||
key: Key { name, variable },
|
||||
values: NumberFuncValue(value),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
#[test_case(r#"{"s3:max-keys": 1}"#, new_func(S3(S3MaxKeys), None, 1); "1")]
|
||||
#[test_case(r#"{"s3:max-keys/a": 1}"#, new_func(S3(S3MaxKeys), Some("a".into()), 1); "2")]
|
||||
#[test_case(r#"{"s3:max-keys": "1"}"#, new_func(S3(S3MaxKeys), None, 1); "3")]
|
||||
#[test_case(r#"{"s3:max-keys/a": "1"}"#, new_func(S3(S3MaxKeys), Some("a".into()), 1); "4")]
|
||||
fn test_deser(input: &str, expect: NumberFunc) -> Result<(), serde_json::Error> {
|
||||
let v: NumberFunc = serde_json::from_str(input)?;
|
||||
assert_eq!(v, expect);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(r#"{"s3:max-keys":"1"}"#, new_func(S3(S3MaxKeys), None, 1); "1")]
|
||||
#[test_case(r#"{"s3:max-keys/a":"1"}"#, new_func(S3(S3MaxKeys), Some("a".into()), 1); "2")]
|
||||
fn test_ser(expect: &str, input: NumberFunc) -> Result<(), serde_json::Error> {
|
||||
let v = serde_json::to_string(&input)?;
|
||||
assert_eq!(v, expect);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,423 +0,0 @@
|
||||
#[cfg(test)]
|
||||
use std::collections::BTreeSet as Set;
|
||||
#[cfg(not(test))]
|
||||
use std::collections::HashSet as Set;
|
||||
use std::fmt;
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
use crate::policy::function::func::FuncKeyValue;
|
||||
use crate::policy::utils::wildcard;
|
||||
use serde::{de, ser::SerializeSeq, Deserialize, Deserializer, Serialize};
|
||||
|
||||
use super::{func::InnerFunc, key_name::KeyName};
|
||||
|
||||
pub type StringFunc = InnerFunc<StringFuncValue>;
|
||||
|
||||
impl StringFunc {
|
||||
pub(crate) fn evaluate(
|
||||
&self,
|
||||
for_all: bool,
|
||||
ignore_case: bool,
|
||||
like: bool,
|
||||
negate: bool,
|
||||
values: &HashMap<String, Vec<String>>,
|
||||
) -> bool {
|
||||
for inner in self.0.iter() {
|
||||
let result = if like {
|
||||
inner.eval_like(for_all, values) ^ negate
|
||||
} else {
|
||||
inner.eval(for_all, ignore_case, values) ^ negate
|
||||
};
|
||||
|
||||
if !result {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl FuncKeyValue<StringFuncValue> {
|
||||
fn eval(&self, for_all: bool, ignore_case: bool, values: &HashMap<String, Vec<String>>) -> bool {
|
||||
let rvalues = values
|
||||
// http.CanonicalHeaderKey ?
|
||||
.get(self.key.name().as_str())
|
||||
.map(|t| {
|
||||
t.iter()
|
||||
.map(|x| {
|
||||
if ignore_case {
|
||||
Cow::Owned(x.to_lowercase())
|
||||
} else {
|
||||
Cow::from(x)
|
||||
}
|
||||
})
|
||||
.collect::<Set<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let fvalues = self
|
||||
.values
|
||||
.0
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let mut c = Cow::from(c);
|
||||
for key in KeyName::COMMON_KEYS {
|
||||
match values.get(key.name()).and_then(|x| x.first()) {
|
||||
Some(v) if !v.is_empty() => return Cow::Owned(c.to_mut().replace(&key.var_name(), v)),
|
||||
_ => continue,
|
||||
};
|
||||
}
|
||||
|
||||
c
|
||||
})
|
||||
.map(|x| if ignore_case { Cow::Owned(x.to_lowercase()) } else { x })
|
||||
.collect::<Set<_>>();
|
||||
|
||||
let ivalues = rvalues.intersection(&fvalues);
|
||||
|
||||
if for_all {
|
||||
rvalues.is_empty() || rvalues.len() == ivalues.count()
|
||||
} else {
|
||||
ivalues.count() > 0
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_like(&self, for_all: bool, values: &HashMap<String, Vec<String>>) -> bool {
|
||||
if let Some(rvalues) = values.get(self.key.name().as_str()) {
|
||||
for v in rvalues.iter() {
|
||||
let matched = self
|
||||
.values
|
||||
.0
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let mut c = Cow::from(c);
|
||||
for key in KeyName::COMMON_KEYS {
|
||||
match values.get(key.name()).and_then(|x| x.first()) {
|
||||
Some(v) if !v.is_empty() => return Cow::Owned(c.to_mut().replace(&key.var_name(), v)),
|
||||
_ => continue,
|
||||
};
|
||||
}
|
||||
|
||||
c
|
||||
})
|
||||
.any(|x| wildcard::is_match(x, v));
|
||||
|
||||
if for_all {
|
||||
if !matched {
|
||||
return false;
|
||||
}
|
||||
} else if matched {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for_all
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析values字段
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
|
||||
pub struct StringFuncValue(pub Set<String>);
|
||||
|
||||
impl Serialize for StringFuncValue {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
if self.0.len() == 1 {
|
||||
serializer.serialize_some(&self.0.iter().next())
|
||||
} else {
|
||||
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
|
||||
for element in &self.0 {
|
||||
seq.serialize_element(element)?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'d> Deserialize<'d> for StringFuncValue {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'d>,
|
||||
{
|
||||
struct StringOrVecVisitor;
|
||||
|
||||
impl<'de> de::Visitor<'de> for StringOrVecVisitor {
|
||||
type Value = StringFuncValue;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a string or an array of strings")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok({
|
||||
let mut hash = Set::new();
|
||||
hash.insert(value.to_string());
|
||||
StringFuncValue(hash)
|
||||
})
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: de::SeqAccess<'de>,
|
||||
{
|
||||
#[cfg(test)]
|
||||
let mut values = Set::new();
|
||||
#[cfg(not(test))]
|
||||
let mut values = Set::with_capacity(seq.size_hint().unwrap_or(0));
|
||||
|
||||
while let Some(value) = seq.next_element::<String>()? {
|
||||
values.insert(value);
|
||||
}
|
||||
Ok(StringFuncValue(values))
|
||||
}
|
||||
}
|
||||
|
||||
let result = deserializer.deserialize_any(StringOrVecVisitor)?;
|
||||
if result.0.is_empty() {
|
||||
use serde::de::Error;
|
||||
|
||||
return Err(Error::custom("empty"));
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{StringFunc, StringFuncValue};
|
||||
use crate::policy::function::func::FuncKeyValue;
|
||||
use crate::policy::function::{
|
||||
key::Key,
|
||||
key_name::AwsKeyName::*,
|
||||
key_name::KeyName::{self, *},
|
||||
};
|
||||
|
||||
use crate::policy::function::key_name::S3KeyName::S3LocationConstraint;
|
||||
use test_case::test_case;
|
||||
|
||||
fn new_func(name: KeyName, variable: Option<String>, values: Vec<&str>) -> StringFunc {
|
||||
StringFunc {
|
||||
0: vec![FuncKeyValue {
|
||||
key: Key { name, variable },
|
||||
values: StringFuncValue(values.into_iter().map(|x| x.to_owned()).collect()),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
#[test_case(r#"{"aws:username": "johndoe"}"#,
|
||||
new_func(Aws(AWSUsername), None, vec!["johndoe"])
|
||||
)]
|
||||
#[test_case(r#"{"aws:username": ["johndoe", "aaa"]}"#, new_func(Aws(AWSUsername), None, vec!["johndoe", "aaa"]
|
||||
))]
|
||||
#[test_case(r#"{"aws:username/value": "johndoe"}"#, new_func(Aws(AWSUsername), Some("value".into()), vec!["johndoe"]
|
||||
))]
|
||||
#[test_case(r#"{"aws:username/value": ["johndoe", "aaa"]}"#, new_func(Aws(AWSUsername), Some("value".into()), vec!["johndoe", "aaa"]
|
||||
))]
|
||||
fn test_deser(input: &str, expect: StringFunc) -> Result<(), serde_json::Error> {
|
||||
let v: StringFunc = serde_json::from_str(input)?;
|
||||
assert_eq!(v, expect);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(r#"{"aws:usernamea":"johndoe"}"#)]
|
||||
#[test_case(r#"{"aws:username":[]}"#)] // 空
|
||||
#[test_case(r#"{"aws:usernamea/value":"johndoe"}"#)]
|
||||
#[test_case(r#"{"aws:usernamea/value":["johndoe", "aaa"]}"#)]
|
||||
#[test_case(r#""aaa""#)]
|
||||
fn test_deser_failed(input: &str) {
|
||||
assert!(serde_json::from_str::<StringFunc>(input).is_err());
|
||||
}
|
||||
|
||||
#[test_case(r#"{"aws:username":"johndoe"}"#, new_func(Aws(AWSUsername), None, vec!["johndoe"]))]
|
||||
#[test_case(r#"{"aws:username":["aaa","johndoe"]}"#, new_func(Aws(AWSUsername), None, vec!["johndoe", "aaa"]))]
|
||||
#[test_case(r#"{"aws:username/value":"johndoe"}"#, new_func(Aws(AWSUsername), Some("value".into()), vec!["johndoe"]))]
|
||||
#[test_case(r#"{"aws:username/value":["aaa","johndoe"]}"#, new_func(Aws(AWSUsername), Some("value".into()), vec!["johndoe", "aaa"]))]
|
||||
fn test_ser(expect: &str, input: StringFunc) -> Result<(), serde_json::Error> {
|
||||
let v = serde_json::to_string(&input)?;
|
||||
assert_eq!(v.as_str(), expect);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn new_fkv(name: &str, values: Vec<&str>) -> FuncKeyValue<StringFuncValue> {
|
||||
FuncKeyValue {
|
||||
key: name.try_into().unwrap(),
|
||||
values: StringFuncValue(values.into_iter().map(ToOwned::to_owned).collect()),
|
||||
}
|
||||
}
|
||||
|
||||
fn test_eval(
|
||||
s: FuncKeyValue<StringFuncValue>,
|
||||
for_all: bool,
|
||||
ignore_case: bool,
|
||||
negate: bool,
|
||||
values: Vec<(&str, Vec<&str>)>,
|
||||
) -> bool {
|
||||
let result = s.eval(
|
||||
for_all,
|
||||
ignore_case,
|
||||
&values
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_owned(), v.into_iter().map(ToOwned::to_owned).collect::<Vec<String>>()))
|
||||
.collect(),
|
||||
);
|
||||
|
||||
result ^ negate
|
||||
}
|
||||
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => true ; "1")]
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["yourbucket/myobject"])] => false ; "2")]
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![] => false ; "3")]
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("delimiter", vec!["/"])] => false ; "4")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-1"])] => true ; "5")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["ap-southeast-1"])] => true ; "6")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["us-east-1"])] => false ; "7")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![] => false ; "8")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("delimiter", vec!["/"])] => false ; "9")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![("groups", vec!["prod", "art"])] => true ; "10")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![("groups", vec!["art"])] => true ; "11")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![] => true ; "12")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![("delimiter", vec!["/"])] => true ; "13")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![("groups", vec!["prod", "art"])] => true ; "14")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![("groups", vec!["art"])] => true ; "15")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![] => false ; "16")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![("delimiter", vec!["/"])] => false ; "17")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec![KeyName::S3(S3LocationConstraint).var_name().as_str()]), false, vec![("LocationConstraint", vec!["us-west-1"])] => true ; "18")]
|
||||
#[test_case(new_fkv("s3:ExistingObjectTag/security", vec!["public"]), false, vec![("ExistingObjectTag/security", vec!["public"])] => true ; "19")]
|
||||
#[test_case(new_fkv("s3:ExistingObjectTag/security", vec!["public"]), false, vec![("ExistingObjectTag/security", vec!["private"])] => false ; "20")]
|
||||
#[test_case(new_fkv("s3:ExistingObjectTag/security", vec!["public"]), false, vec![("ExistingObjectTag/project", vec!["foo"])] => false ; "21")]
|
||||
fn test_string_equals(s: FuncKeyValue<StringFuncValue>, for_all: bool, values: Vec<(&str, Vec<&str>)>) -> bool {
|
||||
test_eval(s, for_all, false, false, values)
|
||||
}
|
||||
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => false ; "1")]
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["yourbucket/myobject"])] => true ; "2")]
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![] => true ; "3")]
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("delimiter", vec!["/"])] => true ; "4")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-1"])] => false ; "5")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["ap-southeast-1"])] => false ; "6")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["us-east-1"])] => true ; "7")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![] => true ; "8")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("delimiter", vec!["/"])] => true ; "9")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![("groups", vec!["prod", "art"])] => false ; "10")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![("groups", vec!["art"])] => false ; "11")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![] => false ; "12")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![("delimiter", vec!["/"])] => false ; "13")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![("groups", vec!["prod", "art"])] => false ; "14")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![("groups", vec!["art"])] => false ; "15")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![] => true ; "16")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![("delimiter", vec!["/"])] => true ; "17")]
|
||||
fn test_string_not_equals(s: FuncKeyValue<StringFuncValue>, for_all: bool, values: Vec<(&str, Vec<&str>)>) -> bool {
|
||||
test_eval(s, for_all, false, true, values)
|
||||
}
|
||||
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => true ; "1")]
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![("x-amz-copy-source", vec!["yourbucket/myobject"])] => false ; "2")]
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![] => false ; "3")]
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![("delimiter", vec!["/"])] => false ; "4")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-1"])] => true ; "5")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("LocationConstraint", vec!["ap-southeast-1"])] => true ; "6")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("LocationConstraint", vec!["us-east-1"])] => false ; "7")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![] => false ; "8")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("delimiter", vec!["/"])] => false ; "9")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![("groups", vec!["prod", "art"])] => true ; "10")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![("groups", vec!["art"])] => true ; "11")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![] => true ; "12")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![("delimiter", vec!["/"])] => true ; "13")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![("groups", vec!["prod", "art"])] => true ; "14")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![("groups", vec!["art"])] => true ; "15")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![] => false ; "16")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![("delimiter", vec!["/"])] => false ; "17")]
|
||||
fn test_string_equals_ignore_case(s: FuncKeyValue<StringFuncValue>, for_all: bool, values: Vec<(&str, Vec<&str>)>) -> bool {
|
||||
test_eval(s, for_all, true, false, values)
|
||||
}
|
||||
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => false ; "1")]
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![("x-amz-copy-source", vec!["yourbucket/myobject"])] => true ; "2")]
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![] => true ; "3")]
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![("delimiter", vec!["/"])] => true ; "4")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-1"])] => false ; "5")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("LocationConstraint", vec!["ap-southeast-1"])] => false ; "6")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("LocationConstraint", vec!["us-east-1"])] => true ; "7")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![] => true ; "8")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("delimiter", vec!["/"])] => true ; "9")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![("groups", vec!["prod", "art"])] => false ; "10")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![("groups", vec!["art"])] => false ; "11")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![] => false ; "12")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![("delimiter", vec!["/"])] => false ; "13")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![("groups", vec!["prod", "art"])] => false ; "14")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![("groups", vec!["art"])] => false ; "15")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![] => true ; "16")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![("delimiter", vec!["/"])] => true ; "17")]
|
||||
fn test_string_not_equals_ignore_case(
|
||||
s: FuncKeyValue<StringFuncValue>,
|
||||
for_all: bool,
|
||||
values: Vec<(&str, Vec<&str>)>,
|
||||
) -> bool {
|
||||
test_eval(s, for_all, true, true, values)
|
||||
}
|
||||
|
||||
fn test_eval_like(s: FuncKeyValue<StringFuncValue>, for_all: bool, negate: bool, values: Vec<(&str, Vec<&str>)>) -> bool {
|
||||
let result = s.eval_like(
|
||||
for_all,
|
||||
&values
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_owned(), v.into_iter().map(ToOwned::to_owned).collect::<Vec<String>>()))
|
||||
.collect(),
|
||||
);
|
||||
|
||||
result ^ negate
|
||||
}
|
||||
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => true ; "1")]
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["yourbucket/myobject"])] => false ; "2")]
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![] => false ; "3")]
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("delimiter", vec!["/"])] => false ; "4")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-1"])] => true ; "5")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["ap-southeast-1"])] => true ; "6")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["us-east-1"])] => false ; "7")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![] => false ; "8")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("delimiter", vec!["/"])] => false ; "9")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-2"])] => true ; "10")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![("groups", vec!["prod", "art"])] => true ; "11")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![("groups", vec!["art"])] => true ; "12")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![] => true ; "13")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![("delimiter", vec!["/"])] => true ; "14")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![("groups", vec!["prod", "art"])] => true ; "15")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![("groups", vec!["art"])] => true ; "16")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![] => false ; "17")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![("delimiter", vec!["/"])] => false ; "18")]
|
||||
fn test_string_like(s: FuncKeyValue<StringFuncValue>, for_all: bool, values: Vec<(&str, Vec<&str>)>) -> bool {
|
||||
test_eval_like(s, for_all, false, values)
|
||||
}
|
||||
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => false ; "1")]
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["yourbucket/myobject"])] => true ; "2")]
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![] => true ; "3")]
|
||||
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("delimiter", vec!["/"])] => true ; "4")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-1"])] => false ; "5")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["ap-southeast-1"])] => false ; "6")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["us-east-1"])] => true ; "7")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![] => true ; "8")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("delimiter", vec!["/"])] => true ; "9")]
|
||||
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-2"])] => false ; "10")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![("groups", vec!["prod", "art"])] => false ; "11")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![("groups", vec!["art"])] => false ; "12")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![] => false ; "13")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![("delimiter", vec!["/"])] => false ; "14")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![("groups", vec!["prod", "art"])] => false ; "15")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![("groups", vec!["art"])] => false ; "16")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![] => true ; "17")]
|
||||
#[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![("delimiter", vec!["/"])] => true ; "18")]
|
||||
fn test_string_not_like(s: FuncKeyValue<StringFuncValue>, for_all: bool, values: Vec<(&str, Vec<&str>)>) -> bool {
|
||||
test_eval_like(s, for_all, true, values)
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
use ecstore::error::{Error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::sys::Validator;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
|
||||
pub struct ID(pub String);
|
||||
|
||||
impl Validator for ID {
|
||||
type Error = Error;
|
||||
/// if id is a valid utf string, then it is valid.
|
||||
fn is_valid(&self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ToString> From<T> for ID {
|
||||
fn from(value: T) -> Self {
|
||||
Self(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ID {
|
||||
type Target = String;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
@@ -1,394 +0,0 @@
|
||||
use super::{Effect, Error as IamError, Statement, ID};
|
||||
use crate::sys::{Args, Validator, DEFAULT_VERSION};
|
||||
use ecstore::error::{Error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
|
||||
pub struct Policy {
|
||||
#[serde(default, rename = "ID")]
|
||||
pub id: ID,
|
||||
#[serde(rename = "Version")]
|
||||
pub version: String,
|
||||
#[serde(rename = "Statement")]
|
||||
pub statements: Vec<Statement>,
|
||||
}
|
||||
|
||||
impl Policy {
|
||||
pub fn is_allowed(&self, args: &Args) -> bool {
|
||||
for statement in self.statements.iter().filter(|s| matches!(s.effect, Effect::Deny)) {
|
||||
if !statement.is_allowed(args) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if args.deny_only || args.is_owner {
|
||||
return true;
|
||||
}
|
||||
|
||||
for statement in self.statements.iter().filter(|s| matches!(s.effect, Effect::Allow)) {
|
||||
if statement.is_allowed(args) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn match_resource(&self, resource: &str) -> bool {
|
||||
for statement in self.statements.iter() {
|
||||
if statement.resources.match_resource(resource) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn drop_duplicate_statements(&mut self) {
|
||||
let mut dups = HashSet::new();
|
||||
for i in 0..self.statements.len() {
|
||||
if dups.contains(&i) {
|
||||
// i is already a duplicate of some statement, so we do not need to
|
||||
// compare with it.
|
||||
continue;
|
||||
}
|
||||
for j in (i + 1)..self.statements.len() {
|
||||
if !self.statements[i].eq(&self.statements[j]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// save duplicate statement index for removal.
|
||||
dups.insert(j);
|
||||
}
|
||||
}
|
||||
|
||||
// remove duplicate items from the slice.
|
||||
let mut c = 0;
|
||||
for i in 0..self.statements.len() {
|
||||
if dups.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
self.statements[c] = self.statements[i].clone();
|
||||
c += 1;
|
||||
}
|
||||
self.statements.truncate(c);
|
||||
}
|
||||
pub fn merge_policies(inputs: Vec<Policy>) -> Policy {
|
||||
let mut merged = Policy::default();
|
||||
|
||||
for p in inputs {
|
||||
if merged.version.is_empty() {
|
||||
merged.version = p.version.clone();
|
||||
}
|
||||
for st in p.statements {
|
||||
merged.statements.push(st.clone());
|
||||
}
|
||||
}
|
||||
merged.drop_duplicate_statements();
|
||||
merged
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.statements.is_empty()
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
self.is_valid()
|
||||
}
|
||||
|
||||
pub fn parse_config(data: &[u8]) -> Result<Policy> {
|
||||
let policy: Policy = serde_json::from_slice(data)?;
|
||||
policy.validate()?;
|
||||
Ok(policy)
|
||||
}
|
||||
}
|
||||
|
||||
impl Validator for Policy {
|
||||
type Error = Error;
|
||||
|
||||
fn is_valid(&self) -> Result<()> {
|
||||
if !self.id.is_empty() && !self.id.eq(DEFAULT_VERSION) {
|
||||
return Err(IamError::InvalidVersion(self.id.0.clone()).into());
|
||||
}
|
||||
|
||||
for statement in self.statements.iter() {
|
||||
statement.is_valid()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub mod default {
|
||||
use std::{collections::HashSet, sync::LazyLock};
|
||||
|
||||
use crate::{
|
||||
policy::{
|
||||
action::{Action, AdminAction, KmsAction, S3Action},
|
||||
resource::Resource,
|
||||
ActionSet, Effect, Functions, ResourceSet, Statement,
|
||||
},
|
||||
sys::DEFAULT_VERSION,
|
||||
};
|
||||
|
||||
use super::Policy;
|
||||
|
||||
#[allow(clippy::incompatible_msrv)]
|
||||
pub static DEFAULT_POLICIES: LazyLock<[(&'static str, Policy); 6]> = LazyLock::new(|| {
|
||||
[
|
||||
(
|
||||
"readwrite",
|
||||
Policy {
|
||||
id: "".into(),
|
||||
version: DEFAULT_VERSION.into(),
|
||||
statements: vec![Statement {
|
||||
sid: "".into(),
|
||||
effect: Effect::Allow,
|
||||
actions: ActionSet({
|
||||
let mut hash_set = HashSet::new();
|
||||
hash_set.insert(Action::S3Action(S3Action::AllActions));
|
||||
hash_set
|
||||
}),
|
||||
not_actions: ActionSet(Default::default()),
|
||||
resources: ResourceSet({
|
||||
let mut hash_set = HashSet::new();
|
||||
hash_set.insert(Resource::S3("*".into()));
|
||||
hash_set
|
||||
}),
|
||||
conditions: Functions::default(),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
),
|
||||
(
|
||||
"readonly",
|
||||
Policy {
|
||||
id: "".into(),
|
||||
version: DEFAULT_VERSION.into(),
|
||||
statements: vec![Statement {
|
||||
sid: "".into(),
|
||||
effect: Effect::Allow,
|
||||
actions: ActionSet({
|
||||
let mut hash_set = HashSet::new();
|
||||
hash_set.insert(Action::S3Action(S3Action::GetBucketLocationAction));
|
||||
hash_set.insert(Action::S3Action(S3Action::GetObjectAction));
|
||||
hash_set
|
||||
}),
|
||||
not_actions: ActionSet(Default::default()),
|
||||
resources: ResourceSet({
|
||||
let mut hash_set = HashSet::new();
|
||||
hash_set.insert(Resource::S3("*".into()));
|
||||
hash_set
|
||||
}),
|
||||
conditions: Functions::default(),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
),
|
||||
(
|
||||
"writeonly",
|
||||
Policy {
|
||||
id: "".into(),
|
||||
version: DEFAULT_VERSION.into(),
|
||||
statements: vec![Statement {
|
||||
sid: "".into(),
|
||||
effect: Effect::Allow,
|
||||
actions: ActionSet({
|
||||
let mut hash_set = HashSet::new();
|
||||
hash_set.insert(Action::S3Action(S3Action::PutObjectAction));
|
||||
hash_set
|
||||
}),
|
||||
not_actions: ActionSet(Default::default()),
|
||||
resources: ResourceSet({
|
||||
let mut hash_set = HashSet::new();
|
||||
hash_set.insert(Resource::S3("*".into()));
|
||||
hash_set
|
||||
}),
|
||||
conditions: Functions::default(),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
),
|
||||
(
|
||||
"writeonly",
|
||||
Policy {
|
||||
id: "".into(),
|
||||
version: DEFAULT_VERSION.into(),
|
||||
statements: vec![Statement {
|
||||
sid: "".into(),
|
||||
effect: Effect::Allow,
|
||||
actions: ActionSet({
|
||||
let mut hash_set = HashSet::new();
|
||||
hash_set.insert(Action::S3Action(S3Action::PutObjectAction));
|
||||
hash_set
|
||||
}),
|
||||
not_actions: ActionSet(Default::default()),
|
||||
resources: ResourceSet({
|
||||
let mut hash_set = HashSet::new();
|
||||
hash_set.insert(Resource::S3("*".into()));
|
||||
hash_set
|
||||
}),
|
||||
conditions: Functions::default(),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
),
|
||||
(
|
||||
"diagnostics",
|
||||
Policy {
|
||||
id: "".into(),
|
||||
version: DEFAULT_VERSION.into(),
|
||||
statements: vec![Statement {
|
||||
sid: "".into(),
|
||||
effect: Effect::Allow,
|
||||
actions: ActionSet({
|
||||
let mut hash_set = HashSet::new();
|
||||
hash_set.insert(Action::AdminAction(AdminAction::ProfilingAdminAction));
|
||||
hash_set.insert(Action::AdminAction(AdminAction::TraceAdminAction));
|
||||
hash_set.insert(Action::AdminAction(AdminAction::ConsoleLogAdminAction));
|
||||
hash_set.insert(Action::AdminAction(AdminAction::ServerInfoAdminAction));
|
||||
hash_set.insert(Action::AdminAction(AdminAction::TopLocksAdminAction));
|
||||
hash_set.insert(Action::AdminAction(AdminAction::HealthInfoAdminAction));
|
||||
hash_set.insert(Action::AdminAction(AdminAction::PrometheusAdminAction));
|
||||
hash_set.insert(Action::AdminAction(AdminAction::BandwidthMonitorAction));
|
||||
hash_set
|
||||
}),
|
||||
not_actions: ActionSet(Default::default()),
|
||||
resources: ResourceSet({
|
||||
let mut hash_set = HashSet::new();
|
||||
hash_set.insert(Resource::S3("*".into()));
|
||||
hash_set
|
||||
}),
|
||||
conditions: Functions::default(),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
),
|
||||
(
|
||||
"consoleAdmin",
|
||||
Policy {
|
||||
id: "".into(),
|
||||
version: DEFAULT_VERSION.into(),
|
||||
statements: vec![
|
||||
Statement {
|
||||
sid: "".into(),
|
||||
effect: Effect::Allow,
|
||||
actions: ActionSet({
|
||||
let mut hash_set = HashSet::new();
|
||||
hash_set.insert(Action::AdminAction(AdminAction::AllActions));
|
||||
hash_set
|
||||
}),
|
||||
not_actions: ActionSet(Default::default()),
|
||||
resources: ResourceSet(HashSet::new()),
|
||||
conditions: Functions::default(),
|
||||
..Default::default()
|
||||
},
|
||||
Statement {
|
||||
sid: "".into(),
|
||||
effect: Effect::Allow,
|
||||
actions: ActionSet({
|
||||
let mut hash_set = HashSet::new();
|
||||
hash_set.insert(Action::KmsAction(KmsAction::AllActions));
|
||||
hash_set
|
||||
}),
|
||||
not_actions: ActionSet(Default::default()),
|
||||
resources: ResourceSet(HashSet::new()),
|
||||
conditions: Functions::default(),
|
||||
..Default::default()
|
||||
},
|
||||
Statement {
|
||||
sid: "".into(),
|
||||
effect: Effect::Allow,
|
||||
actions: ActionSet({
|
||||
let mut hash_set = HashSet::new();
|
||||
hash_set.insert(Action::S3Action(S3Action::AllActions));
|
||||
hash_set
|
||||
}),
|
||||
not_actions: ActionSet(Default::default()),
|
||||
resources: ResourceSet({
|
||||
let mut hash_set = HashSet::new();
|
||||
hash_set.insert(Resource::S3("*".into()));
|
||||
hash_set
|
||||
}),
|
||||
conditions: Functions::default(),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use ecstore::error::Result;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_parse_policy() -> Result<()> {
|
||||
let data = r#"
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["s3:GetObject"],
|
||||
"Resource": ["arn:aws:s3:::dada/*"],
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"s3:ExistingObjectTag/security": "public"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["s3:DeleteObjectTagging"],
|
||||
"Resource": ["arn:aws:s3:::dada/*"],
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"s3:ExistingObjectTag/security": "public"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["s3:DeleteObject"],
|
||||
"Resource": ["arn:aws:s3:::dada/*"]
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:PutObject"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::dada/*"
|
||||
],
|
||||
"Condition": {
|
||||
"ForAllValues:StringLike": {
|
||||
"s3:RequestObjectTagKeys": [
|
||||
"security",
|
||||
"virus"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
"#;
|
||||
|
||||
let p = Policy::parse_config(data.as_bytes())?;
|
||||
|
||||
// println!("{:?}", p);
|
||||
|
||||
let str = serde_json::to_string(&p)?;
|
||||
|
||||
// println!("----- {}", str);
|
||||
|
||||
let _p2 = Policy::parse_config(str.as_bytes())?;
|
||||
// println!("33{:?}", p2);
|
||||
|
||||
// assert_eq!(p, p2);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
use ecstore::error::{Error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
hash::Hash,
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use crate::sys::Validator;
|
||||
|
||||
use super::{
|
||||
function::key_name::KeyName,
|
||||
utils::{path, wildcard},
|
||||
Error as IamError,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
|
||||
pub struct ResourceSet(pub HashSet<Resource>);
|
||||
|
||||
impl ResourceSet {
|
||||
pub fn is_match(&self, resource: &str, conditons: &HashMap<String, Vec<String>>) -> bool {
|
||||
for re in self.0.iter() {
|
||||
if re.is_match(resource, conditons) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn match_resource(&self, resource: &str) -> bool {
|
||||
for re in self.0.iter() {
|
||||
if re.match_resource(resource) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ResourceSet {
|
||||
type Target = HashSet<Resource>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Validator for ResourceSet {
|
||||
type Error = Error;
|
||||
fn is_valid(&self) -> Result<()> {
|
||||
for resource in self.0.iter() {
|
||||
resource.is_valid()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ResourceSet {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.len() == other.len() && self.0.iter().all(|x| other.0.contains(x))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
|
||||
pub enum Resource {
|
||||
S3(String),
|
||||
Kms(String),
|
||||
}
|
||||
|
||||
impl Resource {
|
||||
pub const S3_PREFIX: &'static str = "arn:aws:s3:::";
|
||||
|
||||
pub fn is_match(&self, resource: &str, conditons: &HashMap<String, Vec<String>>) -> bool {
|
||||
let mut pattern = match self {
|
||||
Resource::S3(s) => s.to_owned(),
|
||||
Resource::Kms(s) => s.to_owned(),
|
||||
};
|
||||
if !conditons.is_empty() {
|
||||
for key in KeyName::COMMON_KEYS {
|
||||
if let Some(rvalue) = conditons.get(key.name()) {
|
||||
if matches!(rvalue.first().map(|c| !c.is_empty()), Some(true)) {
|
||||
pattern = pattern.replace(&key.var_name(), &rvalue[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cp = path::clean(resource);
|
||||
if cp != "." && cp == pattern.as_str() {
|
||||
return true;
|
||||
}
|
||||
|
||||
wildcard::is_match(pattern, resource)
|
||||
}
|
||||
|
||||
pub fn match_resource(&self, resource: &str) -> bool {
|
||||
self.is_match(resource, &HashMap::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Resource {
|
||||
type Error = Error;
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
let resource = if value.starts_with(Self::S3_PREFIX) {
|
||||
Resource::S3(value.strip_prefix(Self::S3_PREFIX).unwrap().into())
|
||||
} else {
|
||||
return Err(IamError::InvalidResource("unknown".into(), value.into()).into());
|
||||
};
|
||||
|
||||
resource.is_valid()?;
|
||||
Ok(resource)
|
||||
}
|
||||
}
|
||||
|
||||
impl Validator for Resource {
|
||||
type Error = Error;
|
||||
fn is_valid(&self) -> Result<(), Error> {
|
||||
match self {
|
||||
Self::S3(pattern) => {
|
||||
if pattern.is_empty() || pattern.starts_with('/') {
|
||||
return Err(IamError::InvalidResource("s3".into(), pattern.into()).into());
|
||||
}
|
||||
}
|
||||
Self::Kms(pattern) => {
|
||||
if pattern.is_empty()
|
||||
|| pattern
|
||||
.char_indices()
|
||||
.find(|&(_, c)| c == '/' || c == '\\' || c == '.')
|
||||
.map(|(i, _)| i)
|
||||
.is_some()
|
||||
{
|
||||
return Err(IamError::InvalidResource("kms".into(), pattern.into()).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Resource {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
Resource::S3(s) => serializer.serialize_str(&format!("{}{}", Self::S3_PREFIX, s)),
|
||||
Resource::Kms(s) => serializer.serialize_str(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Resource {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = String::deserialize(deserializer)?;
|
||||
Resource::try_from(value.as_str()).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::policy::resource::Resource;
|
||||
use std::collections::HashMap;
|
||||
use test_case::test_case;
|
||||
|
||||
#[test_case("arn:aws:s3:::*","mybucket" => true; "1")]
|
||||
#[test_case("arn:aws:s3:::*","mybucket/myobject" => true; "2")]
|
||||
#[test_case("arn:aws:s3:::mybucket*","mybucket" => true; "3")]
|
||||
#[test_case("arn:aws:s3:::mybucket*","mybucket/myobject" => true; "4")]
|
||||
#[test_case("arn:aws:s3:::*/*","mybucket/myobject"=> true; "5")]
|
||||
#[test_case("arn:aws:s3:::mybucket/*","mybucket/myobject" => true; "6")]
|
||||
#[test_case("arn:aws:s3:::mybucket*/myobject","mybucket/myobject" => true; "7")]
|
||||
#[test_case("arn:aws:s3:::mybucket*/myobject","mybucket100/myobject" => true; "8")]
|
||||
#[test_case("arn:aws:s3:::mybucket?0/2010/photos/*","mybucket20/2010/photos/1.jpg" => true; "9")]
|
||||
#[test_case("arn:aws:s3:::mybucket","mybucket" => true; "10")]
|
||||
#[test_case("arn:aws:s3:::mybucket?0","mybucket30" => true; "11")]
|
||||
#[test_case("arn:aws:s3:::*/*","mybucket" => false; "12")]
|
||||
#[test_case("arn:aws:s3:::mybucket/*","mybucket10/myobject" => false; "13")]
|
||||
#[test_case("arn:aws:s3:::mybucket?0/2010/photos/*","mybucket0/2010/photos/1.jpg" => false; "14")]
|
||||
#[test_case("arn:aws:s3:::mybucket","mybucket/myobject" => false; "15")]
|
||||
fn test_resource_is_match(resource: &str, object: &str) -> bool {
|
||||
let resource: Resource = resource.try_into().unwrap();
|
||||
resource.is_match(object, &HashMap::new())
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
use crate::sys::{Args, Validator};
|
||||
|
||||
use super::{action::Action, ActionSet, Effect, Error as IamError, Functions, ResourceSet, ID};
|
||||
use ecstore::error::{Error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
|
||||
pub struct Statement {
|
||||
#[serde(rename = "Sid", default)]
|
||||
pub sid: ID,
|
||||
#[serde(rename = "Effect")]
|
||||
pub effect: Effect,
|
||||
#[serde(rename = "Action")]
|
||||
pub actions: ActionSet,
|
||||
#[serde(rename = "NotAction", default)]
|
||||
pub not_actions: ActionSet,
|
||||
#[serde(rename = "Resource", default)]
|
||||
pub resources: ResourceSet,
|
||||
#[serde(rename = "NotResource", default)]
|
||||
pub not_resources: ResourceSet,
|
||||
#[serde(rename = "Condition", default)]
|
||||
pub conditions: Functions,
|
||||
}
|
||||
|
||||
impl Statement {
|
||||
fn is_kms(&self) -> bool {
|
||||
for act in self.actions.iter() {
|
||||
if matches!(act, Action::KmsAction(_)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn is_admin(&self) -> bool {
|
||||
for act in self.actions.iter() {
|
||||
if matches!(act, Action::AdminAction(_)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn is_sts(&self) -> bool {
|
||||
for act in self.actions.iter() {
|
||||
if matches!(act, Action::StsAction(_)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_allowed(&self, args: &Args) -> bool {
|
||||
let check = 'c: {
|
||||
if (!self.actions.is_match(&args.action) && !self.actions.is_empty()) || self.not_actions.is_match(&args.action) {
|
||||
break 'c false;
|
||||
}
|
||||
|
||||
let mut resource = String::from(args.bucket);
|
||||
if !args.object.is_empty() {
|
||||
if !args.object.starts_with('/') {
|
||||
resource.push('/');
|
||||
}
|
||||
|
||||
resource.push_str(args.object);
|
||||
} else {
|
||||
resource.push('/');
|
||||
}
|
||||
|
||||
if self.is_kms() && (resource == "/" || self.resources.is_empty()) {
|
||||
break 'c self.conditions.evaluate(args.conditions);
|
||||
}
|
||||
|
||||
if !self.resources.is_match(&resource, args.conditions) && !self.is_admin() && !self.is_sts() {
|
||||
break 'c false;
|
||||
}
|
||||
|
||||
self.conditions.evaluate(args.conditions)
|
||||
};
|
||||
|
||||
self.effect.is_allowed(check)
|
||||
}
|
||||
}
|
||||
|
||||
impl Validator for Statement {
|
||||
type Error = Error;
|
||||
fn is_valid(&self) -> Result<()> {
|
||||
self.effect.is_valid()?;
|
||||
// check sid
|
||||
self.sid.is_valid()?;
|
||||
|
||||
if self.actions.is_empty() && self.not_actions.is_empty() {
|
||||
return Err(IamError::NonAction.into());
|
||||
}
|
||||
|
||||
if self.resources.is_empty() {
|
||||
return Err(IamError::NonResource.into());
|
||||
}
|
||||
|
||||
self.actions.is_valid()?;
|
||||
self.not_actions.is_valid()?;
|
||||
self.resources.is_valid()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Statement {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.effect == other.effect
|
||||
&& self.actions == other.actions
|
||||
&& self.not_actions == other.not_actions
|
||||
&& self.resources == other.resources
|
||||
&& self.conditions == other.conditions
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
pub mod path;
|
||||
pub mod wildcard;
|
||||
|
||||
pub fn _get_values_from_claims(claim: &HashMap<String, Value>, chaim_name: &str) -> (Vec<String>, bool) {
|
||||
let mut result = vec![];
|
||||
let Some(pname) = claim.get(chaim_name) else {
|
||||
return (result, false);
|
||||
};
|
||||
|
||||
let mut func = |pname_str: &str| {
|
||||
for s in pname_str.split(',').map(str::trim) {
|
||||
if s.is_empty() {
|
||||
continue;
|
||||
}
|
||||
result.push(s.to_owned());
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(arrays) = pname.as_array() {
|
||||
for array in arrays {
|
||||
let Some(pname_str) = array.as_str() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
func(pname_str);
|
||||
}
|
||||
} else {
|
||||
let Some(pname_str) = pname.as_str() else {
|
||||
return (result, false);
|
||||
};
|
||||
|
||||
func(pname_str);
|
||||
}
|
||||
|
||||
(result, true)
|
||||
}
|
||||
|
||||
pub fn _split_path(path: &str, second_index: bool) -> (&str, &str) {
|
||||
let index = if second_index {
|
||||
let Some(first) = path.find('/') else {
|
||||
return (path, "");
|
||||
};
|
||||
|
||||
let Some(second) = &(path[first + 1..]).find('/') else {
|
||||
return (path, "");
|
||||
};
|
||||
|
||||
Some(first + second + 1)
|
||||
} else {
|
||||
path.find('/')
|
||||
};
|
||||
|
||||
let Some(index) = index else {
|
||||
return (path, "");
|
||||
};
|
||||
|
||||
(&path[..index + 1], &path[index + 1..])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::_split_path;
|
||||
|
||||
#[test_case::test_case("format.json", false => ("format.json", ""))]
|
||||
#[test_case::test_case("users/tester.json", false => ("users/", "tester.json"))]
|
||||
#[test_case::test_case("groups/test/group.json", false => ("groups/", "test/group.json"))]
|
||||
#[test_case::test_case("policydb/groups/testgroup.json", true => ("policydb/groups/", "testgroup.json"))]
|
||||
#[test_case::test_case(
|
||||
"policydb/sts-users/uid=slash/user,ou=people,ou=swengg,dc=min,dc=io.json", true =>
|
||||
("policydb/sts-users/", "uid=slash/user,ou=people,ou=swengg,dc=min,dc=io.json"))
|
||||
]
|
||||
#[test_case::test_case(
|
||||
"policydb/sts-users/uid=slash/user/twice,ou=people,ou=swengg,dc=min,dc=io.json", true =>
|
||||
("policydb/sts-users/", "uid=slash/user/twice,ou=people,ou=swengg,dc=min,dc=io.json"))
|
||||
]
|
||||
#[test_case::test_case(
|
||||
"policydb/groups/cn=project/d,ou=groups,ou=swengg,dc=min,dc=io.json", true =>
|
||||
("policydb/groups/", "cn=project/d,ou=groups,ou=swengg,dc=min,dc=io.json"))
|
||||
]
|
||||
fn test_split_path(path: &str, second_index: bool) -> (&str, &str) {
|
||||
_split_path(path, second_index)
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
struct LazyBuf<'a> {
|
||||
s: &'a str,
|
||||
buf: Option<Vec<u8>>,
|
||||
w: usize,
|
||||
}
|
||||
|
||||
impl<'a> LazyBuf<'a> {
|
||||
pub fn new(s: &'a str) -> Self {
|
||||
Self { s, buf: None, w: 0 }
|
||||
}
|
||||
|
||||
fn index(&self, i: usize) -> u8 {
|
||||
self.buf.as_ref().map(|x| x[i]).unwrap_or_else(|| self.s.as_bytes()[i])
|
||||
}
|
||||
|
||||
fn append(&mut self, c: u8) {
|
||||
if self.buf.is_none() {
|
||||
if self.w < self.s.len() && self.s.as_bytes()[self.w] == c {
|
||||
self.w += 1;
|
||||
return;
|
||||
}
|
||||
self.buf = Some({
|
||||
let mut buf = vec![0u8; self.s.len()];
|
||||
buf[..self.w].copy_from_slice(&self.s.as_bytes()[..self.w]);
|
||||
buf
|
||||
});
|
||||
}
|
||||
|
||||
self.buf.as_mut().unwrap()[self.w] = c;
|
||||
self.w += 1;
|
||||
}
|
||||
|
||||
fn string(&self) -> String {
|
||||
match self.buf {
|
||||
Some(ref s) => String::from_utf8_lossy(&s[..self.w]).to_string(),
|
||||
None => String::from_utf8_lossy(&self.s.as_bytes()[..self.w]).to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// copy from golang(path.Clean)
|
||||
pub fn clean(path: &str) -> String {
|
||||
if path.is_empty() {
|
||||
return ".".into();
|
||||
}
|
||||
|
||||
let p = path.as_bytes();
|
||||
let (rooted, n, mut out, mut r, mut dotdot) = (p[0] == b'/', path.len(), LazyBuf::new(path), 0, 0);
|
||||
|
||||
if rooted {
|
||||
out.append(b'/');
|
||||
r = 1;
|
||||
dotdot = 1;
|
||||
}
|
||||
|
||||
while r < n {
|
||||
if p[r] == b'/' || (p[r] == b'.' && (r + 1 == n || p[r + 1] == b'/')) {
|
||||
r += 1;
|
||||
} else if p[r] == b'.' && p[r + 1] == b'.' && (r + 2 == n || p[r + 2] == b'/') {
|
||||
r += 2;
|
||||
if out.w > dotdot {
|
||||
out.w -= 1;
|
||||
|
||||
while out.w > dotdot && out.index(out.w) != b'/' {
|
||||
out.w -= 1;
|
||||
}
|
||||
} else if !rooted {
|
||||
if out.w > 0 {
|
||||
out.append(b'/');
|
||||
}
|
||||
|
||||
out.append(b'.');
|
||||
out.append(b'.');
|
||||
dotdot = out.w;
|
||||
}
|
||||
} else {
|
||||
if rooted && out.w != 1 || !rooted && out.w != 0 {
|
||||
out.append(b'/');
|
||||
}
|
||||
|
||||
while r < n && p[r] != b'/' {
|
||||
out.append(p[r]);
|
||||
r += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if out.w == 0 {
|
||||
".".into()
|
||||
} else {
|
||||
out.string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::clean;
|
||||
|
||||
#[test_case::test_case("", "."; "1")]
|
||||
#[test_case::test_case("abc", "abc"; "2")]
|
||||
#[test_case::test_case("abc/def", "abc/def"; "3")]
|
||||
#[test_case::test_case("a/b/c", "a/b/c"; "4")]
|
||||
#[test_case::test_case(".", "."; "5")]
|
||||
#[test_case::test_case("..", ".."; "6")]
|
||||
#[test_case::test_case("../..", "../.."; "7")]
|
||||
#[test_case::test_case("../../abc", "../../abc"; "8")]
|
||||
#[test_case::test_case("/abc", "/abc"; "9")]
|
||||
#[test_case::test_case("/", "/"; "10")]
|
||||
#[test_case::test_case("abc/", "abc"; "11")]
|
||||
#[test_case::test_case("abc/def/", "abc/def"; "12")]
|
||||
#[test_case::test_case("a/b/c/", "a/b/c"; "13")]
|
||||
#[test_case::test_case("./", "."; "14")]
|
||||
#[test_case::test_case("../", ".."; "15")]
|
||||
#[test_case::test_case("../../", "../.."; "16")]
|
||||
#[test_case::test_case("/abc/", "/abc"; "17")]
|
||||
#[test_case::test_case("abc//def//ghi", "abc/def/ghi"; "18")]
|
||||
#[test_case::test_case("//abc", "/abc"; "19")]
|
||||
#[test_case::test_case("///abc", "/abc"; "20")]
|
||||
#[test_case::test_case("//abc//", "/abc"; "21")]
|
||||
#[test_case::test_case("abc//", "abc"; "22")]
|
||||
#[test_case::test_case("abc/./def", "abc/def"; "23")]
|
||||
#[test_case::test_case("/./abc/def", "/abc/def"; "24")]
|
||||
#[test_case::test_case("abc/.", "abc"; "25")]
|
||||
#[test_case::test_case("abc/def/ghi/../jkl", "abc/def/jkl"; "26")]
|
||||
#[test_case::test_case("abc/def/../ghi/../jkl", "abc/jkl"; "27")]
|
||||
#[test_case::test_case("abc/def/..", "abc"; "28")]
|
||||
#[test_case::test_case("abc/def/../..", "."; "29")]
|
||||
#[test_case::test_case("/abc/def/../..", "/"; "30")]
|
||||
#[test_case::test_case("abc/def/../../..", ".."; "31")]
|
||||
#[test_case::test_case("/abc/def/../../..", "/"; "32")]
|
||||
#[test_case::test_case("abc/def/../../../ghi/jkl/../../../mno", "../../mno"; "33")]
|
||||
#[test_case::test_case("abc/./../def", "def"; "34")]
|
||||
#[test_case::test_case("abc//./../def", "def"; "35")]
|
||||
#[test_case::test_case("abc/../../././../def", "../../def"; "36")]
|
||||
fn test_clean(path: &str, result: &str) {
|
||||
assert_eq!(clean(path), result.to_owned());
|
||||
assert_eq!(clean(result), result.to_owned());
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
#[allow(dead_code)]
|
||||
pub fn is_simple_match<P, N>(pattern: P, name: N) -> bool
|
||||
where
|
||||
P: AsRef<str>,
|
||||
N: AsRef<str>,
|
||||
{
|
||||
inner_match(pattern, name, true)
|
||||
}
|
||||
|
||||
pub fn is_match<P, N>(pattern: P, name: N) -> bool
|
||||
where
|
||||
P: AsRef<str>,
|
||||
N: AsRef<str>,
|
||||
{
|
||||
inner_match(pattern, name, false)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_match_as_pattern_prefix<P, N>(pattern: P, text: N) -> bool
|
||||
where
|
||||
P: AsRef<str>,
|
||||
N: AsRef<str>,
|
||||
{
|
||||
let (mut p, mut t) = (pattern.as_ref().as_bytes().iter(), text.as_ref().as_bytes().iter());
|
||||
|
||||
while let (Some(&x), Some(&y)) = (p.next(), t.next()) {
|
||||
if x == b'*' {
|
||||
return true;
|
||||
}
|
||||
|
||||
if x == b'?' {
|
||||
continue;
|
||||
}
|
||||
|
||||
if x != y {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
text.as_ref().len() <= pattern.as_ref().len()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn inner_match(pattern: impl AsRef<str>, name: impl AsRef<str>, simple: bool) -> bool {
|
||||
let (pattern, name) = (pattern.as_ref(), name.as_ref());
|
||||
|
||||
if pattern.is_empty() {
|
||||
return pattern == name;
|
||||
}
|
||||
|
||||
if pattern == "*" {
|
||||
return true;
|
||||
}
|
||||
|
||||
deep_match(pattern.as_bytes(), name.as_bytes(), simple)
|
||||
}
|
||||
|
||||
fn deep_match(mut pattern: &[u8], mut name: &[u8], simple: bool) -> bool {
|
||||
while !pattern.is_empty() {
|
||||
match pattern[0] {
|
||||
b'?' => {
|
||||
if name.is_empty() {
|
||||
return simple;
|
||||
}
|
||||
}
|
||||
|
||||
b'*' => {
|
||||
return pattern.len() == 1
|
||||
|| deep_match(&pattern[1..], name, simple)
|
||||
|| (!name.is_empty() && deep_match(pattern, &name[1..], simple));
|
||||
}
|
||||
|
||||
_ => {
|
||||
if name.is_empty() || name[0] != pattern[0] {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
name = &name[1..];
|
||||
pattern = &pattern[1..];
|
||||
}
|
||||
|
||||
name.is_empty() && pattern.is_empty()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{is_match, is_match_as_pattern_prefix, is_simple_match};
|
||||
|
||||
#[test_case::test_case("*", "s3:GetObject" => true ; "1")]
|
||||
#[test_case::test_case("", "s3:GetObject" => false ; "2")]
|
||||
#[test_case::test_case("", "" => true; "3")]
|
||||
#[test_case::test_case("s3:*", "s3:ListMultipartUploadParts" => true; "4")]
|
||||
#[test_case::test_case("s3:ListBucketMultipartUploads", "s3:ListBucket" => false; "5")]
|
||||
#[test_case::test_case("s3:ListBucket", "s3:ListBucket" => true; "6")]
|
||||
#[test_case::test_case("s3:ListBucketMultipartUploads", "s3:ListBucketMultipartUploads" => true; "7")]
|
||||
#[test_case::test_case("my-bucket/oo*", "my-bucket/oo" => true; "8")]
|
||||
#[test_case::test_case("my-bucket/In*", "my-bucket/India/Karnataka/" => true; "9")]
|
||||
#[test_case::test_case("my-bucket/In*", "my-bucket/Karnataka/India/" => false; "10")]
|
||||
#[test_case::test_case("my-bucket/In*/Ka*/Ban", "my-bucket/India/Karnataka/Ban" => true; "11")]
|
||||
#[test_case::test_case("my-bucket/In*/Ka*/Ban", "my-bucket/India/Karnataka/Ban/Ban/Ban/Ban/Ban" => true; "12")]
|
||||
#[test_case::test_case("my-bucket/In*/Ka*/Ban", "my-bucket/India/Karnataka/Area1/Area2/Area3/Ban" => true; "13")]
|
||||
#[test_case::test_case( "my-bucket/In*/Ka*/Ba", "my-bucket/India/State1/State2/Karnataka/Area1/Area2/Area3/Ban" => ignore["will fail"] true; "14")]
|
||||
#[test_case::test_case("my-bucket/In*/Ka*/Ban", "my-bucket/India/Karnataka/Bangalore" => false; "15")]
|
||||
#[test_case::test_case("my-bucket/In*/Ka*/Ban*", "my-bucket/India/Karnataka/Bangalore" => true; "16")]
|
||||
#[test_case::test_case("my-bucket/*", "my-bucket/India" => true; "17")]
|
||||
#[test_case::test_case("my-bucket/oo*", "my-bucket/odo" => false; "18")]
|
||||
#[test_case::test_case("my-bucket?/abc*", "mybucket/abc" => false; "19")]
|
||||
#[test_case::test_case("my-bucket?/abc*", "my-bucket1/abc" => true; "20")]
|
||||
#[test_case::test_case("my-?-bucket/abc*", "my--bucket/abc" => false; "21")]
|
||||
#[test_case::test_case("my-?-bucket/abc*", "my-1-bucket/abc" => true; "22")]
|
||||
#[test_case::test_case("my-?-bucket/abc*", "my-k-bucket/abc" => true; "23")]
|
||||
#[test_case::test_case("my??bucket/abc*", "mybucket/abc" => false; "24")]
|
||||
#[test_case::test_case("my??bucket/abc*", "my4abucket/abc" => true; "25")]
|
||||
#[test_case::test_case("my-bucket?abc*", "my-bucket/abc" => true; "26")]
|
||||
#[test_case::test_case("my-bucket/abc?efg", "my-bucket/abcdefg" => true; "27")]
|
||||
#[test_case::test_case("my-bucket/abc?efg", "my-bucket/abc/efg" => true; "28")]
|
||||
#[test_case::test_case("my-bucket/abc????", "my-bucket/abcde" => false; "29")]
|
||||
#[test_case::test_case("my-bucket/abc????", "my-bucket/abcdefg" => true; "30")]
|
||||
#[test_case::test_case("my-bucket/abc?", "my-bucket/abc" => false; "31")]
|
||||
#[test_case::test_case("my-bucket/abc?", "my-bucket/abcd" => true; "32")]
|
||||
#[test_case::test_case("my-bucket/abc?", "my-bucket/abcde" => false; "33")]
|
||||
#[test_case::test_case("my-bucket/mnop*?", "my-bucket/mnop" => false; "34")]
|
||||
#[test_case::test_case("my-bucket/mnop*?", "my-bucket/mnopqrst/mnopqr" => true; "35")]
|
||||
#[test_case::test_case("my-bucket/mnop*?", "my-bucket/mnopqrst/mnopqrs" => true; "36")]
|
||||
#[test_case::test_case("my-bucket/mnop*?", "my-bucket/mnop" => false; "37")]
|
||||
#[test_case::test_case("my-bucket/mnop*?", "my-bucket/mnopq" => true; "38")]
|
||||
#[test_case::test_case("my-bucket/mnop*?", "my-bucket/mnopqr" => true; "39")]
|
||||
#[test_case::test_case("my-bucket/mnop*?and", "my-bucket/mnopqand" => true; "40")]
|
||||
#[test_case::test_case("my-bucket/mnop*?and", "my-bucket/mnopand" => false; "41")]
|
||||
#[test_case::test_case("my-bucket/mnop*?and", "my-bucket/mnopqand" => true; "42")]
|
||||
#[test_case::test_case("my-bucket/mnop*?", "my-bucket/mn" => false; "43")]
|
||||
#[test_case::test_case("my-bucket/mnop*?", "my-bucket/mnopqrst/mnopqrs" => true; "44")]
|
||||
#[test_case::test_case("my-bucket/mnop*??", "my-bucket/mnopqrst" => true; "45")]
|
||||
#[test_case::test_case("my-bucket/mnop*qrst", "my-bucket/mnopabcdegqrst" => true; "46")]
|
||||
#[test_case::test_case("my-bucket/mnop*?and", "my-bucket/mnopqand" => true; "47")]
|
||||
#[test_case::test_case("my-bucket/mnop*?and", "my-bucket/mnopand" => false; "48")]
|
||||
#[test_case::test_case("my-bucket/mnop*?and?", "my-bucket/mnopqanda" => true; "49")]
|
||||
#[test_case::test_case("my-bucket/mnop*?and", "my-bucket/mnopqanda" => false; "50")]
|
||||
#[test_case::test_case("my-?-bucket/abc*", "my-bucket/mnopqanda" => false; "51")]
|
||||
#[test_case::test_case("a?", "a" => false; "52")]
|
||||
#[test_case::test_case("*", "mybucket/myobject" => true; "53")]
|
||||
fn test_is_match(pattern: &str, text: &str) -> bool {
|
||||
is_match(pattern, text)
|
||||
}
|
||||
|
||||
#[test_case::test_case("*", "s3:GetObject" => true ; "1")]
|
||||
#[test_case::test_case("", "s3:GetObject" => false ; "2")]
|
||||
#[test_case::test_case("", "" => true ; "3")]
|
||||
#[test_case::test_case("s3:*", "s3:ListMultipartUploadParts" => true ; "4")]
|
||||
#[test_case::test_case("s3:ListBucketMultipartUploads", "s3:ListBucket" => false ; "5")]
|
||||
#[test_case::test_case("s3:ListBucket", "s3:ListBucket" => true ; "6")]
|
||||
#[test_case::test_case("s3:ListBucketMultipartUploads", "s3:ListBucketMultipartUploads" => true ; "7")]
|
||||
#[test_case::test_case("my-bucket/oo*", "my-bucket/oo" => true ; "8")]
|
||||
#[test_case::test_case("my-bucket/In*", "my-bucket/India/Karnataka/" => true ; "9")]
|
||||
#[test_case::test_case("my-bucket/In*", "my-bucket/Karnataka/India/" => false ; "10")]
|
||||
#[test_case::test_case("my-bucket/In*/Ka*/Ban", "my-bucket/India/Karnataka/Ban" => true ; "11")]
|
||||
#[test_case::test_case("my-bucket/In*/Ka*/Ban", "my-bucket/India/Karnataka/Ban/Ban/Ban/Ban/Ban" => true ; "12")]
|
||||
#[test_case::test_case("my-bucket/In*/Ka*/Ban", "my-bucket/India/Karnataka/Area1/Area2/Area3/Ban" => true ; "13")]
|
||||
#[test_case::test_case("my-bucket/In*/Ka*/Ban", "my-bucket/India/State1/State2/Karnataka/Area1/Area2/Area3/Ban" => true ; "14")]
|
||||
#[test_case::test_case("my-bucket/In*/Ka*/Ban", "my-bucket/India/Karnataka/Bangalore" => false ; "15")]
|
||||
#[test_case::test_case("my-bucket/In*/Ka*/Ban*", "my-bucket/India/Karnataka/Bangalore" => true ; "16")]
|
||||
#[test_case::test_case("my-bucket/*", "my-bucket/India" => true ; "17")]
|
||||
#[test_case::test_case("my-bucket/oo*", "my-bucket/odo" => false ; "18")]
|
||||
#[test_case::test_case("my-bucket/oo?*", "my-bucket/oo???" => true ; "19")]
|
||||
#[test_case::test_case("my-bucket/oo??*", "my-bucket/odo" => false ; "20")]
|
||||
#[test_case::test_case("?h?*", "?h?hello" => true ; "21")]
|
||||
#[test_case::test_case("a?", "a" => true ; "22")]
|
||||
fn test_is_simple_match(pattern: &str, text: &str) -> bool {
|
||||
is_simple_match(pattern, text)
|
||||
}
|
||||
|
||||
#[test_case::test_case("", "" => true ; "1")]
|
||||
#[test_case::test_case("a", "" => true ; "2")]
|
||||
#[test_case::test_case("a", "b" => false ; "3")]
|
||||
#[test_case::test_case("abc", "ab" => true ; "4")]
|
||||
#[test_case::test_case("ab*", "ab" => true ; "5")]
|
||||
#[test_case::test_case("abc*", "ab" => true ; "6")]
|
||||
#[test_case::test_case("abc?", "ab" => true ; "7")]
|
||||
#[test_case::test_case("abc*", "abd" => false ; "8")]
|
||||
#[test_case::test_case("abc*c", "abcd" => true ; "9")]
|
||||
#[test_case::test_case("ab*??d", "abxxc" => true ; "10")]
|
||||
#[test_case::test_case("ab*??", "abxc" => true ; "11")]
|
||||
#[test_case::test_case("ab??", "abxc" => true ; "12")]
|
||||
#[test_case::test_case("ab??", "abx" => true ; "13")]
|
||||
#[test_case::test_case("ab??d", "abcxd" => true ; "14")]
|
||||
#[test_case::test_case("ab??d", "abcxdd" => false ; "15")]
|
||||
#[test_case::test_case("", "b" => false ; "16")]
|
||||
fn test_is_match_as_pattern_prefix(pattern: &str, text: &str) -> bool {
|
||||
is_match_as_pattern_prefix(pattern, text)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
use crate::error::Error;
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub enum ServiceType {
|
||||
S3,
|
||||
STS,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ServiceType {
|
||||
type Error = Error;
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
let service_type = match value {
|
||||
"s3" => Self::S3,
|
||||
"sts" => Self::STS,
|
||||
_ => return Err(Error::InvalidServiceType(value.to_owned())),
|
||||
};
|
||||
|
||||
Ok(service_type)
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
pub mod object;
|
||||
|
||||
use crate::{auth::UserIdentity, cache::Cache, policy::PolicyDoc};
|
||||
use ecstore::error::Result;
|
||||
use crate::cache::Cache;
|
||||
use common::error::Result;
|
||||
use policy::{auth::UserIdentity, policy::PolicyDoc};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
use super::{GroupInfo, MappedPolicy, Store, UserType};
|
||||
use crate::{
|
||||
auth::UserIdentity,
|
||||
cache::{Cache, CacheEntity},
|
||||
error::{is_err_no_such_policy, is_err_no_such_user},
|
||||
get_global_action_cred,
|
||||
manager::{extract_jwt_claims, get_default_policyes},
|
||||
policy::PolicyDoc,
|
||||
};
|
||||
use common::error::{Error, Result};
|
||||
use ecstore::{
|
||||
config::{
|
||||
common::{delete_config, read_config, read_config_with_metadata, save_config},
|
||||
com::{delete_config, read_config, read_config_with_metadata, save_config},
|
||||
error::is_err_config_not_found,
|
||||
RUSTFS_CONFIG_PREFIX,
|
||||
},
|
||||
error::{Error, Result},
|
||||
store::ECStore,
|
||||
store_api::{ObjectInfo, ObjectOptions},
|
||||
store_list_objects::{ObjectInfoOrErr, WalkOptions},
|
||||
@@ -21,6 +19,7 @@ use ecstore::{
|
||||
};
|
||||
use futures::future::join_all;
|
||||
use lazy_static::lazy_static;
|
||||
use policy::{auth::UserIdentity, policy::PolicyDoc};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tokio::sync::broadcast::{self, Receiver as B_Receiver};
|
||||
|
||||
+18
-90
@@ -1,16 +1,3 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::arn::ARN;
|
||||
use crate::auth::contains_reserved_chars;
|
||||
use crate::auth::create_new_credentials_with_metadata;
|
||||
use crate::auth::generate_credentials;
|
||||
use crate::auth::is_access_key_valid;
|
||||
use crate::auth::is_secret_key_valid;
|
||||
use crate::auth::Credentials;
|
||||
use crate::auth::UserIdentity;
|
||||
use crate::auth::ACCOUNT_ON;
|
||||
use crate::error::is_err_no_such_account;
|
||||
use crate::error::is_err_no_such_temp_account;
|
||||
use crate::error::Error as IamError;
|
||||
@@ -18,19 +5,33 @@ use crate::get_global_action_cred;
|
||||
use crate::manager::extract_jwt_claims;
|
||||
use crate::manager::get_default_policyes;
|
||||
use crate::manager::IamCache;
|
||||
use crate::policy::action::Action;
|
||||
use crate::policy::Policy;
|
||||
use crate::policy::PolicyDoc;
|
||||
use crate::store::MappedPolicy;
|
||||
use crate::store::Store;
|
||||
use crate::store::UserType;
|
||||
use ecstore::error::{Error, Result};
|
||||
use common::error::{Error, Result};
|
||||
use ecstore::utils::crypto::base64_decode;
|
||||
use ecstore::utils::crypto::base64_encode;
|
||||
use madmin::AddOrUpdateUserReq;
|
||||
use madmin::GroupDesc;
|
||||
use policy::arn::ARN;
|
||||
use policy::auth::contains_reserved_chars;
|
||||
use policy::auth::create_new_credentials_with_metadata;
|
||||
use policy::auth::generate_credentials;
|
||||
use policy::auth::is_access_key_valid;
|
||||
use policy::auth::is_secret_key_valid;
|
||||
use policy::auth::Credentials;
|
||||
use policy::auth::UserIdentity;
|
||||
use policy::auth::ACCOUNT_ON;
|
||||
use policy::policy::iam_policy_claim_name_sa;
|
||||
use policy::policy::Args;
|
||||
use policy::policy::Policy;
|
||||
use policy::policy::PolicyDoc;
|
||||
use policy::policy::EMBEDDED_POLICY_TYPE;
|
||||
use policy::policy::INHERITED_POLICY_TYPE;
|
||||
use serde_json::json;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
pub const MAX_SVCSESSION_POLICY_SIZE: usize = 4096;
|
||||
@@ -42,9 +43,6 @@ pub const POLICYNAME: &str = "policy";
|
||||
pub const SESSION_POLICY_NAME: &str = "sessionPolicy";
|
||||
pub const SESSION_POLICY_NAME_EXTRACTED: &str = "sessionPolicy-extracted";
|
||||
|
||||
pub const EMBEDDED_POLICY_TYPE: &str = "embedded-policy";
|
||||
pub const INHERITED_POLICY_TYPE: &str = "inherited-policy";
|
||||
|
||||
pub struct IamSys<T> {
|
||||
store: Arc<IamCache<T>>,
|
||||
roles_map: HashMap<ARN, String>,
|
||||
@@ -697,73 +695,3 @@ pub struct UpdateServiceAccountOpts {
|
||||
pub expiration: Option<OffsetDateTime>,
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
pub fn iam_policy_claim_name_sa() -> String {
|
||||
"sa-policy".to_string()
|
||||
}
|
||||
|
||||
/// DEFAULT_VERSION is the default version.
|
||||
/// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_version.html
|
||||
pub const DEFAULT_VERSION: &str = "2012-10-17";
|
||||
|
||||
/// check the data is Validator
|
||||
pub trait Validator {
|
||||
type Error;
|
||||
fn is_valid(&self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Args<'a> {
|
||||
pub account: &'a str,
|
||||
pub groups: &'a Option<Vec<String>>,
|
||||
pub action: Action,
|
||||
pub bucket: &'a str,
|
||||
pub conditions: &'a HashMap<String, Vec<String>>,
|
||||
pub is_owner: bool,
|
||||
pub object: &'a str,
|
||||
pub claims: &'a HashMap<String, Value>,
|
||||
pub deny_only: bool,
|
||||
}
|
||||
|
||||
impl Args<'_> {
|
||||
pub fn get_role_arn(&self) -> Option<&str> {
|
||||
self.claims.get("roleArn").and_then(|x| x.as_str())
|
||||
}
|
||||
pub fn get_policies(&self, policy_claim_name: &str) -> (HashSet<String>, bool) {
|
||||
get_policies_from_claims(self.claims, policy_claim_name)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_values_from_claims(claims: &HashMap<String, Value>, claim_name: &str) -> (HashSet<String>, bool) {
|
||||
let mut s = HashSet::new();
|
||||
if let Some(pname) = claims.get(claim_name) {
|
||||
if let Some(pnames) = pname.as_array() {
|
||||
for pname in pnames {
|
||||
if let Some(pname_str) = pname.as_str() {
|
||||
for pname in pname_str.split(',') {
|
||||
let pname = pname.trim();
|
||||
if !pname.is_empty() {
|
||||
s.insert(pname.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (s, true);
|
||||
} else if let Some(pname_str) = pname.as_str() {
|
||||
for pname in pname_str.split(',') {
|
||||
let pname = pname.trim();
|
||||
if !pname.is_empty() {
|
||||
s.insert(pname.to_string());
|
||||
}
|
||||
}
|
||||
return (s, true);
|
||||
}
|
||||
}
|
||||
(s, false)
|
||||
}
|
||||
|
||||
fn get_policies_from_claims(claims: &HashMap<String, Value>, policy_claim_name: &str) -> (HashSet<String>, bool) {
|
||||
get_values_from_claims(claims, policy_claim_name)
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
use ecstore::error::{Error, Result};
|
||||
use common::error::{Error, Result};
|
||||
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header};
|
||||
use rand::{Rng, RngCore};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
Reference in New Issue
Block a user