diff --git a/iam/src/manager.rs b/iam/src/manager.rs index ef855280..f4fc076c 100644 --- a/iam/src/manager.rs +++ b/iam/src/manager.rs @@ -17,7 +17,9 @@ use policy::{ arn::ARN, auth::{self, get_claims_from_token_with_secret, is_secret_key_valid, jwt_sign, Credentials, UserIdentity}, format::Format, - policy::{iam_policy_claim_name_sa, Policy, PolicyDoc, DEFAULT_POLICIES, EMBEDDED_POLICY_TYPE, INHERITED_POLICY_TYPE}, + policy::{ + default::DEFAULT_POLICIES, iam_policy_claim_name_sa, Policy, PolicyDoc, EMBEDDED_POLICY_TYPE, INHERITED_POLICY_TYPE, + }, }; use serde::{Deserialize, Serialize}; use serde_json::Value; diff --git a/policy/src/policy.rs b/policy/src/policy.rs index 04e9fd99..3685fca7 100644 --- a/policy/src/policy.rs +++ b/policy/src/policy.rs @@ -5,27 +5,22 @@ mod function; mod id; #[allow(clippy::module_inception)] mod policy; +mod principal; pub mod resource; pub mod statement; pub(crate) mod utils; -use std::collections::{HashMap, HashSet}; - -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 policy::*; +pub use principal::Principal; pub use resource::ResourceSet; - -use serde_json::Value; pub use statement::Statement; -use common::error::Result; - pub const EMBEDDED_POLICY_TYPE: &str = "embedded-policy"; pub const INHERITED_POLICY_TYPE: &str = "inherited-policy"; @@ -56,73 +51,3 @@ pub enum Error { #[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 { - type Error; - fn is_valid(&self) -> Result<()> { - Ok(()) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Args<'a> { - pub account: &'a str, - pub groups: &'a Option>, - pub action: Action, - pub bucket: &'a str, - pub conditions: &'a HashMap>, - pub is_owner: bool, - pub object: &'a str, - pub claims: &'a HashMap, - pub deny_only: bool, -} - -impl Args<'_> { - pub fn get_role_arn(&self) -> Option<&str> { - self.claims.get("roleArn").and_then(|x| x.as_str()) - } - pub fn get_policies(&self, policy_claim_name: &str) -> (HashSet, bool) { - get_policies_from_claims(self.claims, policy_claim_name) - } -} - -fn get_values_from_claims(claims: &HashMap, claim_name: &str) -> (HashSet, bool) { - let mut s = HashSet::new(); - if let Some(pname) = claims.get(claim_name) { - if let Some(pnames) = pname.as_array() { - for pname in pnames { - if let Some(pname_str) = pname.as_str() { - for pname in pname_str.split(',') { - let pname = pname.trim(); - if !pname.is_empty() { - s.insert(pname.to_string()); - } - } - } - } - return (s, true); - } else if let Some(pname_str) = pname.as_str() { - for pname in pname_str.split(',') { - let pname = pname.trim(); - if !pname.is_empty() { - s.insert(pname.to_string()); - } - } - return (s, true); - } - } - (s, false) -} - -fn get_policies_from_claims(claims: &HashMap, policy_claim_name: &str) -> (HashSet, bool) { - get_values_from_claims(claims, policy_claim_name) -} - -pub fn iam_policy_claim_name_sa() -> String { - "sa-policy".to_string() -} diff --git a/policy/src/policy/policy.rs b/policy/src/policy/policy.rs index 03a94fff..f1de3790 100644 --- a/policy/src/policy/policy.rs +++ b/policy/src/policy/policy.rs @@ -1,7 +1,42 @@ -use super::{Args, Effect, Error as IamError, Statement, Validator, DEFAULT_VERSION, ID}; +use super::{action::Action, statement::BPStatement, Effect, Error as IamError, Statement, ID}; use common::error::{Error, Result}; use serde::{Deserialize, Serialize}; -use std::collections::HashSet; +use serde_json::Value; +use std::collections::{HashMap, HashSet}; + +/// DEFAULT_VERSION is the default version. +/// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_version.html +pub const DEFAULT_VERSION: &str = "2012-10-17"; + +/// check the data is Validator +pub trait Validator { + type Error; + fn is_valid(&self) -> Result<()> { + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Args<'a> { + pub account: &'a str, + pub groups: &'a Option>, + pub action: Action, + pub bucket: &'a str, + pub conditions: &'a HashMap>, + pub is_owner: bool, + pub object: &'a str, + pub claims: &'a HashMap, + pub deny_only: bool, +} + +impl Args<'_> { + pub fn get_role_arn(&self) -> Option<&str> { + self.claims.get("roleArn").and_then(|x| x.as_str()) + } + pub fn get_policies(&self, policy_claim_name: &str) -> (HashSet, bool) { + get_policies_from_claims(self.claims, policy_claim_name) + } +} #[derive(Serialize, Deserialize, Clone, Default, Debug)] pub struct Policy { @@ -118,6 +153,101 @@ impl Validator for Policy { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BucketPolicyArgs<'a> { + pub account: &'a str, + pub groups: &'a Option>, + pub action: Action, + pub bucket: &'a str, + pub conditions: &'a HashMap>, + pub is_owner: bool, + pub object: &'a str, +} + +#[derive(Serialize, Deserialize, Clone, Default, Debug)] +pub struct BucketPolicy { + #[serde(default, rename = "ID")] + pub id: ID, + #[serde(rename = "Version")] + pub version: String, + #[serde(rename = "Statement")] + pub statements: Vec, +} + +impl BucketPolicy { + pub 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) { + return false; + } + } + + if args.is_owner { + return true; + } + + for statement in self.statements.iter().filter(|s| matches!(s.effect, Effect::Allow)) { + if statement.is_allowed(args) { + return true; + } + } + + false + } +} + +impl Validator for BucketPolicy { + type Error = Error; + + fn is_valid(&self) -> Result<()> { + if !self.id.is_empty() && !self.id.eq(DEFAULT_VERSION) { + return Err(IamError::InvalidVersion(self.id.0.clone()).into()); + } + + for statement in self.statements.iter() { + statement.is_valid()?; + } + + Ok(()) + } +} + +fn get_values_from_claims(claims: &HashMap, claim_name: &str) -> (HashSet, bool) { + let mut s = HashSet::new(); + if let Some(pname) = claims.get(claim_name) { + if let Some(pnames) = pname.as_array() { + for pname in pnames { + if let Some(pname_str) = pname.as_str() { + for pname in pname_str.split(',') { + let pname = pname.trim(); + if !pname.is_empty() { + s.insert(pname.to_string()); + } + } + } + } + return (s, true); + } else if let Some(pname_str) = pname.as_str() { + for pname in pname_str.split(',') { + let pname = pname.trim(); + if !pname.is_empty() { + s.insert(pname.to_string()); + } + } + return (s, true); + } + } + (s, false) +} + +fn get_policies_from_claims(claims: &HashMap, policy_claim_name: &str) -> (HashSet, bool) { + get_values_from_claims(claims, policy_claim_name) +} + +pub fn iam_policy_claim_name_sa() -> String { + "sa-policy".to_string() +} + pub mod default { use std::{collections::HashSet, sync::LazyLock}; diff --git a/policy/src/policy/principal.rs b/policy/src/policy/principal.rs new file mode 100644 index 00000000..bf8087c3 --- /dev/null +++ b/policy/src/policy/principal.rs @@ -0,0 +1,32 @@ +use super::{utils::wildcard, Validator}; +use common::error::{Error, Result}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; + +#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq)] +#[serde(rename_all = "PascalCase", default)] +pub struct Principal { + #[serde(rename = "AWS")] + aws: HashSet, +} + +impl Principal { + pub fn is_match(&self, parincipal: &str) -> bool { + for pattern in self.aws.iter() { + if wildcard::is_simple_match(pattern, parincipal) { + return true; + } + } + false + } +} + +impl Validator for Principal { + type Error = Error; + fn is_valid(&self) -> Result<()> { + if self.aws.is_empty() { + return Err(Error::msg("Principal is empty")); + } + Ok(()) + } +} diff --git a/policy/src/policy/statement.rs b/policy/src/policy/statement.rs index 3d38329f..9b1db671 100644 --- a/policy/src/policy/statement.rs +++ b/policy/src/policy/statement.rs @@ -1,4 +1,7 @@ -use super::{action::Action, ActionSet, Args, Effect, Error as IamError, Functions, ResourceSet, Validator, ID}; +use super::{ + action::Action, ActionSet, Args, BucketPolicyArgs, Effect, Error as IamError, Functions, Principal, ResourceSet, Validator, + ID, +}; use common::error::{Error, Result}; use serde::{Deserialize, Serialize}; @@ -115,3 +118,86 @@ impl PartialEq for Statement { && self.conditions == other.conditions } } + +#[derive(Debug, Deserialize, Serialize, Default, Clone)] +#[serde(rename_all = "PascalCase", default)] +pub struct BPStatement { + #[serde(rename = "Sid", default)] + pub sid: ID, + #[serde(rename = "Effect")] + pub effect: Effect, + #[serde(rename = "Principal")] + pub principal: Principal, + #[serde(rename = "Action")] + pub actions: ActionSet, + #[serde(rename = "NotAction", default)] + pub not_actions: ActionSet, + #[serde(rename = "Resource", default)] + pub resources: ResourceSet, + #[serde(rename = "NotResource", default)] + pub not_resources: ResourceSet, + #[serde(rename = "Condition", default)] + pub conditions: Functions, +} + +impl BPStatement { + pub fn is_allowed(&self, args: &BucketPolicyArgs) -> bool { + let check = 'c: { + if !self.principal.is_match(args.account) { + break 'c false; + } + + 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.resources.is_empty() && !self.resources.is_match(&resource, args.conditions) { + break 'c false; + } + + if !self.not_resources.is_empty() && self.not_resources.is_match(&resource, args.conditions) { + break 'c false; + } + + self.conditions.evaluate(args.conditions) + }; + + self.effect.is_allowed(check) + } +} + +impl Validator for BPStatement { + type Error = Error; + fn is_valid(&self) -> Result<()> { + self.effect.is_valid()?; + // check sid + self.sid.is_valid()?; + + self.principal.is_valid()?; + + if self.actions.is_empty() && self.not_actions.is_empty() { + return Err(IamError::NonAction.into()); + } + + if self.resources.is_empty() { + return Err(IamError::NonResource.into()); + } + + self.actions.is_valid()?; + self.not_actions.is_valid()?; + self.resources.is_valid()?; + + Ok(()) + } +}