feat: Replace LRU cache with Moka async cache in policy variables (#1166)

Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
yxrxy
2025-12-17 00:19:31 +08:00
committed by GitHub
parent 17828ec2a8
commit 8821fcc1e7
15 changed files with 289 additions and 277 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ pub struct PolicySys {}
impl PolicySys {
pub async fn is_allowed(args: &BucketPolicyArgs<'_>) -> bool {
match Self::get(args.bucket).await {
Ok(cfg) => return cfg.is_allowed(args),
Ok(cfg) => return cfg.is_allowed(args).await,
Err(err) => {
if err != StorageError::ConfigNotFound {
info!("config get err {:?}", err);
+2
View File
@@ -47,5 +47,7 @@ tracing.workspace = true
rustfs-madmin.workspace = true
rustfs-utils = { workspace = true, features = ["path"] }
tokio-util.workspace = true
pollster.workspace = true
[dev-dependencies]
pollster.workspace = true
+57 -15
View File
@@ -23,6 +23,7 @@ use crate::{
UpdateServiceAccountOpts,
},
};
use futures::future::join_all;
use rustfs_ecstore::global::get_global_action_cred;
use rustfs_madmin::{AccountStatus, AddOrUpdateUserReq, GroupDesc};
use rustfs_policy::{
@@ -402,13 +403,25 @@ where
self.cache.policy_docs.store(Arc::new(cache));
let ret = m
let items: Vec<_> = m.into_iter().map(|(k, v)| (k, v.policy.clone())).collect();
let futures: Vec<_> = items.iter().map(|(_, policy)| policy.match_resource(bucket_name)).collect();
let results = join_all(futures).await;
let filtered = items
.into_iter()
.filter(|(_, v)| bucket_name.is_empty() || v.policy.match_resource(bucket_name))
.map(|(k, v)| (k, v.policy))
.zip(results)
.filter_map(|((k, policy), matches)| {
if bucket_name.is_empty() || matches {
Some((k, policy))
} else {
None
}
})
.collect();
Ok(ret)
Ok(filtered)
}
pub async fn merge_policies(&self, name: &str) -> (String, Policy) {
@@ -456,22 +469,51 @@ where
self.cache.policy_docs.store(Arc::new(cache));
let ret = m
.into_iter()
.filter(|(_, v)| bucket_name.is_empty() || v.policy.match_resource(bucket_name))
let items: Vec<_> = m.into_iter().map(|(k, v)| (k, v.clone())).collect();
let futures: Vec<_> = items
.iter()
.map(|(_, policy_doc)| policy_doc.policy.match_resource(bucket_name))
.collect();
Ok(ret)
let results = join_all(futures).await;
let filtered = items
.into_iter()
.zip(results)
.filter_map(|((k, policy_doc), matches)| {
if bucket_name.is_empty() || matches {
Some((k, policy_doc))
} else {
None
}
})
.collect();
Ok(filtered)
}
pub async fn list_policy_docs_internal(&self, bucket_name: &str) -> Result<HashMap<String, PolicyDoc>> {
let ret = self
.cache
.policy_docs
.load()
let cache = self.cache.policy_docs.load();
let items: Vec<_> = cache.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
let futures: Vec<_> = items
.iter()
.filter(|(_, v)| bucket_name.is_empty() || v.policy.match_resource(bucket_name))
.map(|(k, v)| (k.clone(), v.clone()))
.map(|(_, policy_doc)| policy_doc.policy.match_resource(bucket_name))
.collect();
let results = join_all(futures).await;
let ret = items
.into_iter()
.zip(results)
.filter_map(|((k, policy_doc), matches)| {
if bucket_name.is_empty() || matches {
Some((k, policy_doc))
} else {
None
}
})
.collect();
Ok(ret)
@@ -1753,7 +1795,7 @@ fn filter_policies(cache: &Cache, policy_name: &str, bucket_name: &str) -> (Stri
}
if let Some(p) = cache.policy_docs.load().get(&policy) {
if bucket_name.is_empty() || p.policy.match_resource(bucket_name) {
if bucket_name.is_empty() || pollster::block_on(p.policy.match_resource(bucket_name)) {
policies.push(policy);
to_merge.push(p.policy.clone());
}
+8 -8
View File
@@ -755,10 +755,10 @@ impl<T: Store> IamSys<T> {
let (has_session_policy, is_allowed_sp) = is_allowed_by_session_policy(args);
if has_session_policy {
return is_allowed_sp && (is_owner || combined_policy.is_allowed(args));
return is_allowed_sp && (is_owner || combined_policy.is_allowed(args).await);
}
is_owner || combined_policy.is_allowed(args)
is_owner || combined_policy.is_allowed(args).await
}
pub async fn is_allowed_service_account(&self, args: &Args<'_>, parent_user: &str) -> bool {
@@ -814,15 +814,15 @@ impl<T: Store> IamSys<T> {
};
if sa_str == INHERITED_POLICY_TYPE {
return is_owner || combined_policy.is_allowed(&parent_args);
return is_owner || combined_policy.is_allowed(&parent_args).await;
}
let (has_session_policy, is_allowed_sp) = is_allowed_by_session_policy_for_service_account(args);
if has_session_policy {
return is_allowed_sp && (is_owner || combined_policy.is_allowed(&parent_args));
return is_allowed_sp && (is_owner || combined_policy.is_allowed(&parent_args).await);
}
is_owner || combined_policy.is_allowed(&parent_args)
is_owner || combined_policy.is_allowed(&parent_args).await
}
pub async fn get_combined_policy(&self, policies: &[String]) -> Policy {
@@ -857,7 +857,7 @@ impl<T: Store> IamSys<T> {
return false;
}
self.get_combined_policy(&policies).await.is_allowed(args)
self.get_combined_policy(&policies).await.is_allowed(args).await
}
}
@@ -883,7 +883,7 @@ fn is_allowed_by_session_policy(args: &Args<'_>) -> (bool, bool) {
let mut session_policy_args = args.clone();
session_policy_args.is_owner = false;
(has_session_policy, sub_policy.is_allowed(&session_policy_args))
(has_session_policy, pollster::block_on(sub_policy.is_allowed(&session_policy_args)))
}
fn is_allowed_by_session_policy_for_service_account(args: &Args<'_>) -> (bool, bool) {
@@ -909,7 +909,7 @@ fn is_allowed_by_session_policy_for_service_account(args: &Args<'_>) -> (bool, b
let mut session_policy_args = args.clone();
session_policy_args.is_owner = false;
(has_session_policy, sub_policy.is_allowed(&session_policy_args))
(has_session_policy, pollster::block_on(sub_policy.is_allowed(&session_policy_args)))
}
#[derive(Debug, Clone, Default)]
+5 -1
View File
@@ -45,8 +45,12 @@ regex = { workspace = true }
reqwest.workspace = true
chrono.workspace = true
tracing.workspace = true
lru.workspace = true
moka.workspace = true
async-trait.workspace = true
futures.workspace = true
pollster.workspace = true
[dev-dependencies]
pollster.workspace = true
test-case.workspace = true
temp-env = { workspace = true }
+6 -6
View File
@@ -38,29 +38,29 @@ pub struct Functions {
}
impl Functions {
pub fn evaluate(&self, values: &HashMap<String, Vec<String>>) -> bool {
self.evaluate_with_resolver(values, None)
pub async fn evaluate(&self, values: &HashMap<String, Vec<String>>) -> bool {
self.evaluate_with_resolver(values, None).await
}
pub fn evaluate_with_resolver(
pub async fn evaluate_with_resolver(
&self,
values: &HashMap<String, Vec<String>>,
resolver: Option<&dyn PolicyVariableResolver>,
) -> bool {
for c in self.for_any_value.iter() {
if !c.evaluate_with_resolver(false, values, resolver) {
if !c.evaluate_with_resolver(false, values, resolver).await {
return false;
}
}
for c in self.for_all_values.iter() {
if !c.evaluate_with_resolver(true, values, resolver) {
if !c.evaluate_with_resolver(true, values, resolver).await {
return false;
}
}
for c in self.for_normal.iter() {
if !c.evaluate_with_resolver(false, values, resolver) {
if !c.evaluate_with_resolver(false, values, resolver).await {
return false;
}
}
@@ -107,7 +107,7 @@ impl Condition {
}
}
pub fn evaluate_with_resolver(
pub async fn evaluate_with_resolver(
&self,
for_all: bool,
values: &HashMap<String, Vec<String>>,
@@ -116,12 +116,12 @@ impl Condition {
use Condition::*;
let r = match self {
StringEquals(s) => s.evaluate_with_resolver(for_all, false, false, false, values, resolver),
StringNotEquals(s) => s.evaluate_with_resolver(for_all, false, false, true, values, resolver),
StringEqualsIgnoreCase(s) => s.evaluate_with_resolver(for_all, true, false, false, values, resolver),
StringNotEqualsIgnoreCase(s) => s.evaluate_with_resolver(for_all, true, false, true, values, resolver),
StringLike(s) => s.evaluate_with_resolver(for_all, false, true, false, values, resolver),
StringNotLike(s) => s.evaluate_with_resolver(for_all, false, true, true, values, resolver),
StringEquals(s) => s.evaluate_with_resolver(for_all, false, false, false, values, resolver).await,
StringNotEquals(s) => s.evaluate_with_resolver(for_all, false, false, true, values, resolver).await,
StringEqualsIgnoreCase(s) => s.evaluate_with_resolver(for_all, true, false, false, values, resolver).await,
StringNotEqualsIgnoreCase(s) => s.evaluate_with_resolver(for_all, true, false, true, values, resolver).await,
StringLike(s) => s.evaluate_with_resolver(for_all, false, true, false, values, resolver).await,
StringNotLike(s) => s.evaluate_with_resolver(for_all, false, true, true, values, resolver).await,
BinaryEquals(s) => s.evaluate(values),
IpAddress(s) => s.evaluate(values),
NotIpAddress(s) => s.evaluate(values),
+40 -39
View File
@@ -21,16 +21,17 @@ use std::{borrow::Cow, collections::HashMap};
use crate::policy::function::func::FuncKeyValue;
use crate::policy::utils::wildcard;
use futures::future;
use serde::{Deserialize, Deserializer, Serialize, de, ser::SerializeSeq};
use super::{func::InnerFunc, key_name::KeyName};
use crate::policy::variables::{PolicyVariableResolver, resolve_aws_variables};
use crate::policy::variables::PolicyVariableResolver;
pub type StringFunc = InnerFunc<StringFuncValue>;
impl StringFunc {
#[allow(clippy::too_many_arguments)]
pub(crate) fn evaluate_with_resolver(
pub(crate) async fn evaluate_with_resolver(
&self,
for_all: bool,
ignore_case: bool,
@@ -41,9 +42,9 @@ impl StringFunc {
) -> bool {
for inner in self.0.iter() {
let result = if like {
inner.eval_like(for_all, values, resolver) ^ negate
inner.eval_like(for_all, values, resolver).await ^ negate
} else {
inner.eval(for_all, ignore_case, values, resolver) ^ negate
inner.eval(for_all, ignore_case, values, resolver).await ^ negate
};
if !result {
@@ -56,7 +57,7 @@ impl StringFunc {
}
impl FuncKeyValue<StringFuncValue> {
fn eval(
async fn eval(
&self,
for_all: bool,
ignore_case: bool,
@@ -79,17 +80,18 @@ impl FuncKeyValue<StringFuncValue> {
})
.unwrap_or_default();
let fvalues = self
.values
.0
.iter()
.flat_map(|c| {
if let Some(res) = resolver {
resolve_aws_variables(c, res)
} else {
vec![c.to_string()]
}
})
let resolved_values: Vec<Vec<String>> = futures::future::join_all(self.values.0.iter().map(|c| async {
if let Some(res) = resolver {
super::super::variables::resolve_aws_variables(c, res).await
} else {
vec![c.to_string()]
}
}))
.await;
let fvalues = resolved_values
.into_iter()
.flatten()
.map(|resolved_c| {
let mut c = Cow::from(resolved_c);
for key in KeyName::COMMON_KEYS {
@@ -113,7 +115,7 @@ impl FuncKeyValue<StringFuncValue> {
}
}
fn eval_like(
async fn eval_like(
&self,
for_all: bool,
values: &HashMap<String, Vec<String>>,
@@ -121,17 +123,22 @@ impl FuncKeyValue<StringFuncValue> {
) -> bool {
if let Some(rvalues) = values.get(self.key.name().as_str()) {
for v in rvalues.iter() {
let matched = self
let resolved_futures: Vec<_> = self
.values
.0
.iter()
.flat_map(|c| {
.map(|c| async {
if let Some(res) = resolver {
resolve_aws_variables(c, res)
super::super::variables::resolve_aws_variables(c, res).await
} else {
vec![c.to_string()]
}
})
.collect();
let resolved_values = future::join_all(resolved_futures).await;
let matched = resolved_values
.into_iter()
.flatten()
.map(|resolved_c| {
let mut c = Cow::from(resolved_c);
for key in KeyName::COMMON_KEYS {
@@ -242,6 +249,7 @@ mod tests {
key_name::AwsKeyName::*,
key_name::KeyName::{self, *},
};
use std::collections::HashMap;
use crate::policy::function::key_name::S3KeyName::S3LocationConstraint;
use test_case::test_case;
@@ -303,17 +311,13 @@ mod tests {
negate: bool,
values: Vec<(&str, Vec<&str>)>,
) -> bool {
let result = s.eval(
for_all,
ignore_case,
&values
.into_iter()
.map(|(k, v)| (k.to_owned(), v.into_iter().map(ToOwned::to_owned).collect::<Vec<String>>()))
.collect(),
None,
);
let map: HashMap<String, Vec<String>> = values
.into_iter()
.map(|(k, v)| (k.to_owned(), v.into_iter().map(ToOwned::to_owned).collect::<Vec<String>>()))
.collect();
let result = s.eval(for_all, ignore_case, &map, None);
result ^ negate
pollster::block_on(result) ^ negate
}
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => true ; "1")]
@@ -409,16 +413,13 @@ mod tests {
}
fn test_eval_like(s: FuncKeyValue<StringFuncValue>, for_all: bool, negate: bool, values: Vec<(&str, Vec<&str>)>) -> bool {
let result = s.eval_like(
for_all,
&values
.into_iter()
.map(|(k, v)| (k.to_owned(), v.into_iter().map(ToOwned::to_owned).collect::<Vec<String>>()))
.collect(),
None,
);
let map: HashMap<String, Vec<String>> = values
.into_iter()
.map(|(k, v)| (k.to_owned(), v.into_iter().map(ToOwned::to_owned).collect::<Vec<String>>()))
.collect();
let result = s.eval_like(for_all, &map, None);
result ^ negate
pollster::block_on(result) ^ negate
}
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => true ; "1")]
+17 -17
View File
@@ -62,9 +62,9 @@ pub struct Policy {
}
impl Policy {
pub fn is_allowed(&self, args: &Args) -> bool {
pub async 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) {
if !statement.is_allowed(args).await {
return false;
}
}
@@ -74,7 +74,7 @@ impl Policy {
}
for statement in self.statements.iter().filter(|s| matches!(s.effect, Effect::Allow)) {
if statement.is_allowed(args) {
if statement.is_allowed(args).await {
return true;
}
}
@@ -82,9 +82,9 @@ impl Policy {
false
}
pub fn match_resource(&self, resource: &str) -> bool {
pub async fn match_resource(&self, resource: &str) -> bool {
for statement in self.statements.iter() {
if statement.resources.match_resource(resource) {
if statement.resources.match_resource(resource).await {
return true;
}
}
@@ -188,9 +188,9 @@ pub struct BucketPolicy {
}
impl BucketPolicy {
pub fn is_allowed(&self, args: &BucketPolicyArgs) -> bool {
pub async fn is_allowed(&self, args: &BucketPolicyArgs<'_>) -> bool {
for statement in self.statements.iter().filter(|s| matches!(s.effect, Effect::Deny)) {
if !statement.is_allowed(args) {
if !statement.is_allowed(args).await {
return false;
}
}
@@ -200,7 +200,7 @@ impl BucketPolicy {
}
for statement in self.statements.iter().filter(|s| matches!(s.effect, Effect::Allow)) {
if statement.is_allowed(args) {
if statement.is_allowed(args).await {
return true;
}
}
@@ -577,8 +577,8 @@ mod test {
deny_only: false,
};
assert!(policy.is_allowed(&args1));
assert!(!policy.is_allowed(&args2));
assert!(pollster::block_on(policy.is_allowed(&args1)));
assert!(!pollster::block_on(policy.is_allowed(&args2)));
Ok(())
}
@@ -631,8 +631,8 @@ mod test {
deny_only: false,
};
assert!(policy.is_allowed(&args1));
assert!(!policy.is_allowed(&args2));
assert!(pollster::block_on(policy.is_allowed(&args1)));
assert!(!pollster::block_on(policy.is_allowed(&args2)));
Ok(())
}
@@ -686,8 +686,8 @@ mod test {
deny_only: false,
};
assert!(policy.is_allowed(&args1));
assert!(!policy.is_allowed(&args2));
assert!(pollster::block_on(policy.is_allowed(&args1)));
assert!(!pollster::block_on(policy.is_allowed(&args2)));
Ok(())
}
@@ -741,8 +741,8 @@ mod test {
deny_only: false,
};
assert!(policy.is_allowed(&args1));
assert!(!policy.is_allowed(&args2));
assert!(pollster::block_on(policy.is_allowed(&args1)));
assert!(!pollster::block_on(policy.is_allowed(&args2)));
Ok(())
}
@@ -798,7 +798,7 @@ mod test {
};
// Either user1 or user2 should be allowed
assert!(policy.is_allowed(&args1) || policy.is_allowed(&args2));
assert!(pollster::block_on(policy.is_allowed(&args1)) || pollster::block_on(policy.is_allowed(&args2)));
Ok(())
}
+15 -14
View File
@@ -24,25 +24,25 @@ use super::{
Error as IamError, Validator,
function::key_name::KeyName,
utils::{path, wildcard},
variables::{PolicyVariableResolver, resolve_aws_variables},
variables::PolicyVariableResolver,
};
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
pub struct ResourceSet(pub HashSet<Resource>);
impl ResourceSet {
pub fn is_match(&self, resource: &str, conditions: &HashMap<String, Vec<String>>) -> bool {
self.is_match_with_resolver(resource, conditions, None)
pub async fn is_match(&self, resource: &str, conditions: &HashMap<String, Vec<String>>) -> bool {
self.is_match_with_resolver(resource, conditions, None).await
}
pub fn is_match_with_resolver(
pub async fn is_match_with_resolver(
&self,
resource: &str,
conditions: &HashMap<String, Vec<String>>,
resolver: Option<&dyn PolicyVariableResolver>,
) -> bool {
for re in self.0.iter() {
if re.is_match_with_resolver(resource, conditions, resolver) {
if re.is_match_with_resolver(resource, conditions, resolver).await {
return true;
}
}
@@ -50,9 +50,9 @@ impl ResourceSet {
false
}
pub fn match_resource(&self, resource: &str) -> bool {
pub async fn match_resource(&self, resource: &str) -> bool {
for re in self.0.iter() {
if re.match_resource(resource) {
if re.match_resource(resource).await {
return true;
}
}
@@ -95,11 +95,11 @@ pub enum Resource {
impl Resource {
pub const S3_PREFIX: &'static str = "arn:aws:s3:::";
pub fn is_match(&self, resource: &str, conditions: &HashMap<String, Vec<String>>) -> bool {
self.is_match_with_resolver(resource, conditions, None)
pub async fn is_match(&self, resource: &str, conditions: &HashMap<String, Vec<String>>) -> bool {
self.is_match_with_resolver(resource, conditions, None).await
}
pub fn is_match_with_resolver(
pub async fn is_match_with_resolver(
&self,
resource: &str,
conditions: &HashMap<String, Vec<String>>,
@@ -111,7 +111,7 @@ impl Resource {
};
let patterns = if let Some(res) = resolver {
resolve_aws_variables(&pattern, res)
super::variables::resolve_aws_variables(&pattern, res).await
} else {
vec![pattern.clone()]
};
@@ -143,8 +143,8 @@ impl Resource {
false
}
pub fn match_resource(&self, resource: &str) -> bool {
self.is_match(resource, &HashMap::new())
pub async fn match_resource(&self, resource: &str) -> bool {
self.is_match(resource, &HashMap::new()).await
}
}
@@ -232,6 +232,7 @@ mod tests {
#[test_case("arn:aws:s3:::mybucket","mybucket/myobject" => false; "15")]
fn test_resource_is_match(resource: &str, object: &str) -> bool {
let resource: Resource = resource.try_into().unwrap();
resource.is_match(object, &HashMap::new())
pollster::block_on(resource.is_match(object, &HashMap::new()))
}
}
+8 -7
View File
@@ -69,7 +69,7 @@ impl Statement {
false
}
pub fn is_allowed(&self, args: &Args) -> bool {
pub async fn is_allowed(&self, args: &Args<'_>) -> bool {
let mut context = VariableContext::new();
context.claims = Some(args.claims.clone());
context.conditions = args.conditions.clone();
@@ -104,19 +104,20 @@ impl Statement {
}
if self.is_kms() && (resource == "/" || self.resources.is_empty()) {
break 'c self.conditions.evaluate_with_resolver(args.conditions, Some(&resolver));
break 'c self.conditions.evaluate_with_resolver(args.conditions, Some(&resolver)).await;
}
if !self
.resources
.is_match_with_resolver(&resource, args.conditions, Some(&resolver))
.await
&& !self.is_admin()
&& !self.is_sts()
{
break 'c false;
}
self.conditions.evaluate_with_resolver(args.conditions, Some(&resolver))
self.conditions.evaluate_with_resolver(args.conditions, Some(&resolver)).await
};
self.effect.is_allowed(check)
@@ -178,7 +179,7 @@ pub struct BPStatement {
}
impl BPStatement {
pub fn is_allowed(&self, args: &BucketPolicyArgs) -> bool {
pub async fn is_allowed(&self, args: &BucketPolicyArgs<'_>) -> bool {
let check = 'c: {
if !self.principal.is_match(args.account) {
break 'c false;
@@ -199,15 +200,15 @@ impl BPStatement {
resource.push('/');
}
if !self.resources.is_empty() && !self.resources.is_match(&resource, args.conditions) {
if !self.resources.is_empty() && !self.resources.is_match(&resource, args.conditions).await {
break 'c false;
}
if !self.not_resources.is_empty() && self.not_resources.is_match(&resource, args.conditions) {
if !self.not_resources.is_empty() && self.not_resources.is_match(&resource, args.conditions).await {
break 'c false;
}
self.conditions.evaluate(args.conditions)
self.conditions.evaluate(args.conditions).await
};
self.effect.is_allowed(check)
+95 -121
View File
@@ -12,16 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use lru::LruCache;
use async_trait::async_trait;
use moka::future::Cache;
use serde_json::Value;
use std::cell::RefCell;
use std::collections::HashMap;
use std::num::NonZeroUsize;
use std::time::{Duration, Instant};
use std::future::Future;
use std::time::Duration;
use time::OffsetDateTime;
/// Context information for variable resolution
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct VariableContext {
pub is_https: bool,
pub source_ip: Option<String>,
@@ -35,109 +35,75 @@ pub struct VariableContext {
impl VariableContext {
pub fn new() -> Self {
Self {
is_https: false,
source_ip: None,
account_id: None,
region: None,
username: None,
claims: None,
conditions: HashMap::new(),
custom_variables: HashMap::new(),
}
Self::default()
}
}
impl Default for VariableContext {
fn default() -> Self {
Self::new()
}
}
/// Variable resolution cache
struct CachedVariable {
value: String,
timestamp: Instant,
is_dynamic: bool,
}
pub struct VariableResolverCache {
/// LRU cache storing resolved results
cache: LruCache<String, CachedVariable>,
/// Cache expiration time
ttl: Duration,
/// Moka cache storing resolved results
cache: Cache<String, String>,
}
impl VariableResolverCache {
pub fn new(capacity: usize, ttl_seconds: u64) -> Self {
Self {
cache: LruCache::new(usize::from(NonZeroUsize::new(capacity).unwrap_or(NonZeroUsize::new(100).unwrap()))),
ttl: Duration::from_secs(ttl_seconds),
}
let cache = Cache::builder()
.max_capacity(capacity as u64)
.time_to_live(Duration::from_secs(ttl_seconds))
.build();
Self { cache }
}
pub fn get(&mut self, key: &str) -> Option<String> {
if let Some(cached) = self.cache.get(key) {
// Check if expired
if !cached.is_dynamic && cached.timestamp.elapsed() < self.ttl {
return Some(cached.value.clone());
}
}
None
pub async fn get(&self, key: &str) -> Option<String> {
self.cache.get(key).await
}
pub fn put(&mut self, key: String, value: String, is_dynamic: bool) {
let cached = CachedVariable {
value,
timestamp: Instant::now(),
is_dynamic,
};
self.cache.put(key, cached);
pub async fn put(&self, key: String, value: String) {
self.cache.insert(key, value).await;
}
pub fn clear(&mut self) {
self.cache.clear();
pub async fn clear(&self) {
self.cache.invalidate_all();
}
}
/// Cached dynamic AWS variable resolver
pub struct CachedAwsVariableResolver {
inner: VariableResolver,
cache: RefCell<VariableResolverCache>,
cache: VariableResolverCache,
}
impl CachedAwsVariableResolver {
pub fn new(context: VariableContext) -> Self {
Self {
inner: VariableResolver::new(context),
cache: RefCell::new(VariableResolverCache::new(100, 300)), // 100 entries, 5 minutes expiration
cache: VariableResolverCache::new(100, 300), // 100 entries, 5 minutes expiration
}
}
pub fn is_dynamic(&self, variable_name: &str) -> bool {
self.inner.is_dynamic(variable_name)
}
}
#[async_trait]
impl PolicyVariableResolver for CachedAwsVariableResolver {
fn resolve(&self, variable_name: &str) -> Option<String> {
async fn resolve(&self, variable_name: &str) -> Option<String> {
if self.is_dynamic(variable_name) {
return self.inner.resolve(variable_name);
return self.inner.resolve(variable_name).await;
}
if let Some(cached) = self.cache.borrow_mut().get(variable_name) {
if let Some(cached) = self.cache.get(variable_name).await {
return Some(cached);
}
let value = self.inner.resolve(variable_name)?;
self.cache.borrow_mut().put(variable_name.to_string(), value.clone(), false);
let value = self.inner.resolve(variable_name).await?;
self.cache.put(variable_name.to_string(), value.clone()).await;
Some(value)
}
fn resolve_multiple(&self, variable_name: &str) -> Option<Vec<String>> {
if self.is_dynamic(variable_name) {
return self.inner.resolve_multiple(variable_name);
}
self.inner.resolve_multiple(variable_name)
async fn resolve_multiple(&self, variable_name: &str) -> Option<Vec<String>> {
self.inner.resolve_multiple(variable_name).await
}
fn is_dynamic(&self, variable_name: &str) -> bool {
@@ -146,10 +112,11 @@ impl PolicyVariableResolver for CachedAwsVariableResolver {
}
/// Policy variable resolver trait
pub trait PolicyVariableResolver {
fn resolve(&self, variable_name: &str) -> Option<String>;
fn resolve_multiple(&self, variable_name: &str) -> Option<Vec<String>> {
self.resolve(variable_name).map(|s| vec![s])
#[async_trait]
pub trait PolicyVariableResolver: Sync {
async fn resolve(&self, variable_name: &str) -> Option<String>;
async fn resolve_multiple(&self, variable_name: &str) -> Option<Vec<String>> {
self.resolve(variable_name).await.map(|s| vec![s])
}
fn is_dynamic(&self, variable_name: &str) -> bool;
}
@@ -192,18 +159,9 @@ impl VariableResolver {
}
fn resolve_userid(&self) -> Option<String> {
// Check claims for sub or parent
if let Some(claims) = &self.context.claims {
if let Some(sub) = claims.get("sub").and_then(|v| v.as_str()) {
return Some(sub.to_string());
}
if let Some(parent) = claims.get("parent").and_then(|v| v.as_str()) {
return Some(parent.to_string());
}
}
None
self.get_claim_as_strings("sub")
.or_else(|| self.get_claim_as_strings("parent"))
.and_then(|mut vec| vec.pop()) // 取第一个值,保持原有逻辑
}
fn resolve_principal_type(&self) -> String {
@@ -252,8 +210,9 @@ impl VariableResolver {
}
}
#[async_trait]
impl PolicyVariableResolver for VariableResolver {
fn resolve(&self, variable_name: &str) -> Option<String> {
async fn resolve(&self, variable_name: &str) -> Option<String> {
match variable_name {
"aws:username" => self.resolve_username(),
"aws:userid" => self.resolve_userid(),
@@ -275,22 +234,15 @@ impl PolicyVariableResolver for VariableResolver {
}
}
fn resolve_multiple(&self, variable_name: &str) -> Option<Vec<String>> {
async fn resolve_multiple(&self, variable_name: &str) -> Option<Vec<String>> {
match variable_name {
"aws:username" => {
// Check context.username
if let Some(ref username) = self.context.username {
Some(vec![username.clone()])
} else {
None
}
}
"aws:userid" => {
// Check claims for sub or parent
self.get_claim_as_strings("sub")
.or_else(|| self.get_claim_as_strings("parent"))
}
_ => self.resolve(variable_name).map(|s| vec![s]),
"aws:username" => self.resolve_username().map(|s| vec![s]),
"aws:userid" => self
.get_claim_as_strings("sub")
.or_else(|| self.get_claim_as_strings("parent")),
_ => self.resolve(variable_name).await.map(|s| vec![s]),
}
}
@@ -299,8 +251,7 @@ impl PolicyVariableResolver for VariableResolver {
}
}
/// Dynamically resolve AWS variables
pub fn resolve_aws_variables(pattern: &str, resolver: &dyn PolicyVariableResolver) -> Vec<String> {
pub async fn resolve_aws_variables(pattern: &str, resolver: &dyn PolicyVariableResolver) -> Vec<String> {
let mut results = vec![pattern.to_string()];
let mut changed = true;
@@ -313,7 +264,7 @@ pub fn resolve_aws_variables(pattern: &str, resolver: &dyn PolicyVariableResolve
let mut new_results = Vec::new();
for result in &results {
let resolved = resolve_single_pass(result, resolver);
let resolved = resolve_single_pass(result, resolver).await;
if resolved.len() > 1 || (resolved.len() == 1 && &resolved[0] != result) {
changed = true;
}
@@ -333,8 +284,16 @@ pub fn resolve_aws_variables(pattern: &str, resolver: &dyn PolicyVariableResolve
results
}
// Need to box the future to avoid infinite size due to recursion
fn resolve_aws_variables_boxed<'a>(
pattern: &'a str,
resolver: &'a dyn PolicyVariableResolver,
) -> std::pin::Pin<Box<dyn Future<Output = Vec<String>> + Send + 'a>> {
Box::pin(resolve_aws_variables(pattern, resolver))
}
/// Single pass resolution of variables in a string
fn resolve_single_pass(pattern: &str, resolver: &dyn PolicyVariableResolver) -> Vec<String> {
async fn resolve_single_pass(pattern: &str, resolver: &dyn PolicyVariableResolver) -> Vec<String> {
// Find all ${...} format variables
let mut results = vec![pattern.to_string()];
@@ -370,7 +329,7 @@ fn resolve_single_pass(pattern: &str, resolver: &dyn PolicyVariableResolver) ->
if var_name.contains("${") {
// For nested variables like ${${a}-${b}}, we need to resolve the inner variables first
// Then use the resolved result as a new variable to resolve
let resolved_inner = resolve_aws_variables(var_name, resolver);
let resolved_inner = resolve_aws_variables_boxed(var_name, resolver).await;
let mut new_results = Vec::new();
for resolved_var_name in resolved_inner {
@@ -390,7 +349,7 @@ fn resolve_single_pass(pattern: &str, resolver: &dyn PolicyVariableResolver) ->
}
} else {
// Regular variable resolution
if let Some(values) = resolver.resolve_multiple(var_name) {
if let Some(values) = resolver.resolve_multiple(var_name).await {
if !values.is_empty() {
// If there are multiple values, create a new result for each value
let mut new_results = Vec::new();
@@ -440,19 +399,18 @@ mod tests {
use serde_json::Value;
use std::collections::HashMap;
#[test]
fn test_resolve_aws_variables_with_username() {
#[tokio::test]
async fn test_resolve_aws_variables_with_username() {
let mut context = VariableContext::new();
context.username = Some("testuser".to_string());
let resolver = VariableResolver::new(context);
let result = resolve_aws_variables("${aws:username}-bucket", &resolver);
let result = resolve_aws_variables("${aws:username}-bucket", &resolver).await;
assert_eq!(result, vec!["testuser-bucket".to_string()]);
}
#[test]
fn test_resolve_aws_variables_with_userid() {
#[tokio::test]
async fn test_resolve_aws_variables_with_userid() {
let mut claims = HashMap::new();
claims.insert("sub".to_string(), Value::String("AIDACKCEVSQ6C2EXAMPLE".to_string()));
@@ -460,13 +418,12 @@ mod tests {
context.claims = Some(claims);
let resolver = VariableResolver::new(context);
let result = resolve_aws_variables("${aws:userid}-bucket", &resolver);
let result = resolve_aws_variables("${aws:userid}-bucket", &resolver).await;
assert_eq!(result, vec!["AIDACKCEVSQ6C2EXAMPLE-bucket".to_string()]);
}
#[test]
fn test_resolve_aws_variables_with_multiple_variables() {
#[tokio::test]
async fn test_resolve_aws_variables_with_multiple_variables() {
let mut claims = HashMap::new();
claims.insert("sub".to_string(), Value::String("AIDACKCEVSQ6C2EXAMPLE".to_string()));
@@ -475,17 +432,34 @@ mod tests {
context.username = Some("testuser".to_string());
let resolver = VariableResolver::new(context);
let result = resolve_aws_variables("${aws:username}-${aws:userid}-bucket", &resolver);
let result = resolve_aws_variables("${aws:username}-${aws:userid}-bucket", &resolver).await;
assert_eq!(result, vec!["testuser-AIDACKCEVSQ6C2EXAMPLE-bucket".to_string()]);
}
#[test]
fn test_resolve_aws_variables_no_variables() {
#[tokio::test]
async fn test_resolve_aws_variables_no_variables() {
let context = VariableContext::new();
let resolver = VariableResolver::new(context);
let result = resolve_aws_variables("test-bucket", &resolver);
let result = resolve_aws_variables("test-bucket", &resolver).await;
assert_eq!(result, vec!["test-bucket".to_string()]);
}
#[tokio::test]
async fn test_cached_aws_variable_resolver_dynamic_variables() {
let context = VariableContext::new();
let cached_resolver = CachedAwsVariableResolver::new(context);
// Dynamic variables should not be cached
let result1 = resolve_aws_variables("${aws:EpochTime}-bucket", &cached_resolver).await;
// Add a delay of 1 second to ensure different timestamps
tokio::time::sleep(Duration::from_secs(1)).await;
let result2 = resolve_aws_variables("${aws:EpochTime}-bucket", &cached_resolver).await;
// Both results should be different (different timestamps)
assert_ne!(result1, result2);
}
}
+2 -2
View File
@@ -612,7 +612,7 @@ struct ArgsBuilder {
"24"
)]
fn policy_is_allowed(policy: Policy, args: ArgsBuilder) -> bool {
policy.is_allowed(&Args {
pollster::block_on(policy.is_allowed(&Args {
account: &args.account,
groups: &{
if args.groups.is_empty() {
@@ -628,5 +628,5 @@ fn policy_is_allowed(policy: Policy, args: ArgsBuilder) -> bool {
object: &args.object,
claims: &args.claims,
deny_only: args.deny_only,
})
}))
}