use crate::service::auth::authorizer::{iam_resource_arn, Authorizer};
use crate::store::traits::{PolicyStore, RoleStore, UserStore};
use crate::wami::policies::permissions_boundary::{
operations, DeletePermissionsBoundaryRequest, PrincipalType, PutPermissionsBoundaryRequest,
};
use std::sync::Arc;
use tokio::sync::RwLock;
use wami_core::actions::WamiAction;
use wami_core::context::WamiContext;
use wami_core::error::{AmiError, Result};
pub trait PermissionsBoundaryServiceStore: UserStore + RoleStore + PolicyStore {}
impl<T> PermissionsBoundaryServiceStore for T where T: UserStore + RoleStore + PolicyStore {}
#[wami_macros::service(
store_trait = "crate::service::policies::permissions_boundary::PermissionsBoundaryServiceStore",
generate_new = false
)]
pub struct PermissionsBoundaryService<S> {
store: Arc<RwLock<S>>,
#[allow(dead_code)] account_id: String,
authz: Option<Arc<dyn Authorizer>>,
}
impl<S> PermissionsBoundaryService<S>
where
S: PermissionsBoundaryServiceStore,
{
pub fn new(store: Arc<RwLock<S>>, account_id: String) -> Self {
Self {
store,
account_id,
authz: None,
}
}
pub fn with_authorizer(
store: Arc<RwLock<S>>,
account_id: String,
authz: Arc<dyn Authorizer>,
) -> Self {
Self {
store,
account_id,
authz: Some(authz),
}
}
async fn guard(
&self,
context: &WamiContext,
action: WamiAction,
resource_type: &str,
resource_id: &str,
) -> Result<()> {
if let Some(authz) = &self.authz {
let arn = iam_resource_arn(context, resource_type, resource_id)?;
authz.check_or_deny(context, action.as_str(), &arn).await?;
}
Ok(())
}
pub async fn put_permissions_boundary(
&self,
context: &WamiContext,
request: PutPermissionsBoundaryRequest,
) -> Result<()> {
let resource_type = match request.principal_type {
PrincipalType::User => "user",
PrincipalType::Role => "role",
};
self.guard(
context,
WamiAction::IamSetBoundary,
resource_type,
&request.principal_name,
)
.await?;
crate::wami::policies::permissions_boundary::PermissionsBoundary::validate_arn(
&request.permissions_boundary,
)?;
let store = self.read_store().await;
let policy = store
.get_policy(&request.permissions_boundary)
.await?
.ok_or_else(|| AmiError::ResourceNotFound {
resource: format!("Policy: {}", request.permissions_boundary),
})?;
operations::validate_boundary_policy(&policy)?;
drop(store);
match request.principal_type {
PrincipalType::User => {
let mut store = self.write_store().await;
let mut user = store
.get_user(&request.principal_name)
.await?
.ok_or_else(|| AmiError::ResourceNotFound {
resource: format!("User: {}", request.principal_name),
})?;
user.permissions_boundary = Some(request.permissions_boundary);
store.update_user(user).await?;
}
PrincipalType::Role => {
let mut store = self.write_store().await;
let mut role = store
.get_role(&request.principal_name)
.await?
.ok_or_else(|| AmiError::ResourceNotFound {
resource: format!("Role: {}", request.principal_name),
})?;
role.permissions_boundary = Some(request.permissions_boundary);
store.update_role(role).await?;
}
}
Ok(())
}
pub async fn delete_permissions_boundary(
&self,
context: &WamiContext,
request: DeletePermissionsBoundaryRequest,
) -> Result<()> {
let resource_type = match request.principal_type {
PrincipalType::User => "user",
PrincipalType::Role => "role",
};
self.guard(
context,
WamiAction::IamSetBoundary,
resource_type,
&request.principal_name,
)
.await?;
match request.principal_type {
PrincipalType::User => {
let mut store = self.write_store().await;
let mut user = store
.get_user(&request.principal_name)
.await?
.ok_or_else(|| AmiError::ResourceNotFound {
resource: format!("User: {}", request.principal_name),
})?;
user.permissions_boundary = None;
store.update_user(user).await?;
}
PrincipalType::Role => {
let mut store = self.write_store().await;
let mut role = store
.get_role(&request.principal_name)
.await?
.ok_or_else(|| AmiError::ResourceNotFound {
resource: format!("Role: {}", request.principal_name),
})?;
role.permissions_boundary = None;
store.update_role(role).await?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::service::auth::decision::{Decision, DenyReason};
use crate::store::memory::InMemoryWamiStore;
use crate::wami::identity::role::builder::build_role;
use crate::wami::identity::user::builder::build_user;
use crate::wami::policies::policy::builder::build_policy;
use std::sync::Arc;
use wami_core::arn::{TenantPath, WamiArn};
use wami_core::context::WamiContext;
fn test_context() -> WamiContext {
let arn: WamiArn = "arn:wami:.*:12345678:wami:123456789012:user/test"
.parse()
.unwrap();
WamiContext::builder()
.instance_id("123456789012")
.tenant_path(TenantPath::single(12345678))
.caller_arn(arn)
.is_root(false)
.build()
.unwrap()
}
#[tokio::test]
async fn test_put_boundary_on_user() {
let store = Arc::new(RwLock::new(InMemoryWamiStore::default()));
let context = test_context();
let service = PermissionsBoundaryService::new(store.clone(), "123456789012".to_string());
let user = build_user("alice".to_string(), Some("/".to_string()), &context).unwrap();
{
let mut s = store.write().await;
s.create_user(user).await.unwrap();
}
let policy_doc = r#"{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
}]
}"#;
let policy = build_policy(
"S3Boundary".to_string(),
policy_doc.to_string(),
Some("/".to_string()),
None,
None,
&context,
)
.unwrap();
{
let mut s = store.write().await;
s.create_policy(policy.clone()).await.unwrap();
}
let request = PutPermissionsBoundaryRequest {
principal_type: PrincipalType::User,
principal_name: "alice".to_string(),
permissions_boundary: policy.wami_arn.to_string(),
};
let result = service.put_permissions_boundary(&context, request).await;
match result {
Ok(_) => {
let s = store.read().await;
let updated_user = s.get_user("alice").await.unwrap().unwrap();
assert_eq!(
updated_user.permissions_boundary,
Some(policy.wami_arn.to_string())
);
}
Err(_) => {
}
}
}
#[tokio::test]
async fn test_put_boundary_on_role() {
let store = Arc::new(RwLock::new(InMemoryWamiStore::default()));
let context = test_context();
let service = PermissionsBoundaryService::new(store.clone(), "123456789012".to_string());
let assume_policy = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}"#;
let role = build_role(
"test-role".to_string(),
assume_policy.to_string(),
Some("/".to_string()),
None,
None,
&context,
)
.unwrap();
{
let mut s = store.write().await;
s.create_role(role).await.unwrap();
}
let policy_doc = r#"{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
}]
}"#;
let policy = build_policy(
"S3Boundary".to_string(),
policy_doc.to_string(),
Some("/".to_string()),
None,
None,
&context,
)
.unwrap();
{
let mut s = store.write().await;
s.create_policy(policy.clone()).await.unwrap();
}
let request = PutPermissionsBoundaryRequest {
principal_type: PrincipalType::Role,
principal_name: "test-role".to_string(),
permissions_boundary: policy.arn.clone(),
};
service
.put_permissions_boundary(&context, request)
.await
.unwrap();
let s = store.read().await;
let updated_role = s.get_role("test-role").await.unwrap().unwrap();
assert_eq!(updated_role.permissions_boundary, Some(policy.arn.clone()));
}
#[tokio::test]
async fn test_delete_boundary_from_role() {
let store = Arc::new(RwLock::new(InMemoryWamiStore::default()));
let context = test_context();
let service = PermissionsBoundaryService::new(store.clone(), "123456789012".to_string());
let assume_policy = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}"#;
let mut role = build_role(
"test-role".to_string(),
assume_policy.to_string(),
Some("/".to_string()),
None,
None,
&context,
)
.unwrap();
role.permissions_boundary = Some("arn:aws:iam::123456789012:policy/boundary".to_string());
{
let mut s = store.write().await;
s.create_role(role).await.unwrap();
}
let request = DeletePermissionsBoundaryRequest {
principal_type: PrincipalType::Role,
principal_name: "test-role".to_string(),
};
service
.delete_permissions_boundary(&context, request)
.await
.unwrap();
let s = store.read().await;
let updated_role = s.get_role("test-role").await.unwrap().unwrap();
assert_eq!(updated_role.permissions_boundary, None);
}
#[tokio::test]
async fn test_delete_boundary_from_user() {
let store = Arc::new(RwLock::new(InMemoryWamiStore::default()));
let context = test_context();
let service = PermissionsBoundaryService::new(store.clone(), "123456789012".to_string());
let mut user =
build_user("test-user".to_string(), Some("/".to_string()), &context).unwrap();
user.permissions_boundary = Some("arn:aws:iam::123456789012:policy/boundary".to_string());
{
let mut s = store.write().await;
s.create_user(user).await.unwrap();
}
service
.delete_permissions_boundary(
&context,
DeletePermissionsBoundaryRequest {
principal_type: PrincipalType::User,
principal_name: "test-user".to_string(),
},
)
.await
.unwrap();
let s = store.read().await;
let updated_user = s.get_user("test-user").await.unwrap().unwrap();
assert_eq!(updated_user.permissions_boundary, None);
}
#[tokio::test]
async fn test_delete_boundary_from_unknown_user() {
let store = Arc::new(RwLock::new(InMemoryWamiStore::default()));
let context = test_context();
let service = PermissionsBoundaryService::new(store.clone(), "123456789012".to_string());
let result = service
.delete_permissions_boundary(
&context,
DeletePermissionsBoundaryRequest {
principal_type: PrincipalType::User,
principal_name: "missing".to_string(),
},
)
.await;
assert!(matches!(result, Err(AmiError::ResourceNotFound { .. })));
}
#[tokio::test]
async fn test_put_boundary_invalid_arn() {
let store = Arc::new(RwLock::new(InMemoryWamiStore::default()));
let context = test_context();
let account_id = "123456789012";
let service = PermissionsBoundaryService::new(store, account_id.to_string());
let request = PutPermissionsBoundaryRequest {
principal_type: PrincipalType::User,
principal_name: "alice".to_string(),
permissions_boundary: "not-an-arn".to_string(),
};
let result = service.put_permissions_boundary(&context, request).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_put_boundary_nonexistent_policy() {
let store = Arc::new(RwLock::new(InMemoryWamiStore::default()));
let context = test_context();
let service = PermissionsBoundaryService::new(store.clone(), "123456789012".to_string());
let user = build_user("alice".to_string(), Some("/".to_string()), &context).unwrap();
{
let mut s = store.write().await;
s.create_user(user).await.unwrap();
}
let request = PutPermissionsBoundaryRequest {
principal_type: PrincipalType::User,
principal_name: "alice".to_string(),
permissions_boundary: "arn:aws:iam::123456789012:policy/nonexistent".to_string(),
};
let result = service.put_permissions_boundary(&context, request).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_put_boundary_nonexistent_user() {
let store = Arc::new(RwLock::new(InMemoryWamiStore::default()));
let context = test_context();
let service = PermissionsBoundaryService::new(store.clone(), "123456789012".to_string());
let policy_doc = r#"{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
}]
}"#;
let policy = build_policy(
"S3Boundary".to_string(),
policy_doc.to_string(),
Some("/".to_string()),
None,
None,
&context,
)
.unwrap();
{
let mut s = store.write().await;
s.create_policy(policy.clone()).await.unwrap();
}
let request = PutPermissionsBoundaryRequest {
principal_type: PrincipalType::User,
principal_name: "nonexistent".to_string(),
permissions_boundary: policy.arn,
};
let result = service.put_permissions_boundary(&context, request).await;
assert!(result.is_err());
}
use crate::service::auth::authorizer::Authorizer;
use async_trait::async_trait;
struct DenyAllAuthorizer;
#[async_trait]
impl Authorizer for DenyAllAuthorizer {
async fn authorize(
&self,
_context: &WamiContext,
_action: &str,
_resource_arn: &WamiArn,
) -> wami_core::error::Result<Decision> {
Ok(Decision::Deny(DenyReason::NoMatch))
}
}
#[tokio::test]
async fn test_guard_put_permissions_boundary_denied() {
let store = Arc::new(RwLock::new(InMemoryWamiStore::default()));
let service = PermissionsBoundaryService::with_authorizer(
store,
"123456789012".to_string(),
Arc::new(DenyAllAuthorizer),
);
let context = test_context();
let request = PutPermissionsBoundaryRequest {
principal_type: PrincipalType::User,
principal_name: "alice".to_string(),
permissions_boundary: "arn:aws:iam::123456789012:policy/boundary".to_string(),
};
let result = service.put_permissions_boundary(&context, request).await;
assert!(matches!(
result,
Err(wami_core::error::AmiError::AccessDenied { .. })
));
}
#[tokio::test]
async fn test_guard_delete_permissions_boundary_denied() {
let store = Arc::new(RwLock::new(InMemoryWamiStore::default()));
let service = PermissionsBoundaryService::with_authorizer(
store,
"123456789012".to_string(),
Arc::new(DenyAllAuthorizer),
);
let context = test_context();
let request = DeletePermissionsBoundaryRequest {
principal_type: PrincipalType::Role,
principal_name: "test-role".to_string(),
};
let result = service.delete_permissions_boundary(&context, request).await;
assert!(matches!(
result,
Err(wami_core::error::AmiError::AccessDenied { .. })
));
}
}