test(iam): add policy_is_allowed

This commit is contained in:
bestgopher
2024-12-23 21:46:25 +08:00
parent efed0a4884
commit 2126c36a73
20 changed files with 1454 additions and 212 deletions
+18 -13
View File
@@ -7,16 +7,18 @@ 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 inner in self.0.iter() {
let rvalues = values.get(inner.key.name().as_str()).map(|t| t.iter()).unwrap_or_default();
for r in rvalues {
let Ok(ip) = r.parse::<IpAddr>() else {
return false;
};
for 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;
for ip_net in inner.values.0.iter() {
if ip_net.contains(ip) {
return true;
}
}
}
}
@@ -47,7 +49,7 @@ impl<'de> Deserialize<'de> for AddrFuncValue {
where
E: serde::de::Error,
{
Ok(AddrFuncValue(vec![Self::to_cidr::<E>(v)?]))
Ok(AddrFuncValue(vec![Self::cidr::<E>(v)?]))
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
@@ -57,7 +59,7 @@ impl<'de> Deserialize<'de> for AddrFuncValue {
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.push(Self::cidr::<A::Error>(v)?)
}
data
}))
@@ -65,7 +67,7 @@ impl<'de> Deserialize<'de> for AddrFuncValue {
}
impl AddrFuncValueVisitor {
fn to_cidr<E: serde::de::Error>(v: &str) -> Result<IpNetwork, E> {
fn cidr<E: serde::de::Error>(v: &str) -> Result<IpNetwork, E> {
let mut cidr_str = Cow::from(v);
if v.find('/').is_none() {
cidr_str.to_mut().push_str("/32");
@@ -84,6 +86,7 @@ impl<'de> Deserialize<'de> for AddrFuncValue {
#[cfg(test)]
mod tests {
use super::{AddrFunc, AddrFuncValue};
use crate::policy::function::func::FuncKeyValue;
use crate::policy::function::{
key::Key,
key_name::AwsKeyName::*,
@@ -93,8 +96,10 @@ mod tests {
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()),
0: vec![FuncKeyValue {
key: Key { name, variable },
values: AddrFuncValue(value.into_iter().filter_map(|x| x.parse().ok()).collect()),
}],
}
}
+46 -11
View File
@@ -1,23 +1,34 @@
use super::func::InnerFunc;
use serde::de::{Error, IgnoredAny, SeqAccess};
use serde::{de, Deserialize, Deserializer, Serialize};
use std::{collections::HashMap, fmt};
pub type BoolFunc = InnerFunc<BoolFuncValue>;
impl BoolFunc {
pub fn evaluate_bool(&self, values: &HashMap<String, Vec<String>>) -> bool {
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,
for inner in self.0.iter() {
if !match values.get(inner.key.name().as_str()).and_then(|x| x.get(0)) {
Some(x) => inner.values.0.to_string().as_str() == x,
None => false,
} {
return false;
}
}
true
}
pub fn evaluate_null(&self, values: &HashMap<String, Vec<String>>) -> bool {
let len = values.get(self.key.name().as_str()).map(Vec::len).unwrap_or(0);
if self.values.0 {
return len == 0;
for inner in self.0.iter() {
let len = values.get(inner.key.name().as_str()).map(Vec::len).unwrap_or(0);
let r = if inner.values.0 { len == 0 } else { len != 0 };
if !r {
return false;
}
}
len != 0
true
}
}
@@ -50,17 +61,32 @@ impl<'de> Deserialize<'de> for BoolFuncValue {
fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
where
E: de::Error,
E: Error,
{
Ok(BoolFuncValue(value))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
E: Error,
{
Ok(BoolFuncValue(value.parse::<bool>().map_err(|e| E::custom(format!("{e:?}")))?))
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let Some(v) = seq.next_element::<BoolFuncValue>()? else {
return Err(Error::custom("no value for boolean"));
};
if seq.next_element::<IgnoredAny>()?.is_some() {
return Err(Error::custom("only allow one boolean value"));
}
Ok(v)
}
}
deserializer.deserialize_any(BoolOrStringVisitor)
@@ -70,6 +96,7 @@ impl<'de> Deserialize<'de> for BoolFuncValue {
#[cfg(test)]
mod tests {
use super::{BoolFunc, BoolFuncValue};
use crate::policy::function::func::FuncKeyValue;
use crate::policy::function::{
key::Key,
key_name::AwsKeyName::*,
@@ -79,8 +106,10 @@ mod tests {
fn new_func(name: KeyName, variable: Option<String>, value: bool) -> BoolFunc {
BoolFunc {
key: Key { name, variable },
values: BoolFuncValue(value),
0: vec![FuncKeyValue {
key: Key { name, variable },
values: BoolFuncValue(value),
}],
}
}
@@ -92,6 +121,8 @@ mod tests {
#[test_case(r#"{"aws:SecureTransport/a": "false"}"#, new_func(Aws(AWSSecureTransport), Some("a".into()), false); "10")]
#[test_case(r#"{"aws:SecureTransport/a": true}"#, new_func(Aws(AWSSecureTransport), Some("a".into()), true); "11")]
#[test_case(r#"{"aws:SecureTransport/a": false}"#, new_func(Aws(AWSSecureTransport), Some("a".into()), false); "12")]
#[test_case(r#"{"aws:SecureTransport/a": [true]}"#, new_func(Aws(AWSSecureTransport), Some("a".into()), true); "13")]
#[test_case(r#"{"aws:SecureTransport/a": ["false"]}"#, new_func(Aws(AWSSecureTransport), Some("a".into()), false); "14")]
fn test_deser(input: &str, expect: BoolFunc) -> Result<(), serde_json::Error> {
let v: BoolFunc = serde_json::from_str(input)?;
assert_eq!(v, expect);
@@ -103,6 +134,9 @@ mod tests {
#[test_case(r#"{"aws:usernamea/value":"johndoe"}"#)]
#[test_case(r#"{"aws:usernamea/value":["johndoe", "aaa"]}"#)]
#[test_case(r#""aaa""#)]
#[test_case(r#"{"aws:SecureTransport/a": ["false", "true"]}"#)]
#[test_case(r#"{"aws:SecureTransport/a": [true, false]}"#)]
#[test_case(r#"{"aws:SecureTransport/a": ["aa"]}"#)]
fn test_deser_failed(input: &str) {
assert!(serde_json::from_str::<BoolFunc>(input).is_err());
}
@@ -111,6 +145,7 @@ mod tests {
#[test_case(r#"{"aws:SecureTransport":"false"}"#, new_func(Aws(AWSSecureTransport), None, false);"2")]
#[test_case(r#"{"aws:SecureTransport/aa":"true"}"#, new_func(Aws(AWSSecureTransport),Some("aa".into()), true);"3")]
#[test_case(r#"{"aws:SecureTransport/aa":"false"}"#, new_func(Aws(AWSSecureTransport), Some("aa".into()), false);"4")]
# [test_case(r#"{"aws:SecureTransport/aa":"false"}"#, new_func(Aws(AWSSecureTransport), Some("aa".into()), false); "5")]
fn test_ser(expect: &str, input: BoolFunc) -> Result<(), serde_json::Error> {
let v = serde_json::to_string(&input)?;
assert_eq!(v.as_str(), expect);
+97 -9
View File
@@ -1,11 +1,12 @@
use serde::de::{Error, MapAccess};
use serde::ser::SerializeMap;
use serde::{Deserialize, Serialize, Serializer};
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)]
#[derive(Clone, Deserialize)]
pub enum Condition {
StringEquals(StringFunc),
StringNotEquals(StringFunc),
@@ -34,16 +35,73 @@ pub enum Condition {
}
impl Condition {
pub fn from_deserializer<'a, D: MapAccess<'a>>(key: &str, d: &mut D) -> Result<Self, D::Error> {
Ok(match key {
"StringEquals" => Self::StringEquals(d.next_value()?),
"StringNotEquals" => Self::StringNotEquals(d.next_value()?),
"StringEqualsIgnoreCase" => Self::StringEqualsIgnoreCase(d.next_value()?),
"StringNotEqualsIgnoreCase" => Self::StringNotEqualsIgnoreCase(d.next_value()?),
"StringLike" => Self::StringLike(d.next_value()?),
"StringNotLike" => Self::StringNotLike(d.next_value()?),
"BinaryEquals" => Self::BinaryEquals(d.next_value()?),
"IpAddress" => Self::IpAddress(d.next_value()?),
"NotIpAddress" => Self::NotIpAddress(d.next_value()?),
"Null" => Self::Null(d.next_value()?),
"Bool" => Self::Bool(d.next_value()?),
"NumericEquals" => Self::NumericEquals(d.next_value()?),
"NumericNotEquals" => Self::NumericNotEquals(d.next_value()?),
"NumericLessThan" => Self::NumericLessThan(d.next_value()?),
"NumericGreaterThan" => Self::NumericGreaterThan(d.next_value()?),
"NumericGreaterThanIfExists" => Self::NumericGreaterThanIfExists(d.next_value()?),
"NumericGreaterThanEquals" => Self::NumericGreaterThanEquals(d.next_value()?),
"DateEquals" => Self::DateEquals(d.next_value()?),
"DateNotEquals" => Self::DateNotEquals(d.next_value()?),
"DateLessThanEquals" => Self::DateLessThanEquals(d.next_value()?),
"DateGreaterThan" => Self::DateGreaterThan(d.next_value()?),
"DateGreaterThanEquals" => Self::DateGreaterThanEquals(d.next_value()?),
_ => Err(Error::custom(format!("unknown key: {key}")))?,
})
}
pub fn to_key(&self) -> &'static str {
match self {
Condition::StringEquals(_) => "StringEquals",
Condition::StringNotEquals(_) => "StringNotEquals",
Condition::StringEqualsIgnoreCase(_) => "StringEqualsIgnoreCase",
Condition::StringNotEqualsIgnoreCase(_) => "StringNotEqualsIgnoreCase",
Condition::StringLike(_) => "StringLike",
Condition::StringNotLike(_) => "StringNotLike",
Condition::BinaryEquals(_) => "BinaryEquals",
Condition::IpAddress(_) => "IpAddress",
Condition::NotIpAddress(_) => "NotIpAddress",
Condition::Null(_) => "Null",
Condition::Bool(_) => "Bool",
Condition::NumericEquals(_) => "NumericEquals",
Condition::NumericNotEquals(_) => "NumericNotEquals",
Condition::NumericLessThan(_) => "NumericLessThan",
Condition::NumericLessThanEquals(_) => "NumericLessThanEquals",
Condition::NumericGreaterThan(_) => "NumericGreaterThan",
Condition::NumericGreaterThanIfExists(_) => "NumericGreaterThanIfExists",
Condition::NumericGreaterThanEquals(_) => "NumericGreaterThanEquals",
Condition::DateEquals(_) => "DateEquals",
Condition::DateNotEquals(_) => "DateNotEquals",
Condition::DateLessThan(_) => "DateLessThan",
Condition::DateLessThanEquals(_) => "DateLessThanEquals",
Condition::DateGreaterThan(_) => "DateGreaterThan",
Condition::DateGreaterThanEquals(_) => "DateGreaterThanEquals",
}
}
pub fn evaluate(&self, for_all: bool, values: &HashMap<String, Vec<String>>) -> bool {
use Condition::*;
let r = match self {
StringEquals(s) => s.evaluate(for_all, false, false, 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),
StringEquals(s) => s.evaluate(for_all, false, false, false, values),
StringNotEquals(s) => s.evaluate(for_all, false, false, true, values),
StringEqualsIgnoreCase(s) => s.evaluate(for_all, true, false, false, values),
StringNotEqualsIgnoreCase(s) => s.evaluate(for_all, true, false, true, values),
StringLike(s) => s.evaluate(for_all, false, true, false, values),
StringNotLike(s) => s.evaluate(for_all, false, true, true, values),
BinaryEquals(s) => todo!(),
IpAddress(s) => s.evaluate(values),
NotIpAddress(s) => s.evaluate(values),
@@ -71,8 +129,38 @@ impl Condition {
}
}
#[inline]
pub fn is_negate(&self) -> bool {
use Condition::*;
matches!(self, StringNotEquals(_) | StringNotEqualsIgnoreCase(_) | NotIpAddress(_))
}
pub fn serialize_map<T: SerializeMap>(&self, se: &mut T) -> Result<(), T::Error> {
match self {
Condition::StringEquals(s) => se.serialize_value(s),
Condition::StringNotEquals(s) => se.serialize_value(s),
Condition::StringEqualsIgnoreCase(s) => se.serialize_value(s),
Condition::StringNotEqualsIgnoreCase(s) => se.serialize_value(s),
Condition::StringLike(s) => se.serialize_value(s),
Condition::StringNotLike(s) => se.serialize_value(s),
Condition::BinaryEquals(s) => se.serialize_value(s),
Condition::IpAddress(s) => se.serialize_value(s),
Condition::NotIpAddress(s) => se.serialize_value(s),
Condition::Null(s) => se.serialize_value(s),
Condition::Bool(s) => se.serialize_value(s),
Condition::NumericEquals(s) => se.serialize_value(s),
Condition::NumericNotEquals(s) => se.serialize_value(s),
Condition::NumericLessThan(s) => se.serialize_value(s),
Condition::NumericLessThanEquals(s) => se.serialize_value(s),
Condition::NumericGreaterThan(s) => se.serialize_value(s),
Condition::NumericGreaterThanIfExists(s) => se.serialize_value(s),
Condition::NumericGreaterThanEquals(s) => se.serialize_value(s),
Condition::DateEquals(s) => se.serialize_value(s),
Condition::DateNotEquals(s) => se.serialize_value(s),
Condition::DateLessThan(s) => se.serialize_value(s),
Condition::DateLessThanEquals(s) => se.serialize_value(s),
Condition::DateGreaterThan(s) => se.serialize_value(s),
Condition::DateGreaterThanEquals(s) => se.serialize_value(s),
}
}
}
+20 -15
View File
@@ -6,21 +6,23 @@ 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,
};
pub fn evaluate(&self, op: impl Fn(&OffsetDateTime, &OffsetDateTime) -> bool, values: &HashMap<String, Vec<String>>) -> bool {
for inner in self.0.iter() {
let v = match values.get(inner.key.name().as_str()).and_then(|x| x.get(0)) {
Some(x) => x,
None => return false,
};
let Ok(rv) = OffsetDateTime::parse(v, &Rfc3339) else {
return false;
};
let Ok(rv) = OffsetDateTime::parse(v, &Rfc3339) else {
return false;
};
op(&self.values.0, &rv)
if !op(&inner.values.0, &rv) {
return false;
}
}
true
}
}
@@ -74,6 +76,7 @@ impl<'de> Deserialize<'de> for DateFuncValue {
#[cfg(test)]
mod tests {
use super::{DateFunc, DateFuncValue};
use crate::policy::function::func::FuncKeyValue;
use crate::policy::function::{
key::Key,
key_name::KeyName::{self, *},
@@ -84,8 +87,10 @@ mod tests {
fn new_func(name: KeyName, variable: Option<String>, value: &str) -> DateFunc {
DateFunc {
key: Key { name, variable },
values: DateFuncValue(OffsetDateTime::parse(value, &Rfc3339).unwrap()),
0: vec![FuncKeyValue {
key: Key { name, variable },
values: DateFuncValue(OffsetDateTime::parse(value, &Rfc3339).unwrap()),
}],
}
}
+29 -28
View File
@@ -1,36 +1,22 @@
use std::{collections::HashMap, marker::PhantomData};
use std::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)),
}
}
}
use super::key::Key;
#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
pub struct InnerFunc<T> {
pub struct InnerFunc<T>(pub(crate) Vec<FuncKeyValue<T>>);
#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
pub struct FuncKeyValue<T> {
pub key: Key,
pub values: T,
}
impl<T: Clone> Clone for InnerFunc<T> {
impl<T: Clone> Clone for FuncKeyValue<T> {
fn clone(&self) -> Self {
Self {
key: self.key.clone(),
@@ -39,6 +25,12 @@ impl<T: Clone> Clone for InnerFunc<T> {
}
}
impl<T: Clone> Clone for InnerFunc<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T: Serialize> Serialize for InnerFunc<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
@@ -46,9 +38,13 @@ impl<T: Serialize> Serialize for InnerFunc<T> {
{
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_key(&self.key)?;
map.serialize_value(&self.values)?;
let mut map = serializer.serialize_map(Some(self.0.len()))?;
for kv in self.0.iter() {
map.serialize_key(&kv.key)?;
map.serialize_value(&kv.values)?;
}
map.end()
}
}
@@ -78,11 +74,16 @@ where
{
use serde::de::Error;
let Some((key, values)) = map.next_entry::<Key, T>()? else {
return Err(A::Error::custom("no k-v pair"));
};
let mut inner = Vec::with_capacity(map.size_hint().unwrap_or(0));
while let Some((key, values)) = map.next_entry::<Key, T>()? {
inner.push(FuncKeyValue { key, values });
}
Ok(InnerFunc { key, values })
if inner.is_empty() {
return Err(Error::custom("has no condition key"));
}
Ok(InnerFunc(inner))
}
}
+9 -5
View File
@@ -1,7 +1,6 @@
use serde::{Deserialize, Serialize};
use super::key_name::KeyName;
use crate::policy::{Error, Validator};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq, Eq))]
@@ -19,8 +18,8 @@ impl Key {
self.name.eq(other)
}
pub fn val_name(&self) -> String {
self.name.val_name()
pub fn var_name(&self) -> String {
self.name.var_name()
}
pub fn name(&self) -> String {
@@ -34,7 +33,12 @@ impl Key {
impl From<Key> for String {
fn from(value: Key) -> Self {
value.name()
let mut data = String::from(Into::<&str>::into(&value.name));
if let Some(x) = value.variable.as_ref() {
data.push('/');
data.push_str(&x);
}
data
}
}
+45 -13
View File
@@ -35,7 +35,7 @@ impl TryFrom<&str> for KeyName {
}
impl KeyName {
pub const COMMON_KEYS: &[KeyName] = &[
pub const COMMON_KEYS: &'static [KeyName] = &[
// s3
KeyName::S3(S3KeyName::S3SignatureVersion),
KeyName::S3(S3KeyName::S3AuthType),
@@ -82,8 +82,36 @@ impl KeyName {
KeyName::Jwt(JwtKeyName::JWTClientID),
];
pub fn name(&self) -> &str {
pub const fn prefix(&self) -> usize {
match self {
KeyName::Aws(_) => "aws:".len(),
KeyName::Jwt(_) => "jwt:".len(),
KeyName::Ldap(_) => "ldap:".len(),
KeyName::Sts(_) => "sts:".len(),
KeyName::Svc(_) => "svc:".len(),
KeyName::S3(_) => "s3:".len(),
}
}
pub fn name(&self) -> &str {
&Into::<&str>::into(self)[self.prefix()..]
}
pub fn var_name(&self) -> String {
match self {
KeyName::Aws(s) => format!("${{aws:{}}}", Into::<&str>::into(s)),
KeyName::Jwt(s) => format!("${{jwt:{}}}", Into::<&str>::into(s)),
KeyName::Ldap(s) => format!("${{ldap:{}}}", Into::<&str>::into(s)),
KeyName::Sts(s) => format!("${{sts:{}}}", Into::<&str>::into(s)),
KeyName::Svc(s) => format!("${{svc:{}}}", Into::<&str>::into(s)),
KeyName::S3(s) => format!("${{s3:{}}}", Into::<&str>::into(s)),
}
}
}
impl From<&KeyName> for &'static str {
fn from(k: &KeyName) -> Self {
match k {
KeyName::Aws(aws) => aws.into(),
KeyName::Jwt(jwt) => jwt.into(),
KeyName::Ldap(ldap) => ldap.into(),
@@ -92,17 +120,6 @@ impl KeyName {
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)]
@@ -137,6 +154,21 @@ pub enum S3KeyName {
#[strum(serialize = "s3:max-keys")]
S3MaxKeys,
#[strum(serialize = "s3:x-amz-metadata-directive")]
S3XAmzMetadataDirective,
#[strum(serialize = "s3:x-amz-storage-class")]
S3XAmzStorageClass,
#[strum(serialize = "s3:prefix")]
S3Prefix,
#[strum(serialize = "s3:delimiter")]
S3Delimiter,
#[strum(serialize = "s3:ExistingObjectTag")]
S3ExistingObjectTag,
}
#[derive(Clone, EnumString, Debug, IntoStaticStr, Eq, PartialEq, Serialize, Deserialize)]
+27 -18
View File
@@ -13,17 +13,23 @@ pub type NumberFunc = InnerFunc<NumberFuncValue>;
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,
};
pub fn evaluate(&self, op: impl Fn(&i64, &i64) -> bool, if_exists: bool, values: &HashMap<String, Vec<String>>) -> bool {
for inner in self.0.iter() {
let v = match values.get(inner.key.name().as_str()).and_then(|x| x.get(0)) {
Some(x) => x,
None => return if_exists,
};
let Ok(rv) = v.parse::<i64>() else {
return false;
};
let Ok(rv) = v.parse::<i64>() else {
return false;
};
op(&rv, &self.values.0)
if !op(&rv, &inner.values.0) {
return false;
}
}
true
}
}
@@ -50,13 +56,6 @@ impl<'de> Deserialize<'de> for NumberFuncValue {
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,
@@ -70,6 +69,13 @@ impl<'de> Deserialize<'de> for NumberFuncValue {
{
Ok(NumberFuncValue(value as i64))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: Error,
{
Ok(NumberFuncValue(value.parse().map_err(|e| E::custom(format!("{e:?}")))?))
}
}
deserializer.deserialize_any(NumberVisitor)
@@ -79,6 +85,7 @@ impl<'de> Deserialize<'de> for NumberFuncValue {
#[cfg(test)]
mod tests {
use super::{NumberFunc, NumberFuncValue};
use crate::policy::function::func::FuncKeyValue;
use crate::policy::function::{
key::Key,
key_name::KeyName::{self, *},
@@ -88,8 +95,10 @@ mod tests {
fn new_func(name: KeyName, variable: Option<String>, value: i64) -> NumberFunc {
NumberFunc {
key: Key { name, variable },
values: NumberFuncValue(value),
0: vec![FuncKeyValue {
key: Key { name, variable },
values: NumberFuncValue(value),
}],
}
}
+223 -20
View File
@@ -5,17 +5,43 @@ 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::function::func::FuncKeyValue;
use crate::policy::utils::wildcard;
use serde::{de, ser::SerializeSeq, Deserialize, Deserializer, Serialize};
use super::{func::InnerFunc, key_name::KeyName};
pub type StringFunc = InnerFunc<StringFuncValue>;
impl StringFunc {
pub(crate) fn evaluate(
&self,
for_all: bool,
ignore_case: bool,
like: bool,
negate: bool,
values: &HashMap<String, Vec<String>>,
) -> bool {
for inner in self.0.iter() {
let result = if like {
inner.eval_like(for_all, values) ^ negate
} else {
inner.eval(for_all, ignore_case, values) ^ negate
};
if !result {
return false;
}
}
true
}
}
impl FuncKeyValue<StringFuncValue> {
fn eval(&self, for_all: bool, ignore_case: bool, values: &HashMap<String, Vec<String>>) -> bool {
let rvalues = values
// http.CanonicalHeaderKey ?
.get(self.key.name().as_str())
.map(|t| {
t.iter()
@@ -38,7 +64,7 @@ impl StringFunc {
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)),
Some(v) if !v.is_empty() => return Cow::Owned(c.to_mut().replace(&key.var_name(), v)),
_ => continue,
};
}
@@ -49,6 +75,7 @@ impl StringFunc {
.collect::<Set<_>>();
let ivalues = rvalues.intersection(&fvalues);
if for_all {
rvalues.is_empty() || rvalues.len() == ivalues.count()
} else {
@@ -67,7 +94,7 @@ impl StringFunc {
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)),
Some(v) if !v.is_empty() => return Cow::Owned(c.to_mut().replace(&key.var_name(), v)),
_ => continue,
};
}
@@ -88,20 +115,12 @@ impl StringFunc {
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>);
pub struct StringFuncValue(pub Set<String>);
impl Serialize for StringFuncValue {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
@@ -165,7 +184,7 @@ impl<'d> Deserialize<'d> for StringFuncValue {
if result.0.is_empty() {
use serde::de::Error;
return Err(D::Error::custom("empty"));
return Err(Error::custom("empty"));
}
Ok(result)
@@ -175,24 +194,34 @@ impl<'d> Deserialize<'d> for StringFuncValue {
#[cfg(test)]
mod tests {
use super::{StringFunc, StringFuncValue};
use crate::policy::function::func::FuncKeyValue;
use crate::policy::function::{
key::Key,
key_name::AwsKeyName::*,
key_name::KeyName::{self, *},
};
use crate::policy::function::key_name::S3KeyName::S3LocationConstraint;
use test_case::test_case;
fn new_func(name: KeyName, variable: Option<String>, values: Vec<&str>) -> StringFunc {
StringFunc {
key: Key { name, variable },
values: StringFuncValue(values.into_iter().map(|x| x.to_owned()).collect()),
0: vec![FuncKeyValue {
key: Key { name, variable },
values: StringFuncValue(values.into_iter().map(|x| x.to_owned()).collect()),
}],
}
}
#[test_case(r#"{"aws:username": "johndoe"}"#, new_func(Aws(AWSUsername), None, vec!["johndoe"]))]
#[test_case(r#"{"aws:username": ["johndoe", "aaa"]}"#, new_func(Aws(AWSUsername), None, vec!["johndoe", "aaa"]))]
#[test_case(r#"{"aws:username/value": "johndoe"}"#, new_func(Aws(AWSUsername), Some("value".into()), vec!["johndoe"]))]
#[test_case(r#"{"aws:username/value": ["johndoe", "aaa"]}"#, new_func(Aws(AWSUsername), Some("value".into()), vec!["johndoe", "aaa"]))]
#[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);
@@ -217,4 +246,178 @@ mod tests {
assert_eq!(v.as_str(), expect);
Ok(())
}
fn new_fkv(name: &str, values: Vec<&str>) -> FuncKeyValue<StringFuncValue> {
FuncKeyValue {
key: name.try_into().unwrap(),
values: StringFuncValue(values.into_iter().map(ToOwned::to_owned).collect()),
}
}
fn test_eval(
s: FuncKeyValue<StringFuncValue>,
for_all: bool,
ignore_case: bool,
negate: bool,
values: Vec<(&str, Vec<&str>)>,
) -> bool {
let result = s.eval(
for_all,
ignore_case,
&values
.into_iter()
.map(|(k, v)| (k.to_owned(), v.into_iter().map(ToOwned::to_owned).collect::<Vec<String>>()))
.collect(),
);
result ^ negate
}
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => true ; "1")]
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["yourbucket/myobject"])] => false ; "2")]
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![] => false ; "3")]
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("delimiter", vec!["/"])] => false ; "4")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-1"])] => true ; "5")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["ap-southeast-1"])] => true ; "6")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["us-east-1"])] => false ; "7")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![] => false ; "8")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("delimiter", vec!["/"])] => false ; "9")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![("groups", vec!["prod", "art"])] => true ; "10")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![("groups", vec!["art"])] => true ; "11")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![] => true ; "12")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![("delimiter", vec!["/"])] => true ; "13")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![("groups", vec!["prod", "art"])] => true ; "14")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![("groups", vec!["art"])] => true ; "15")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![] => false ; "16")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![("delimiter", vec!["/"])] => false ; "17")]
#[test_case(new_fkv("s3:LocationConstraint", vec![KeyName::S3(S3LocationConstraint).var_name().as_str()]), false, vec![("LocationConstraint", vec!["us-west-1"])] => true ; "18")]
#[test_case(new_fkv("s3:ExistingObjectTag/security", vec!["public"]), false, vec![("ExistingObjectTag/security", vec!["public"])] => true ; "19")]
#[test_case(new_fkv("s3:ExistingObjectTag/security", vec!["public"]), false, vec![("ExistingObjectTag/security", vec!["private"])] => false ; "20")]
#[test_case(new_fkv("s3:ExistingObjectTag/security", vec!["public"]), false, vec![("ExistingObjectTag/project", vec!["foo"])] => false ; "21")]
fn test_string_equals(s: FuncKeyValue<StringFuncValue>, for_all: bool, values: Vec<(&str, Vec<&str>)>) -> bool {
test_eval(s, for_all, false, false, values)
}
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => false ; "1")]
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["yourbucket/myobject"])] => true ; "2")]
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![] => true ; "3")]
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("delimiter", vec!["/"])] => true ; "4")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-1"])] => false ; "5")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["ap-southeast-1"])] => false ; "6")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["us-east-1"])] => true ; "7")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![] => true ; "8")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("delimiter", vec!["/"])] => true ; "9")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![("groups", vec!["prod", "art"])] => false ; "10")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![("groups", vec!["art"])] => false ; "11")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![] => false ; "12")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![("delimiter", vec!["/"])] => false ; "13")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![("groups", vec!["prod", "art"])] => false ; "14")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![("groups", vec!["art"])] => false ; "15")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![] => true ; "16")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![("delimiter", vec!["/"])] => true ; "17")]
fn test_string_not_equals(s: FuncKeyValue<StringFuncValue>, for_all: bool, values: Vec<(&str, Vec<&str>)>) -> bool {
test_eval(s, for_all, false, true, values)
}
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => true ; "1")]
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![("x-amz-copy-source", vec!["yourbucket/myobject"])] => false ; "2")]
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![] => false ; "3")]
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![("delimiter", vec!["/"])] => false ; "4")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-1"])] => true ; "5")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("LocationConstraint", vec!["ap-southeast-1"])] => true ; "6")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("LocationConstraint", vec!["us-east-1"])] => false ; "7")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![] => false ; "8")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("delimiter", vec!["/"])] => false ; "9")]
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![("groups", vec!["prod", "art"])] => true ; "10")]
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![("groups", vec!["art"])] => true ; "11")]
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![] => true ; "12")]
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![("delimiter", vec!["/"])] => true ; "13")]
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![("groups", vec!["prod", "art"])] => true ; "14")]
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![("groups", vec!["art"])] => true ; "15")]
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![] => false ; "16")]
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![("delimiter", vec!["/"])] => false ; "17")]
fn test_string_equals_ignore_case(s: FuncKeyValue<StringFuncValue>, for_all: bool, values: Vec<(&str, Vec<&str>)>) -> bool {
test_eval(s, for_all, true, false, values)
}
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => false ; "1")]
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![("x-amz-copy-source", vec!["yourbucket/myobject"])] => true ; "2")]
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![] => true ; "3")]
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![("delimiter", vec!["/"])] => true ; "4")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-1"])] => false ; "5")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("LocationConstraint", vec!["ap-southeast-1"])] => false ; "6")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("LocationConstraint", vec!["us-east-1"])] => true ; "7")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![] => true ; "8")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("delimiter", vec!["/"])] => true ; "9")]
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![("groups", vec!["prod", "art"])] => false ; "10")]
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![("groups", vec!["art"])] => false ; "11")]
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![] => false ; "12")]
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![("delimiter", vec!["/"])] => false ; "13")]
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![("groups", vec!["prod", "art"])] => false ; "14")]
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![("groups", vec!["art"])] => false ; "15")]
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![] => true ; "16")]
#[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![("delimiter", vec!["/"])] => true ; "17")]
fn test_string_not_equals_ignore_case(
s: FuncKeyValue<StringFuncValue>,
for_all: bool,
values: Vec<(&str, Vec<&str>)>,
) -> bool {
test_eval(s, for_all, true, true, values)
}
fn test_eval_like(s: FuncKeyValue<StringFuncValue>, for_all: bool, negate: bool, values: Vec<(&str, Vec<&str>)>) -> bool {
let result = s.eval_like(
for_all,
&values
.into_iter()
.map(|(k, v)| (k.to_owned(), v.into_iter().map(ToOwned::to_owned).collect::<Vec<String>>()))
.collect(),
);
result ^ negate
}
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => true ; "1")]
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["yourbucket/myobject"])] => false ; "2")]
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![] => false ; "3")]
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("delimiter", vec!["/"])] => false ; "4")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-1"])] => true ; "5")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["ap-southeast-1"])] => true ; "6")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["us-east-1"])] => false ; "7")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![] => false ; "8")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("delimiter", vec!["/"])] => false ; "9")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-2"])] => true ; "10")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![("groups", vec!["prod", "art"])] => true ; "11")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![("groups", vec!["art"])] => true ; "12")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![] => true ; "13")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![("delimiter", vec!["/"])] => true ; "14")]
#[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![("groups", vec!["prod", "art"])] => true ; "15")]
#[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![("groups", vec!["art"])] => true ; "16")]
#[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![] => false ; "17")]
#[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![("delimiter", vec!["/"])] => false ; "18")]
fn test_string_like(s: FuncKeyValue<StringFuncValue>, for_all: bool, values: Vec<(&str, Vec<&str>)>) -> bool {
test_eval_like(s, for_all, false, values)
}
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => false ; "1")]
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["yourbucket/myobject"])] => true ; "2")]
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![] => true ; "3")]
#[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("delimiter", vec!["/"])] => true ; "4")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-1"])] => false ; "5")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["ap-southeast-1"])] => false ; "6")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["us-east-1"])] => true ; "7")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![] => true ; "8")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("delimiter", vec!["/"])] => true ; "9")]
#[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-2"])] => false ; "10")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![("groups", vec!["prod", "art"])] => false ; "11")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![("groups", vec!["art"])] => false ; "12")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![] => false ; "13")]
#[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![("delimiter", vec!["/"])] => false ; "14")]
#[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![("groups", vec!["prod", "art"])] => false ; "15")]
#[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![("groups", vec!["art"])] => false ; "16")]
#[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![] => true ; "17")]
#[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![("delimiter", vec!["/"])] => true ; "18")]
fn test_string_not_like(s: FuncKeyValue<StringFuncValue>, for_all: bool, values: Vec<(&str, Vec<&str>)>) -> bool {
test_eval_like(s, for_all, true, values)
}
}