use axum::{
extract::{Request, State},
middleware::Next,
response::Response,
};
use crate::{abac::Environment, extract::Auth, middleware::PermissionCheckOutput, prelude::*};
use cloudillo_types::types::SubjectAttrs;
pub fn check_perm_create(
resource_type: &'static str,
action: &'static str,
) -> impl Fn(State<App>, Auth, Request, Next) -> PermissionCheckOutput + Clone {
move |state, auth, req, next| {
Box::pin(check_create_permission(state, auth, req, next, resource_type, action))
}
}
async fn check_create_permission(
State(app): State<App>,
Auth(auth_ctx): Auth,
req: Request,
next: Next,
resource_type: &str,
action: &str,
) -> Result<Response, Error> {
use tracing::warn;
if crate::file_access::scope_grants_collection_op(
auth_ctx.scope.as_deref(),
resource_type,
action,
) {
return Ok(next.run(req).await);
}
if !auth_ctx.roles.iter().any(|r| r.as_ref() == "contributor") {
warn!(
subject = %auth_ctx.id_tag,
resource_type = resource_type,
action = action,
roles = ?auth_ctx.roles,
"CREATE permission denied: requires at least 'contributor' role"
);
return Err(Error::PermissionDenied);
}
let subject_attrs = load_subject_attrs(&app, &auth_ctx).await?;
let environment = Environment::new();
let checker = app.permission_checker.read().await;
if !checker.has_collection_permission(
&auth_ctx,
&subject_attrs,
resource_type,
action,
&environment,
) {
warn!(
subject = %auth_ctx.id_tag,
resource_type = resource_type,
action = action,
tier = %subject_attrs.tier,
quota_remaining_bytes = %subject_attrs.quota_remaining_bytes,
roles = ?subject_attrs.roles,
banned = subject_attrs.banned,
email_verified = subject_attrs.email_verified,
"CREATE permission denied"
);
return Err(Error::PermissionDenied);
}
Ok(next.run(req).await)
}
async fn load_subject_attrs(
app: &App,
auth_ctx: &cloudillo_types::auth_adapter::AuthCtx,
) -> ClResult<SubjectAttrs> {
let banned = match app.meta_adapter.get_profile_info(auth_ctx.tn_id, &auth_ctx.id_tag).await {
Ok(_profile_data) => {
false
}
Err(_) => {
false
}
};
let email_verified = match app.auth_adapter.read_tenant(&auth_ctx.id_tag).await {
Ok(_) => {
true
}
Err(_) => {
false
}
};
let tier: Box<str> = if auth_ctx.roles.iter().any(|r| r.as_ref() == "leader") {
"premium".into()
} else if auth_ctx.roles.iter().any(|r| r.as_ref() == "creator") {
"standard".into()
} else {
"free".into()
};
let quota_bytes = match tier.as_ref() {
"premium" => 1024 * 1024 * 1024, "standard" => 100 * 1024 * 1024, _ => 10 * 1024 * 1024, };
let rate_limit_remaining_val = 100u32;
Ok(SubjectAttrs {
id_tag: auth_ctx.id_tag.clone(),
roles: auth_ctx.roles.to_vec(),
tier,
quota_remaining_bytes: quota_bytes.to_string().into(),
rate_limit_remaining: rate_limit_remaining_val.to_string().into(),
banned,
email_verified,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_subject_attrs_creation() {
let attrs = SubjectAttrs {
id_tag: "alice".into(),
roles: vec!["creator".into()],
tier: "standard".into(),
quota_remaining_bytes: "100000000".into(),
rate_limit_remaining: "50".into(),
banned: false,
email_verified: true,
};
assert_eq!(attrs.id_tag.as_ref(), "alice");
assert_eq!(attrs.tier.as_ref(), "standard");
assert!(!attrs.banned);
assert!(attrs.email_verified);
}
#[test]
fn test_subject_attrs_implements_attr_set() {
use crate::abac::AttrSet;
let attrs = SubjectAttrs {
id_tag: "bob".into(),
roles: vec!["member".into(), "creator".into()],
tier: "premium".into(),
quota_remaining_bytes: "500000000".into(),
rate_limit_remaining: "95".into(),
banned: false,
email_verified: true,
};
assert_eq!(attrs.get("id_tag"), Some("bob"));
assert_eq!(attrs.get("tier"), Some("premium"));
assert_eq!(attrs.get("banned"), Some("false"));
let roles = attrs.get_list("roles");
assert!(roles.is_some());
assert_eq!(roles.unwrap().len(), 2);
assert!(attrs.has("tier", "premium"));
assert!(!attrs.has("tier", "free"));
assert!(attrs.contains("roles", "creator"));
assert!(!attrs.contains("roles", "admin"));
}
}