add iam system

add iam store

feat: add crypto crate

introduce decrypt_data and encrypt_data functions

Signed-off-by: bestgopher <84328409@qq.com>
This commit is contained in:
bestgopher
2024-10-14 23:11:49 +08:00
parent 8519a596ec
commit bb2f765e5d
61 changed files with 5319 additions and 64 deletions
+18
View File
@@ -0,0 +1,18 @@
use std::str::FromStr;
#[derive(PartialEq, Eq, Hash)]
pub struct ARN {
partition: String,
service: String,
region: String,
resource_type: String,
resource_id: String,
}
impl FromStr for ARN {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
todo!()
}
}
+23
View File
@@ -0,0 +1,23 @@
mod credentials;
pub use credentials::Credentials;
pub use credentials::CredentialsBuilder;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
#[derive(Serialize, Deserialize, Clone)]
pub struct UserIdentity {
pub version: i64,
pub credentials: Credentials,
pub update_at: OffsetDateTime,
}
impl From<Credentials> for UserIdentity {
fn from(value: Credentials) -> Self {
UserIdentity {
version: 1,
credentials: value,
update_at: OffsetDateTime::now_utc(),
}
}
}
+372
View File
@@ -0,0 +1,372 @@
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::cell::LazyCell;
use std::collections::HashMap;
use std::env::var;
use time::format_description::BorrowedFormatItem;
use time::{Date, OffsetDateTime};
use crate::policy::{Policy, Validator};
use crate::service_type::ServiceType;
use crate::{utils, Error};
#[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::ErrCredMalformed);
};
if h != "Credential" {
return Err(Error::ErrCredMalformed);
}
let mut cred_elems = cred_elems.trim().rsplitn(5, '/');
let Some(request) = cred_elems.next() else {
return Err(Error::ErrCredMalformed);
};
let Some(service) = cred_elems.next() else {
return Err(Error::ErrCredMalformed);
};
let Some(region) = cred_elems.next() else {
return Err(Error::ErrCredMalformed);
};
let Some(date) = cred_elems.next() else {
return Err(Error::ErrCredMalformed);
};
let Some(ak) = cred_elems.next() else {
return Err(Error::ErrCredMalformed);
};
if ak.len() < 3 {
return Err(Error::ErrCredMalformed);
}
if request != "aws4_request" {
return Err(Error::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::ErrCredMalformed)?
},
region: region.to_owned(),
service: service.try_into()?,
request: request.to_owned(),
},
})
}
}
#[derive(Serialize, Deserialize, Clone, Default)]
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, Vec<String>>>,
pub name: Option<String>,
pub description: Option<String>,
}
impl Credentials {
pub fn new(elem: &str) -> crate::Result<Self> {
let header: CredentialHeader = elem.try_into()?;
Self::check_key_value(header)
}
pub fn check_key_value(header: CredentialHeader) -> crate::Result<Self> {
todo!()
}
pub fn is_expired(&self) -> bool {
self.expiration
.as_ref()
.map(|e| time::OffsetDateTime::now_utc() > *e)
.unwrap_or(false)
}
pub fn is_temp(&self) -> bool {
!self.session_token.is_empty() && !self.is_expired()
}
pub fn is_service_account(&self) -> bool {
const IAM_POLICY_CLAIM_NAME_SA: &str = "sa-policy";
self.claims
.as_ref()
.map(|x| {
x.get(IAM_POLICY_CLAIM_NAME_SA)
.map_or(false, |_| !self.parent_user.is_empty())
})
.unwrap_or_default()
}
pub fn is_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
}
}
#[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) -> crate::Result<Credentials> {
self.try_into()
}
}
impl TryFrom<CredentialsBuilder> for Credentials {
type Error = crate::Error;
fn try_from(mut value: CredentialsBuilder) -> Result<Self, Self::Error> {
if value.parent_user.is_empty() {
return Err(Error::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::StringError("Either ak or sk is empty".into()));
}
if value.parent_user == value.access_key.as_str() {
return Err(Error::InvalidArgument);
}
if value.access_key == "site-replicator-0" && !value.allow_site_replicator_account {
return Err(Error::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::InvalidArgument)?;
if policy_buf.len() > 4096 {
return Err(crate::Error::StringError("session policy is too large".into()));
}
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(|_| crate::Error::StringError("session policy is too large".into()))?;
cred.session_token = session_token;
// cred.expiration = Some(
// OffsetDateTime::from_unix_timestamp(
// claim
// .get("exp")
// .and_then(|x| x.as_i64())
// .ok_or(crate::Error::StringError("invalid exp".into()))?,
// )
// .map_err(|_| crate::Error::StringError("invalie timestamp".into()))?,
// );
} else {
// cred.expiration =
// Some(OffsetDateTime::from_unix_timestamp(0).map_err(|_| crate::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_CredentialHeader_from_str_failed(input: &str) {
if CredentialHeader::try_from(input).is_ok() {
unreachable!()
}
}
}
+320
View File
@@ -0,0 +1,320 @@
use std::{
collections::{HashMap, HashSet},
ops::{Deref, DerefMut},
ptr,
sync::Arc,
};
use arc_swap::{ArcSwap, AsRaw, Guard};
use log::warn;
use time::OffsetDateTime;
use crate::{
auth::UserIdentity,
policy::{Args, MappedPolicy, Policy, PolicyDoc},
Error,
};
pub struct Cache {
pub policy_docs: ArcSwap<CacheEntity<PolicyDoc>>,
pub users: ArcSwap<CacheEntity<UserIdentity>>,
pub user_policies: ArcSwap<CacheEntity<MappedPolicy>>,
pub sts_accounts: ArcSwap<CacheEntity<UserIdentity>>,
pub sts_policies: ArcSwap<CacheEntity<MappedPolicy>>,
pub groups: ArcSwap<CacheEntity<String>>,
pub user_group_memeberships: ArcSwap<CacheEntity<HashSet<String>>>,
pub group_policies: ArcSwap<CacheEntity<MappedPolicy>>,
}
impl Default for Cache {
fn default() -> Self {
Self {
policy_docs: ArcSwap::new(Arc::new(CacheEntity::default())),
users: ArcSwap::new(Arc::new(CacheEntity::default())),
user_policies: ArcSwap::new(Arc::new(CacheEntity::default())),
sts_accounts: ArcSwap::new(Arc::new(CacheEntity::default())),
sts_policies: ArcSwap::new(Arc::new(CacheEntity::default())),
groups: ArcSwap::new(Arc::new(CacheEntity::default())),
user_group_memeberships: ArcSwap::new(Arc::new(CacheEntity::default())),
group_policies: ArcSwap::new(Arc::new(CacheEntity::default())),
}
}
}
impl Cache {
pub fn ptr_eq<Base, A, B>(a: A, b: B) -> bool
where
A: AsRaw<Base>,
B: AsRaw<Base>,
{
let a = a.as_raw();
let b = b.as_raw();
ptr::eq(a, b)
}
fn exec<T: Clone>(target: &ArcSwap<CacheEntity<T>>, t: OffsetDateTime, mut op: impl FnMut(&mut CacheEntity<T>)) {
let mut cur = target.load();
loop {
// 当前的更新时间晚于执行时间,说明后台任务加载完毕,不需要执行当前操作。
if cur.load_time >= t {
return;
}
let mut new = CacheEntity::clone(&cur);
op(&mut new);
// 使用cas原子替换内容
let prev = target.compare_and_swap(&*cur, Arc::new(new));
let swapped = Self::ptr_eq(&*cur, &*prev);
if swapped {
return;
} else {
cur = prev;
}
}
}
pub fn add_or_update<T: Clone>(target: &ArcSwap<CacheEntity<T>>, key: &str, value: &T, t: OffsetDateTime) {
Self::exec(target, t, |map: &mut CacheEntity<T>| {
map.insert(key.to_string(), value.clone());
})
}
pub fn delete<T: Clone>(target: &ArcSwap<CacheEntity<T>>, key: &str, t: OffsetDateTime) {
Self::exec(target, t, |map: &mut CacheEntity<T>| {
map.remove(key);
})
}
}
impl CacheInner {
#[inline]
fn get_user<'a>(&self, user_name: &'a str) -> Option<&UserIdentity> {
self.users.get(user_name).or_else(|| self.sts_accounts.get(user_name))
}
fn get_policy(&self, name: &str, groups: &[String]) -> crate::Result<Vec<Policy>> {
todo!()
}
/// 如果是临时用户,返回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 fn is_allowed_service_account(&self, args: &Args, parent: &str) -> bool {
warn!("unimplement is_allowed_sts");
false
}
pub fn is_allowed(&self, args: Args) -> bool {
todo!()
}
pub fn policy_db_get(&self, name: &str, groups: &[String]) -> Vec<String> {
todo!()
}
}
#[derive(Clone)]
pub struct CacheEntity<T> {
map: HashMap<String, T>,
/// 重新加载的时间
load_time: OffsetDateTime,
}
impl<T> Deref for CacheEntity<T> {
type Target = HashMap<String, T>;
fn deref(&self) -> &Self::Target {
&self.map
}
}
impl<T> DerefMut for CacheEntity<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.map
}
}
impl<T> CacheEntity<T> {
pub fn new(map: HashMap<String, T>) -> Self {
Self {
map,
load_time: OffsetDateTime::UNIX_EPOCH,
}
}
}
impl<T> Default for CacheEntity<T> {
fn default() -> Self {
Self {
map: HashMap::new(),
load_time: OffsetDateTime::UNIX_EPOCH,
}
}
}
impl<T> CacheEntity<T> {
pub fn update_load_time(mut self) -> Self {
self.load_time = OffsetDateTime::now_utc();
self
}
}
pub type G<T> = Guard<Arc<CacheEntity<T>>>;
pub struct CacheInner {
pub policy_docs: G<PolicyDoc>,
pub users: G<UserIdentity>,
pub user_policies: G<MappedPolicy>,
pub sts_accounts: G<UserIdentity>,
pub sts_policies: G<MappedPolicy>,
pub groups: G<String>,
pub user_group_memeberships: G<HashSet<String>>,
pub group_policies: G<MappedPolicy>,
}
impl From<&Cache> for CacheInner {
fn from(value: &Cache) -> Self {
Self {
policy_docs: value.policy_docs.load(),
users: value.users.load(),
user_policies: value.user_policies.load(),
sts_accounts: value.sts_accounts.load(),
sts_policies: value.sts_policies.load(),
groups: value.groups.load(),
user_group_memeberships: value.user_group_memeberships.load(),
group_policies: value.group_policies.load(),
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use arc_swap::ArcSwap;
use futures::future::join_all;
use time::OffsetDateTime;
use super::CacheEntity;
use crate::cache::Cache;
#[tokio::test]
async fn test_cache_entity_add() {
let cache = ArcSwap::new(Arc::new(CacheEntity::<usize>::default()));
let mut f = vec![];
for (index, key) in (0..100).map(|x| x.to_string()).enumerate() {
let c = &cache;
f.push(async move {
Cache::add_or_update(&c, &key, &index, OffsetDateTime::now_utc());
});
}
join_all(f).await;
let cache = cache.load();
for (index, key) in (0..100).map(|x| x.to_string()).enumerate() {
assert_eq!(cache.get(&key), Some(&index));
}
}
#[tokio::test]
async fn test_cache_entity_update() {
let cache = ArcSwap::new(Arc::new(CacheEntity::<usize>::default()));
let mut f = vec![];
for (index, key) in (0..100).map(|x| x.to_string()).enumerate() {
let c = &cache;
f.push(async move {
Cache::add_or_update(&c, &key, &index, OffsetDateTime::now_utc());
});
}
join_all(f).await;
let cache_load = cache.load();
for (index, key) in (0..100).map(|x| x.to_string()).enumerate() {
assert_eq!(cache_load.get(&key), Some(&index));
}
let mut f = vec![];
for (index, key) in (0..100).map(|x| x.to_string()).enumerate() {
let c = &cache;
f.push(async move {
Cache::add_or_update(&c, &key, &(index * 1000), OffsetDateTime::now_utc());
});
}
join_all(f).await;
let cache_load = cache.load();
for (index, key) in (0..100).map(|x| x.to_string()).enumerate() {
assert_eq!(cache_load.get(&key), Some(&(index * 1000)));
}
}
#[tokio::test]
async fn test_cache_entity_delete() {
let cache = ArcSwap::new(Arc::new(CacheEntity::<usize>::default()));
let mut f = vec![];
for (index, key) in (0..100).map(|x| x.to_string()).enumerate() {
let c = &cache;
f.push(async move {
Cache::add_or_update(&c, &key, &index, OffsetDateTime::now_utc());
});
}
join_all(f).await;
let cache_load = cache.load();
for (index, key) in (0..100).map(|x| x.to_string()).enumerate() {
assert_eq!(cache_load.get(&key), Some(&index));
}
let mut f = vec![];
for key in (0..100).map(|x| x.to_string()) {
let c = &cache;
f.push(async move {
Cache::delete(&c, &key, OffsetDateTime::now_utc());
});
}
join_all(f).await;
let cache_load = cache.load();
assert!(cache_load.is_empty());
}
}
+35
View File
@@ -0,0 +1,35 @@
use core::error;
use crate::policy;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
PolicyError(#[from] policy::Error),
#[error("ecsotre error: {0}")]
EcstoreError(ecstore::error::Error),
#[error("{0}")]
StringError(String),
#[error("crypto: {0}")]
CryptoError(#[from] crypto::Error),
#[error("user '{0}' does not exist")]
NoSuchUser(String),
#[error("invalid arguments specified")]
InvalidArgument,
#[error("not initialized")]
IamSysNotInitialized,
#[error("invalid service type: {0}")]
InvalidServiceType(String),
#[error("malformed credential")]
ErrCredMalformed,
}
pub type Result<T> = std::result::Result<T, Error>;
+17
View File
@@ -0,0 +1,17 @@
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
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,
}
}
}
+58
View File
@@ -0,0 +1,58 @@
use auth::{Credentials, UserIdentity};
use ecstore::store::ECStore;
use log::debug;
use manager::IamCache;
use policy::{Args, Policy};
use std::sync::{Arc, OnceLock};
use store::object::ObjectStore;
use time::OffsetDateTime;
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;
pub use error::{Error, Result};
static IAM_SYS: OnceLock<Arc<IamCache<ObjectStore>>> = OnceLock::new();
pub async fn init_iam_sys(ecstore: Arc<ECStore>) -> crate::Result<()> {
debug!("init iam system");
let s = IamCache::new(ObjectStore::new(ecstore)).await;
IAM_SYS.get_or_init(move || s);
Ok(())
}
#[inline]
pub fn get() -> crate::Result<Arc<IamCache<ObjectStore>>> {
IAM_SYS.get().map(|x| Arc::clone(x)).ok_or(Error::IamSysNotInitialized)
}
pub async fn is_allowed<'a>(args: Args<'a>) -> crate::Result<bool> {
Ok(get()?.is_allowed(args).await)
}
pub async fn get_service_account(ak: &str) -> crate::Result<(Credentials, Option<Policy>)> {
let (mut sa, policy) = get()?.get_service_account(ak).await?;
sa.credentials.secret_key.clear();
sa.credentials.access_key.clear();
Ok((sa.credentials, policy))
}
pub async fn add_service_account(cred: Credentials) -> crate::Result<OffsetDateTime> {
get()?.add_service_account(cred).await
}
pub async fn check_key(ak: &str) -> crate::Result<Option<UserIdentity>> {
get()?.check_key(ak).await
}
+223
View File
@@ -0,0 +1,223 @@
use std::{
collections::HashMap,
sync::{
atomic::{AtomicBool, AtomicI64, Ordering},
Arc,
},
time::Duration,
};
use log::debug;
use time::OffsetDateTime;
use tokio::{
select,
sync::{
mpsc,
mpsc::{Receiver, Sender},
},
};
use crate::{
arn::ARN,
auth::{Credentials, UserIdentity},
cache::Cache,
format::Format,
handler::Handler,
policy::{Args, Policy, UserType},
store::Store,
Error,
};
pub struct IamCache<T> {
pub cache: Cache,
pub api: T,
pub loading: Arc<AtomicBool>,
pub roles: HashMap<ARN, Vec<String>>,
pub send_chan: Sender<i64>,
pub last_timestamp: AtomicI64,
}
impl<T> IamCache<T>
where
T: Store,
{
pub(crate) async fn new(api: T) -> Arc<Self> {
let (sender, reciver) = mpsc::channel::<i64>(100);
let sys = Arc::new(Self {
api,
cache: Cache::default(),
loading: Arc::new(AtomicBool::new(false)),
send_chan: sender,
roles: HashMap::new(),
last_timestamp: AtomicI64::new(0),
});
sys.clone().init(reciver).await.unwrap();
sys
}
async fn init(self: Arc<Self>, reciver: Receiver<i64>) -> crate::Result<()> {
self.clone().save_iam_formatter().await?;
self.clone().load().await?;
// 后台线程开启定时更新或者接收到信号更新
tokio::spawn({
let s = Arc::clone(&self);
async move {
let ticker = tokio::time::interval(Duration::from_secs(120));
tokio::pin!(ticker, reciver);
loop {
select! {
_ = ticker.tick() => {
s.clone().load().await.unwrap();
},
i = reciver.recv() => {
match i {
Some(t) => {
let last = s.last_timestamp.load(Ordering::Relaxed);
if last <= t {
s.clone().load().await.unwrap();
ticker.reset();
}
},
None => return,
}
}
}
}
}
});
Ok(())
}
async fn notify(&self) {
self.send_chan.send(OffsetDateTime::now_utc().unix_timestamp()).await.unwrap();
}
async fn load(self: Arc<Self>) -> crate::Result<()> {
debug!("load iam to cache");
self.api.load_all(&self.cache).await?;
self.last_timestamp
.store(OffsetDateTime::now_utc().unix_timestamp(), Ordering::Relaxed);
Ok(())
}
pub async fn list_all_iam_config_items(&self) -> crate::Result<HashMap<String, Vec<String>>> {
todo!()
}
// todo, 判断是否存在,是否可以重试
async fn save_iam_formatter(self: Arc<Self>) -> crate::Result<()> {
match self.api.load_iam_config::<Format>(Format::PATH).await {
Ok((format, _)) if format.version >= 1 => return Ok(()),
Err(Error::EcstoreError(e)) if !ecstore::disk::error::is_err_file_not_found(&e) => {
return Err(Error::EcstoreError(e))
}
_ => {}
}
self.api.save_iam_config(Format::new(), Format::PATH).await?;
Ok(())
}
pub async fn list_service_accounts(&self, access_key: &str) -> crate::Result<Vec<Credentials>> {
let users = self.cache.users.load();
Ok(users
.values()
.filter_map(|x| {
if !access_key.is_empty() && x.credentials.parent_user.as_str() == access_key {
if x.credentials.is_service_account() {
let mut c = x.credentials.clone();
c.secret_key = String::new();
c.session_token = String::new();
return Some(c);
}
}
None
})
.collect())
}
/// create a service account and update cache
pub async fn add_service_account(&self, cred: Credentials) -> crate::Result<OffsetDateTime> {
if cred.parent_user.is_empty() {
return Err(Error::InvalidArgument);
}
if (cred.access_key.is_empty() && !cred.secret_key.is_empty())
|| (!cred.access_key.is_empty() && cred.secret_key.is_empty())
{
return Err(Error::StringError("Either ak or sk is empty".into()));
}
let users = self.cache.users.load();
if let Some(x) = users.get(&cred.access_key) {
if x.credentials.parent_user.as_str() != cred.parent_user.as_str() {
return Err(crate::Error::StringError("access key is taken by another user".into()));
}
return Err(crate::Error::StringError("access key already taken".into()));
}
if let Some(x) = users.get(&cred.parent_user) {
if x.credentials.is_service_account() {
return Err(crate::Error::StringError(
"unable to create a service account for another service account".into(),
));
}
}
let user_entiry = UserIdentity::from(cred);
let path = format!(
"config/iam/{}{}/identity.json",
UserType::Svc.prefix(),
user_entiry.credentials.access_key
);
debug!("save object: {path:?}");
self.api.save_iam_config(&user_entiry, path).await?;
Cache::add_or_update(
&self.cache.users,
&user_entiry.credentials.access_key,
&user_entiry,
OffsetDateTime::now_utc(),
);
Ok(user_entiry.update_at)
}
pub async fn is_allowed<'a>(&self, args: Args<'a>) -> bool {
let handler = Handler::new((&self.cache).into(), &self.api, &self.roles);
handler.is_allowed(args).await
}
pub async fn get_service_account(&self, ak: &str) -> crate::Result<(UserIdentity, Option<Policy>)> {
let user = self.cache.users.load();
let Some(u) = user.get(ak) else {
return Err(Error::StringError("no service account".into()));
};
// if !u.credentials.is_service_account() {
// return Err(Error::StringError("it is not service account".into()));
// }
Ok((u.clone(), None))
}
pub async fn check_key(&self, ak: &str) -> crate::Result<Option<UserIdentity>> {
let user = self
.cache
.users
.load()
.get(ak)
.cloned()
.or_else(|| self.cache.sts_accounts.load().get(ak).cloned());
match user {
Some(u) if u.credentials.is_valid() => Ok(Some(u)),
_ => Ok(None),
}
}
}
+120
View File
@@ -0,0 +1,120 @@
pub mod action;
mod doc;
mod effect;
mod function;
mod id;
mod policy;
mod resource;
mod statement;
pub(crate) mod utils;
use action::Action;
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;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub use statement::Statement;
use std::collections::HashMap;
use time::OffsetDateTime;
#[derive(Serialize, Deserialize, Clone)]
pub struct MappedPolicy {
pub version: i64,
pub policies: String,
pub update_at: OffsetDateTime,
}
impl MappedPolicy {
pub fn new(policy: &str) -> Self {
Self {
version: 1,
policies: policy.to_owned(),
update_at: OffsetDateTime::now_utc(),
}
}
}
pub struct GroupInfo {
version: i64,
status: String,
members: Vec<String>,
update_at: OffsetDateTime,
}
#[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),
}
/// 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 {
fn is_valid(&self) -> Result<(), Error> {
Ok(())
}
}
pub enum UserType {
Svc,
Sts,
Reg,
}
impl UserType {
pub fn prefix(&self) -> &'static str {
match self {
UserType::Svc => "service-accounts/",
UserType::Sts => "sts/",
UserType::Reg => "users/",
}
}
}
pub struct Args<'a> {
pub account: &'a str,
pub groups: &'a [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<'a> Args<'a> {
pub fn get_role_arn(&self) -> Option<&str> {
self.claims.get("roleArn").and_then(|x| x.as_str())
}
}
+143
View File
@@ -0,0 +1,143 @@
use std::{collections::HashSet, ops::Deref};
use serde::{Deserialize, Serialize};
use strum::{EnumString, IntoStaticStr};
use super::{utils::wildcard, Error, Validator};
#[derive(Serialize, Deserialize, Clone, Default)]
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 {
fn is_valid(&self) -> Result<(), super::Error> {
Ok(())
}
}
#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, IntoStaticStr)]
#[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 Action {
const S3_PREFIX: &str = "s3:";
const ADMIN_PREFIX: &str = "admin:";
const STS_PREFIX: &str = "sts:";
const KMS_PREFIX: &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(|_| Error::InvalidAction(value.into()))?))
} else if value.starts_with(Self::ADMIN_PREFIX) {
Ok(Self::AdminAction(
AdminAction::try_from(value).map_err(|_| Error::InvalidAction(value.into()))?,
))
} else if value.starts_with(Self::STS_PREFIX) {
Ok(Self::StsAction(
StsAction::try_from(value).map_err(|_| Error::InvalidAction(value.into()))?,
))
} else if value.starts_with(Self::KMS_PREFIX) {
Ok(Self::KmsAction(
KmsAction::try_from(value).map_err(|_| Error::InvalidAction(value.into()))?,
))
} else {
Err(Error::InvalidAction(value.into()))
}
}
}
#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, EnumString, IntoStaticStr)]
#[serde(try_from = "&str", into = "&str")]
pub enum S3Action {
#[strum(serialize = "s3:*")]
AllActions,
#[strum(serialize = "s3:GetBucketLocation")]
GetBucketLocationAction,
#[strum(serialize = "s3:GetObject")]
GetObjectAction,
#[strum(serialize = "s3:PutObject")]
PutObjectAction,
#[strum(serialize = "s3:GetObjectVersion")]
GetObjectVersionAction,
}
#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, EnumString, IntoStaticStr)]
#[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)]
#[serde(try_from = "&str", into = "&str")]
pub enum StsAction {}
#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, EnumString, IntoStaticStr)]
#[serde(try_from = "&str", into = "&str")]
pub enum KmsAction {
#[strum(serialize = "kms:*")]
AllActions,
}
+12
View File
@@ -0,0 +1,12 @@
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>,
}
+32
View File
@@ -0,0 +1,32 @@
use std::default;
use serde::{Deserialize, Serialize};
use strum::{EnumString, IntoStaticStr};
use super::{Error, Validator};
#[derive(Serialize, Clone, Deserialize, EnumString, IntoStaticStr, Default)]
#[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 {
fn is_valid(&self) -> Result<(), Error> {
Ok(())
}
}
+175
View File
@@ -0,0 +1,175 @@
use std::{collections::HashMap, ops::Deref};
use func::Func;
use key::Key;
use serde::{de, Deserialize, Serialize};
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, Serialize)]
pub struct Functions(pub Vec<Func>);
impl Functions {
pub fn evaluate(&self, values: &HashMap<String, Vec<String>>) -> bool {
self.0.iter().all(|x| x.evaluate(values))
}
}
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 inner_data = Vec::with_capacity(map.size_hint().unwrap_or(0));
while let Some(key) = map.next_key::<&str>()? {
let mut tokens = key.split(":");
let name = tokens.next();
let qualifier = tokens.next();
// 多个:
if tokens.next().is_some() {
return Err(A::Error::custom("invalid codition"));
}
let Some(name) = name else { return Err(A::Error::custom("invalid codition")) };
let f = match qualifier {
Some("ForAnyValues") => Func::ForAnyValues,
Some("ForAllValues") => Func::ForAllValues,
Some(q) => return Err(A::Error::custom(format!("invalid qualifier `{q}`"))),
None => Func::ForNormal,
};
// inner_data.push(f(name.try_into()?))
}
Ok(Functions(inner_data))
}
}
deserializer.deserialize_map(FuncVisitor)
}
}
impl Deref for Functions {
type Target = Vec<Func>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Value;
#[cfg(test)]
mod tests {
#[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"
}
}"# => true; "1")]
#[test_case::test_case(r#"{}"# => true; "2")]
#[test_case::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"
},
"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::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"
)]
fn test_serde(input: &str) -> bool {
true
}
}
+146
View File
@@ -0,0 +1,146 @@
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 {
let rvalues = values.get(self.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 self.values.0.iter() {
if ip_net.contains(ip) {
return true;
}
}
}
false
}
}
#[derive(Serialize, Clone)]
#[serde(transparent)]
#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
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::to_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::to_cidr::<A::Error>(v)?)
}
data
}))
}
}
impl AddrFuncValueVisitor {
fn to_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");
}
Ok(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::{
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 {
key: Key { name, variable },
values: AddrFuncValue(value.into_iter().map(|x| x.parse().unwrap()).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(())
}
}
+10
View File
@@ -0,0 +1,10 @@
use serde::{Deserialize, Serialize};
use super::func::InnerFunc;
pub type BinaryFunc = InnerFunc<BinaryFuncValue>;
// todo implement it
#[derive(Serialize, Deserialize, Clone)]
#[serde(transparent)]
pub struct BinaryFuncValue(String);
+119
View File
@@ -0,0 +1,119 @@
use super::func::InnerFunc;
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 {
match values.get(self.key.name().as_str()).and_then(|x| x.get(0)) {
Some(x) => self.values.0.to_string().as_str() == x,
None => false,
}
}
pub fn evaluate_null(&self, values: &HashMap<String, Vec<String>>) -> bool {
let len = values.get(self.key.name().as_str()).map(Vec::len).unwrap_or(0);
if self.values.0 {
return len == 0;
}
len != 0
}
}
#[derive(Clone)]
#[cfg_attr(test, derive(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: de::Error,
{
Ok(BoolFuncValue(value))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(BoolFuncValue(value.parse::<bool>().map_err(|e| E::custom(format!("{e:?}")))?))
}
}
deserializer.deserialize_any(BoolOrStringVisitor)
}
}
#[cfg(test)]
mod tests {
use super::{BoolFunc, BoolFuncValue};
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 {
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")]
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""#)]
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")]
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(())
}
}
+78
View File
@@ -0,0 +1,78 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use super::{addr::AddrFunc, binary::BinaryFunc, bool_null::BoolFunc, date::DateFunc, number::NumberFunc, string::StringFunc};
#[derive(Clone, Serialize, Deserialize)]
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 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, values),
StringNotEquals(s) => s.evaluate(for_all, false, false, values),
StringEqualsIgnoreCase(s) => s.evaluate(for_all, true, false, values),
StringNotEqualsIgnoreCase(s) => s.evaluate(for_all, true, false, values),
StringLike(s) => s.evaluate(for_all, false, true, values),
StringNotLike(s) => s.evaluate(for_all, false, true, values),
BinaryEquals(s) => todo!(),
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
}
}
pub fn is_negate(&self) -> bool {
use Condition::*;
matches!(self, StringNotEquals(_) | StringNotEqualsIgnoreCase(_) | NotIpAddress(_))
}
}
+107
View File
@@ -0,0 +1,107 @@
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 FnOnce(&OffsetDateTime, &OffsetDateTime) -> bool,
values: &HashMap<String, Vec<String>>,
) -> bool {
let v = match values.get(self.key.name().as_str()).and_then(|x| x.get(0)) {
Some(x) => x,
None => return false,
};
let Ok(rv) = OffsetDateTime::parse(v, &Rfc3339) else {
return false;
};
op(&self.values.0, &rv)
}
}
#[derive(Clone)]
#[cfg_attr(test, derive(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> de::Visitor<'de> 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::{
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 {
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(())
}
}
+91
View File
@@ -0,0 +1,91 @@
use std::{collections::HashMap, marker::PhantomData};
use serde::{
de::{self, Visitor},
Deserialize, Deserializer, Serialize,
};
use super::{condition::Condition, key::Key};
#[derive(Clone, Serialize, Deserialize)]
pub enum Func {
ForAnyValues(Vec<Condition>),
ForAllValues(Vec<Condition>),
ForNormal(Vec<Condition>),
}
impl Func {
pub fn evaluate(&self, values: &HashMap<String, Vec<String>>) -> bool {
match self {
Self::ForAnyValues(conditions) => conditions.iter().all(|x| x.evaluate(true, values)),
Self::ForAllValues(conditions) => conditions.iter().all(|x| x.evaluate(false, values)),
Self::ForNormal(conditions) => conditions.iter().all(|x| x.evaluate(false, values)),
}
}
}
#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
pub struct InnerFunc<T> {
pub key: Key,
pub values: T,
}
impl<T: Clone> Clone for InnerFunc<T> {
fn clone(&self) -> Self {
Self {
key: self.key.clone(),
values: self.values.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(1))?;
map.serialize_key(&self.key)?;
map.serialize_value(&self.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 Some((key, values)) = map.next_entry::<Key, T>()? else {
return Err(A::Error::custom("no k-v pair"));
};
Ok(InnerFunc { key, values })
}
}
deserializer.deserialize_map(FuncVisitor::<T>(PhantomData))
}
}
+110
View File
@@ -0,0 +1,110 @@
use serde::{Deserialize, Serialize};
use super::key_name::KeyName;
use crate::policy::{Error, Validator};
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(into = "String")]
#[serde(try_from = "&str")]
pub struct Key {
pub name: KeyName,
pub variable: Option<String>,
}
impl Validator for Key {}
impl Key {
pub fn is(&self, other: &KeyName) -> bool {
self.name.eq(other)
}
pub fn val_name(&self) -> String {
self.name.val_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 {
value.name()
}
}
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(|| Error::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(())
}
}
+333
View File
@@ -0,0 +1,333 @@
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: &[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::LDAPUser),
KeyName::Ldap(LdapKeyName::LDAPUsername),
KeyName::Ldap(LdapKeyName::LDAPGroups),
// 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 fn name(&self) -> &str {
match self {
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(),
}
}
pub fn val_name(&self) -> String {
match self {
KeyName::Aws(aws) => Into::<&str>::into(aws).to_owned(),
KeyName::Jwt(jwt) => Into::<&str>::into(jwt).to_owned(),
KeyName::Ldap(ldap) => Into::<&str>::into(ldap).to_owned(),
KeyName::Sts(sts) => Into::<&str>::into(sts).to_owned(),
KeyName::Svc(svc) => Into::<&str>::into(svc).to_owned(),
KeyName::S3(s3) => Into::<&str>::into(s3).to_owned(),
}
}
}
#[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,
}
#[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")]
LDAPUser,
#[strum(serialize = "ldap:username")]
LDAPUsername,
#[strum(serialize = "ldap:groups")]
LDAPGroups,
}
#[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::LDAPUser))]
#[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::LDAPUser))]
#[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::LDAPUser))]
#[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);
}
}
+113
View File
@@ -0,0 +1,113 @@
use std::collections::HashMap;
use super::func::InnerFunc;
use serde::{
de::{Error, Visitor},
Deserialize, Deserializer, Serialize,
};
pub type NumberFunc = InnerFunc<NumberFuncValue>;
#[derive(Clone)]
#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
pub struct NumberFuncValue(i64);
impl NumberFunc {
pub fn evaluate(&self, op: impl FnOnce(&i64, &i64) -> bool, if_exists: bool, values: &HashMap<String, Vec<String>>) -> bool {
let v = match values.get(self.key.name().as_str()).and_then(|x| x.get(0)) {
Some(x) => x,
None => return if_exists,
};
let Ok(rv) = v.parse::<i64>() else {
return false;
};
op(&rv, &self.values.0)
}
}
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<'de> Visitor<'de> 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_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: Error,
{
Ok(NumberFuncValue(value.parse().map_err(|e| E::custom(format!("{e:?}")))?))
}
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))
}
}
deserializer.deserialize_any(NumberVisitor)
}
}
#[cfg(test)]
mod tests {
use super::{NumberFunc, NumberFuncValue};
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 {
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(())
}
}
+220
View File
@@ -0,0 +1,220 @@
#[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 serde::{de, ser::SerializeSeq, Deserialize, Deserializer, Serialize};
use crate::policy::utils::wildcard;
use super::{func::InnerFunc, key_name::KeyName};
pub type StringFunc = InnerFunc<StringFuncValue>;
impl StringFunc {
fn eval(&self, for_all: bool, ignore_case: bool, values: &HashMap<String, Vec<String>>) -> bool {
let rvalues = values
.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.get(0)) {
Some(v) if !v.is_empty() => return Cow::Owned(c.to_mut().replace(key.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.get(0)) {
Some(v) if !v.is_empty() => return Cow::Owned(c.to_mut().replace(key.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
}
pub(crate) fn evaluate(&self, for_all: bool, ignore_case: bool, like: bool, values: &HashMap<String, Vec<String>>) -> bool {
if like {
self.eval_like(for_all, values)
} else {
self.eval(for_all, ignore_case, values)
}
}
}
/// 解析values字段
#[derive(Clone)]
#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
pub struct StringFuncValue(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(D::Error::custom("empty"));
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::{StringFunc, StringFuncValue};
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>, values: Vec<&str>) -> StringFunc {
StringFunc {
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(())
}
}
+29
View File
@@ -0,0 +1,29 @@
use std::ops::Deref;
use serde::{Deserialize, Serialize};
use super::{Error, Validator};
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct ID(pub String);
impl Validator for ID {
/// if id is a valid utf string, then it is valid.
fn is_valid(&self) -> Result<(), Error> {
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
}
}
+235
View File
@@ -0,0 +1,235 @@
use serde::{Deserialize, Serialize};
use super::{Args, Effect, Error, Statement, Validator, DEFAULT_VERSION, ID};
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Policy {
pub id: ID,
pub version: String,
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 false;
}
}
false
}
}
impl Validator for Policy {
fn is_valid(&self) -> Result<(), Error> {
if !self.id.is_empty() && !self.id.eq(DEFAULT_VERSION) {
return Err(Error::InvalidVersion(self.id.0.clone()));
}
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, DEFAULT_VERSION,
};
use super::Policy;
pub const 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()),
resoures: ResourceSet({
let mut hash_set = HashSet::new();
hash_set.insert(Resource::S3("*".into()));
hash_set
}),
conditions: Functions(vec![]),
}],
},
),
(
"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()),
resoures: ResourceSet({
let mut hash_set = HashSet::new();
hash_set.insert(Resource::S3("*".into()));
hash_set
}),
conditions: Functions(vec![]),
}],
},
),
(
"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()),
resoures: ResourceSet({
let mut hash_set = HashSet::new();
hash_set.insert(Resource::S3("*".into()));
hash_set
}),
conditions: Functions(vec![]),
}],
},
),
(
"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()),
resoures: ResourceSet({
let mut hash_set = HashSet::new();
hash_set.insert(Resource::S3("*".into()));
hash_set
}),
conditions: Functions(vec![]),
}],
},
),
(
"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()),
resoures: ResourceSet({
let mut hash_set = HashSet::new();
hash_set.insert(Resource::S3("*".into()));
hash_set
}),
conditions: Functions(vec![]),
}],
},
),
(
"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()),
resoures: ResourceSet(HashSet::new()),
conditions: Functions(vec![]),
},
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()),
resoures: ResourceSet(HashSet::new()),
conditions: Functions(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()),
resoures: ResourceSet({
let mut hash_set = HashSet::new();
hash_set.insert(Resource::S3("*".into()));
hash_set
}),
conditions: Functions(vec![]),
},
],
},
),
]
});
}
+118
View File
@@ -0,0 +1,118 @@
use std::{
collections::{HashMap, HashSet},
hash::Hash,
ops::Deref,
};
use serde::{Deserialize, Serialize};
use super::{
function::key_name::KeyName,
utils::{path, wildcard},
Error, Validator,
};
#[derive(Serialize, Deserialize, Clone, Default)]
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
}
}
impl Deref for ResourceSet {
type Target = HashSet<Resource>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Validator for ResourceSet {
fn is_valid(&self) -> Result<(), Error> {
for resource in self.0.iter() {
resource.is_valid()?;
}
Ok(())
}
}
#[derive(Hash, Eq, PartialEq, Serialize, Deserialize, Clone)]
pub enum Resource {
S3(String),
Kms(String),
}
impl Resource {
pub const S3_PREFIX: &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.name(), &rvalue[0]);
}
}
}
}
let cp = path::clean(resource);
if cp != "." && cp == pattern.as_str() {
return true;
}
wildcard::is_match(pattern, resource)
}
}
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[Self::S3_PREFIX.len() + 1..].into())
} else {
return Err(Error::InvalidResource("unknown".into(), value.into()));
};
resource.is_valid()?;
Ok(resource)
}
}
impl Validator for Resource {
fn is_valid(&self) -> Result<(), Error> {
match self {
Self::S3(pattern) => {
if pattern.is_empty() || pattern.starts_with('/') {
return Err(Error::InvalidResource("s3".into(), pattern.into()));
}
}
Self::Kms(pattern) => {
if pattern.is_empty()
|| pattern
.char_indices()
.find(|&(_, c)| c == '/' || c == '\\' || c == '.')
.map(|(i, _)| i)
.is_some()
{
return Err(Error::InvalidResource("kms".into(), pattern.into()));
}
}
}
Ok(())
}
}
+102
View File
@@ -0,0 +1,102 @@
use std::borrow::Cow;
use serde::{Deserialize, Serialize};
use super::{action::Action, ActionSet, Args, Effect, Error, Functions, ResourceSet, Validator, ID};
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Statement {
pub sid: ID,
pub effect: Effect,
pub actions: ActionSet,
pub not_actions: ActionSet,
pub resoures: ResourceSet,
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() {
if resource == "/" || self.resoures.is_empty() {
break 'c self.conditions.evaluate(&args.conditions);
}
}
if !self.resoures.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 {
fn is_valid(&self) -> Result<(), Error> {
self.effect.is_valid()?;
// check sid
self.sid.is_valid()?;
if self.actions.is_empty() || self.not_actions.is_empty() {
return Err(Error::NonAction);
}
if self.resoures.is_empty() {
return Err(Error::NonResource);
}
self.actions.is_valid()?;
self.not_actions.is_valid()?;
self.resoures.is_valid()?;
Ok(())
}
}
+87
View File
@@ -0,0 +1,87 @@
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)
}
}
+141
View File
@@ -0,0 +1,141 @@
use std::{fmt::Write, usize};
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());
}
}
+189
View File
@@ -0,0 +1,189 @@
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)
}
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().into_iter(), text.as_ref().as_bytes().into_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()
}
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(name.as_bytes(), pattern.as_bytes(), simple)
}
fn deep_match(mut name: &[u8], mut pattern: &[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(name, &pattern[1..], simple)
|| (!name.is_empty() && deep_match(&name[1..], pattern, 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")]
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)
}
}
+20
View File
@@ -0,0 +1,20 @@
use crate::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)
}
}
+43
View File
@@ -0,0 +1,43 @@
pub mod object;
use std::collections::HashMap;
use ecstore::store_api::ObjectInfo;
use serde::{de::DeserializeOwned, Serialize};
use crate::{
auth::UserIdentity,
cache::Cache,
policy::{PolicyDoc, UserType, DEFAULT_POLICIES},
};
#[async_trait::async_trait]
pub trait Store: Clone + Send + Sync + 'static {
async fn load_iam_config<Item>(&self, path: impl AsRef<str> + Send) -> crate::Result<(Item, ObjectInfo)>
where
Item: DeserializeOwned;
async fn save_iam_config<Item: Serialize + Send>(&self, item: Item, path: impl AsRef<str> + Send) -> crate::Result<()>;
async fn load_all(&self, cache: &Cache) -> crate::Result<()>;
fn get_default_policyes() -> HashMap<String, PolicyDoc> {
DEFAULT_POLICIES
.iter()
.map(|(n, p)| {
(
n.to_string(),
PolicyDoc {
version: 1,
policy: p.clone(),
..Default::default()
},
)
})
.collect()
}
async fn load_users(&self, user_type: UserType) -> crate::Result<HashMap<String, UserIdentity>>;
async fn load_policy_docs(&self) -> crate::Result<HashMap<String, PolicyDoc>>;
}
+345
View File
@@ -0,0 +1,345 @@
use std::{collections::HashMap, path::Path, sync::Arc};
use ecstore::{
config::error::is_not_found,
store::{ECStore, ListPathOptions},
store_api::{HTTPRangeSpec, ObjectIO, ObjectInfo, ObjectOptions, PutObjReader},
utils::path::dir,
};
use futures::{future::try_join_all, SinkExt};
use log::debug;
use serde::{de::DeserializeOwned, Serialize};
use super::Store;
use crate::{
auth::UserIdentity,
cache::{Cache, CacheEntity, CacheInner},
policy::{utils::split_path, MappedPolicy, PolicyDoc, UserType},
Error,
};
#[derive(Clone)]
pub struct ObjectStore {
object_api: Arc<ECStore>,
}
impl ObjectStore {
const BUCKET_NAME: &str = ".rustfs.sys";
pub fn new(object_api: Arc<ECStore>) -> Self {
Self { object_api }
}
async fn list_iam_config_items(&self, prefix: &str, items: &[&str]) -> crate::Result<Vec<String>> {
debug!("list iam config items, prefix: {prefix}");
// todo, 实现walk,使用walk
let mut futures = Vec::with_capacity(items.len());
for item in items {
let prefix = format!("{}{}", prefix, item);
futures.push(async move {
let items = self
.object_api
.list_path(
&ListPathOptions {
bucket: Self::BUCKET_NAME.into(),
prefix: prefix.clone(),
..Default::default()
},
"",
)
.await;
match items {
Ok(items) => Result::<_, crate::Error>::Ok(items.objects),
Err(e) if is_not_found(&e) => Result::<_, crate::Error>::Ok(vec![]),
Err(e) => Err(Error::StringError(format!("list {prefix} failed, err: {e:?}"))),
}
});
}
Ok(try_join_all(futures)
.await?
.into_iter()
.flat_map(|x| x.into_iter())
.map(|x| x.name)
.collect())
}
async fn load_policy(&self, name: &str) -> crate::Result<PolicyDoc> {
let (mut policy, object) = self
.load_iam_config::<PolicyDoc>(&format!("config/iam/policies/{name}/policy.json"))
.await?;
if policy.version == 0 {
policy.create_date = object.mod_time;
policy.update_date = object.mod_time;
}
Ok(policy)
}
async fn load_user_identity(&self, user_type: UserType, name: &str) -> crate::Result<Option<UserIdentity>> {
let (mut user, _) = self
.load_iam_config::<UserIdentity>(&format!(
"config/iam/{base}{name}/identity.json",
base = user_type.prefix(),
name = name
))
.await?;
if user.credentials.is_expired() {
return Ok(None);
}
if user.credentials.access_key.is_empty() {
user.credentials.access_key = name.to_owned();
}
// todo, 校验session token
Ok(Some(user))
}
async fn load_mapped_policy(&self, user_type: UserType, name: &str, is_group: bool) -> crate::Result<MappedPolicy> {
let (p, _) = self
.load_iam_config::<MappedPolicy>(&format!("{base}{name}.json", base = user_type.prefix(), name = name))
.await?;
Ok(p)
}
}
#[async_trait::async_trait]
impl Store for ObjectStore {
async fn load_iam_config<Item>(&self, path: impl AsRef<str> + Send) -> crate::Result<(Item, ObjectInfo)>
where
Item: DeserializeOwned,
{
debug!("load iam config, path: {}", path.as_ref());
let mut reader = self
.object_api
.get_object_reader(
Self::BUCKET_NAME,
path.as_ref(),
HTTPRangeSpec::nil(),
Default::default(),
&Default::default(),
)
.await
.map_err(crate::Error::EcstoreError)?;
let data = reader.read_all().await.map_err(crate::Error::EcstoreError)?;
// let data = crypto::decrypt_data(&[], &data)?;
Ok((
serde_json::from_slice(&data).map_err(|e| crate::Error::StringError(e.to_string()))?,
reader.object_info,
))
}
async fn save_iam_config<Item: Serialize + Send>(&self, item: Item, path: impl AsRef<str> + Send) -> crate::Result<()> {
let data = serde_json::to_vec(&item).map_err(|e| crate::Error::StringError(e.to_string()))?;
// let data = crypto::encrypt_data(&[], &data)?;
self.object_api
.put_object(
Self::BUCKET_NAME,
path.as_ref(),
&mut PutObjReader::from_vec(data),
&ObjectOptions {
max_parity: true,
..Default::default()
},
)
.await
.map_err(crate::Error::EcstoreError)?;
Ok(())
}
async fn load_policy_docs(&self) -> crate::Result<HashMap<String, PolicyDoc>> {
let paths = self.list_iam_config_items("config/iam/", &["policies/"]).await?;
let mut result = Self::get_default_policyes();
for path in paths {
let name = Path::new(&path).iter().rev().nth(0).unwrap();
let (mut policy_doc, object_info) = self
.load_iam_config::<PolicyDoc>(format!("config/iam/policies/{}/policy.json", name.to_str().unwrap()))
.await?;
if policy_doc.version == 0 {
policy_doc.create_date = object_info.mod_time.clone();
policy_doc.update_date = object_info.mod_time.clone();
}
result.insert(name.to_str().unwrap().to_owned(), policy_doc);
}
Ok(result)
}
async fn load_users(&self, user_type: UserType) -> crate::Result<HashMap<String, UserIdentity>> {
let paths = self.list_iam_config_items("config/iam/", &[user_type.prefix()]).await?;
let mut result = HashMap::new();
for path in paths {
let name = Path::new(&path).iter().rev().nth(0).unwrap();
let (mut user_identity, _) = self
.load_iam_config::<UserIdentity>(format!("config/iam/users/{}/identity.json", name.to_str().unwrap()))
.await?;
if user_identity.credentials.is_expired() {
return Err(Error::NoSuchUser(name.to_str().unwrap().to_owned()));
}
if user_identity.credentials.access_key.is_empty() {
user_identity.credentials.access_key = name.to_str().unwrap().to_owned();
}
// todo 解析 sts
result.insert(name.to_str().unwrap().to_owned(), user_identity);
}
Ok(result)
}
/// load all and make a new cache.
async fn load_all(&self, cache: &Cache) -> crate::Result<()> {
let items = self
.list_iam_config_items(
"config/iam/",
&[
"policydb/",
"policies/",
"groups/",
"policydb/users/",
"policydb/groups/",
"service-accounts/",
"policydb/sts-users/",
"sts",
],
)
.await?;
debug!("all iam items: {items:?}");
let (policy_docs, users, user_policies, sts_policies, sts_accounts) = (
Arc::new(tokio::sync::Mutex::new(CacheEntity::new(Self::get_default_policyes()))),
Arc::new(tokio::sync::Mutex::new(CacheEntity::default())),
Arc::new(tokio::sync::Mutex::new(CacheEntity::default())),
Arc::new(tokio::sync::Mutex::new(CacheEntity::default())),
Arc::new(tokio::sync::Mutex::new(CacheEntity::default())),
);
// 一次读取32个元素
let mut iter = items
.iter()
.map(|item| item.trim_start_matches("config/iam/"))
.map(|item| split_path(item, item.starts_with("policydb/")))
.filter_map(|(list_key, trimmed_item)| {
debug!("list_key: {list_key}, trimmed_item: {trimmed_item}");
if list_key == "format.json" {
return None;
}
let (policy_docs, users, user_policies, sts_policies, sts_accounts) = (
policy_docs.clone(),
users.clone(),
user_policies.clone(),
sts_policies.clone(),
sts_accounts.clone(),
);
Some(async move {
match list_key {
"policies/" => {
let name = dir(trimmed_item).trim_end_matches('/');
let policy_doc = self.load_policy(name).await?;
policy_docs.lock().await.insert(name.to_owned(), policy_doc);
}
"users/" => {
let name = dir(trimmed_item);
if let Some(user) = self.load_user_identity(UserType::Reg, name).await? {
users.lock().await.insert(name.to_owned(), user);
};
}
"groups/" => {}
"policydb/users/" | "policydb/groups/" => {
let name = trimmed_item.strip_suffix(".json").unwrap_or(trimmed_item);
let mapped_policy = self
.load_mapped_policy(UserType::Reg, name, list_key == "policydb/groups/")
.await?;
if !mapped_policy.policies.is_empty() {
user_policies.lock().await.insert(name.to_owned(), mapped_policy);
}
}
"service-accounts/" => {
let name = dir(trimmed_item).trim_end_matches('/');
let Some(user) = self.load_user_identity(UserType::Svc, name).await? else {
return Ok(());
};
let parent = user.credentials.parent_user.clone();
{
users.lock().await.insert(name.to_owned(), user);
}
if users.lock().await.get(&parent).is_some() {
return Ok(());
}
match self.load_mapped_policy(UserType::Sts, parent.as_str(), false).await {
Ok(m) => sts_policies.lock().await.insert(name.to_owned(), m),
Err(Error::EcstoreError(e)) if is_not_found(&e) => return Ok(()),
Err(e) => return Err(e),
};
}
"sts/" => {
let name = dir(trimmed_item);
if let Some(user) = self.load_user_identity(UserType::Sts, name).await? {
sts_accounts.lock().await.insert(name.to_owned(), user);
};
}
"policydb/sts-users/" => {
let name = trimmed_item.strip_suffix(".json").unwrap_or(trimmed_item);
let mapped_policy = self.load_mapped_policy(UserType::Sts, name, false).await?;
if !mapped_policy.policies.is_empty() {
sts_policies.lock().await.insert(name.to_owned(), mapped_policy);
}
}
_ => {}
}
crate::Result::Ok(())
})
});
let mut all_futures = Vec::with_capacity(32);
while let Some(f) = iter.next() {
all_futures.push(f);
if all_futures.len() == 32 {
try_join_all(all_futures).await?;
all_futures = Vec::with_capacity(32);
}
}
if !all_futures.is_empty() {
try_join_all(all_futures).await?;
}
Arc::into_inner(users).map(|x| cache.users.store(Arc::new(x.into_inner().update_load_time())));
Arc::into_inner(policy_docs).map(|x| cache.policy_docs.store(Arc::new(x.into_inner().update_load_time())));
Arc::into_inner(user_policies).map(|x| cache.user_policies.store(Arc::new(x.into_inner().update_load_time())));
Arc::into_inner(sts_policies).map(|x| cache.sts_policies.store(Arc::new(x.into_inner().update_load_time())));
Arc::into_inner(sts_accounts).map(|x| cache.sts_accounts.store(Arc::new(x.into_inner().update_load_time())));
Ok(())
}
}
+62
View File
@@ -0,0 +1,62 @@
use rand::{Rng, RngCore};
use crate::Error;
pub fn gen_access_key(length: usize) -> crate::Result<String> {
const ALPHA_NUMERIC_TABLE: [char; 36] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
];
if length < 3 {
return Err(Error::StringError("access key length is too short".into()));
}
let mut result = String::with_capacity(length);
let mut rng = rand::thread_rng();
for _ in 0..length {
result.push(ALPHA_NUMERIC_TABLE[rng.gen_range(0..ALPHA_NUMERIC_TABLE.len())]);
}
Ok(result)
}
pub fn gen_secret_key(length: usize) -> crate::Result<String> {
use base64_simd::URL_SAFE_NO_PAD;
if length < 8 {
return Err(Error::StringError("secret key length is too short".into()));
}
let mut rng = rand::thread_rng();
let mut key = vec![0u8; URL_SAFE_NO_PAD.estimated_decoded_length(length)];
rng.fill_bytes(&mut key);
let encoded = URL_SAFE_NO_PAD.encode_to_string(&key);
let key_str = encoded.replace("/", "+");
Ok(key_str)
}
#[cfg(test)]
mod tests {
use super::{gen_access_key, gen_secret_key};
#[test]
fn test_gen_access_key() {
let a = gen_access_key(10).unwrap();
let b = gen_access_key(10).unwrap();
assert_eq!(a.len(), 10);
assert_eq!(b.len(), 10);
assert_ne!(a, b);
}
#[test]
fn test_gen_secret_key() {
let a = gen_secret_key(10).unwrap();
let b = gen_secret_key(10).unwrap();
assert_ne!(a, b);
}
}