Skip to main content

cloudillo_core/
create_perm.rs

1//! Collection-level permission middleware for ABAC (CREATE operations)
2//!
3//! Validates CREATE permissions for resources that don't yet exist,
4//! based on subject attributes like quota, tier, and role.
5
6use axum::{
7	extract::{Request, State},
8	middleware::Next,
9	response::Response,
10};
11
12use crate::{abac::Environment, extract::Auth, middleware::PermissionCheckOutput, prelude::*};
13use cloudillo_types::types::SubjectAttrs;
14
15/// Middleware factory for collection permission checks
16///
17/// Returns a middleware function that validates CREATE permissions via ABAC.
18/// Evaluates collection-level policies based on subject attributes.
19///
20/// # Arguments
21/// * `resource_type` - The resource being created (e.g., "file", "action")
22/// * `action` - The permission action to check (e.g., "create")
23///
24/// # Returns
25/// A cloneable middleware function with return type `PermissionCheckOutput`
26pub fn check_perm_create(
27	resource_type: &'static str,
28	action: &'static str,
29) -> impl Fn(State<App>, Auth, Request, Next) -> PermissionCheckOutput + Clone {
30	move |state, auth, req, next| {
31		Box::pin(check_create_permission(state, auth, req, next, resource_type, action))
32	}
33}
34
35async fn check_create_permission(
36	State(app): State<App>,
37	Auth(auth_ctx): Auth,
38	req: Request,
39	next: Next,
40	resource_type: &str,
41	action: &str,
42) -> Result<Response, Error> {
43	use tracing::warn;
44
45	// Check if user has a role that allows content creation
46	// Minimum role for creating content: "contributor"
47	if !auth_ctx.roles.iter().any(|r| r.as_ref() == "contributor") {
48		warn!(
49			subject = %auth_ctx.id_tag,
50			resource_type = resource_type,
51			action = action,
52			roles = ?auth_ctx.roles,
53			"CREATE permission denied: requires at least 'contributor' role"
54		);
55		return Err(Error::PermissionDenied);
56	}
57
58	// Load subject attributes
59	let subject_attrs = load_subject_attrs(&app, &auth_ctx).await?;
60
61	// Create environment context
62	let environment = Environment::new();
63	let checker = app.permission_checker.read().await;
64
65	// Evaluate collection policy
66	if !checker.has_collection_permission(
67		&auth_ctx,
68		&subject_attrs,
69		resource_type,
70		action,
71		&environment,
72	) {
73		warn!(
74			subject = %auth_ctx.id_tag,
75			resource_type = resource_type,
76			action = action,
77			tier = %subject_attrs.tier,
78			quota_remaining_bytes = %subject_attrs.quota_remaining_bytes,
79			roles = ?subject_attrs.roles,
80			banned = subject_attrs.banned,
81			email_verified = subject_attrs.email_verified,
82			"CREATE permission denied"
83		);
84		return Err(Error::PermissionDenied);
85	}
86
87	Ok(next.run(req).await)
88}
89
90/// Load subject attributes for collection-level permission evaluation
91///
92/// Loads user's tier, quota, roles, and status from authentication/metadata.
93/// These attributes are used to evaluate CREATE operation permissions.
94async fn load_subject_attrs(
95	app: &App,
96	auth_ctx: &cloudillo_types::auth_adapter::AuthCtx,
97) -> ClResult<SubjectAttrs> {
98	// Check if user is banned by querying profile ban status
99	// Note: We use get_profile_info which returns ProfileData with status information
100	let banned = match app.meta_adapter.get_profile_info(auth_ctx.tn_id, &auth_ctx.id_tag).await {
101		Ok(_profile_data) => {
102			// Check if profile status indicates banned
103			// ProfileData.status is not available in current implementation,
104			// so we need to query more directly. For now, default to false.
105			// TODO: Extend ProfileData or add get_profile_ban_status method to adapter
106			false
107		}
108		Err(_) => {
109			// If profile doesn't exist locally, assume not banned
110			// (user might be from remote instance)
111			false
112		}
113	};
114
115	// Check if email is verified by checking tenant status
116	// If we can successfully read the tenant, they have been created and verified
117	let email_verified = match app.auth_adapter.read_tenant(&auth_ctx.id_tag).await {
118		Ok(_) => {
119			// If tenant exists and we can read it, assume verified
120			// In the current schema, tenant status 'A' means Active/verified
121			// TODO: Add explicit email_verified field to tenants table for better tracking
122			true
123		}
124		Err(_) => {
125			// If we can't read tenant, they may not be local or not verified
126			false
127		}
128	};
129
130	// Determine user tier based on roles
131	let tier: Box<str> = if auth_ctx.roles.iter().any(|r| r.as_ref() == "leader") {
132		"premium".into()
133	} else if auth_ctx.roles.iter().any(|r| r.as_ref() == "creator") {
134		"standard".into()
135	} else {
136		"free".into()
137	};
138
139	// Calculate quota remaining (in bytes)
140	// TODO: Query from meta_adapter to get user's actual used quota
141	let quota_bytes = match tier.as_ref() {
142		"premium" => 1024 * 1024 * 1024, // 1GB
143		"standard" => 100 * 1024 * 1024, // 100MB
144		_ => 10 * 1024 * 1024,           // 10MB (free tier)
145	};
146
147	// Get rate limit remaining (per hour)
148	// TODO: Query from meta_adapter or time-based tracker for actual rate limit tracking
149	let rate_limit_remaining_val = 100u32; // per hour
150
151	Ok(SubjectAttrs {
152		id_tag: auth_ctx.id_tag.clone(),
153		roles: auth_ctx.roles.to_vec(),
154		tier,
155		quota_remaining_bytes: quota_bytes.to_string().into(),
156		rate_limit_remaining: rate_limit_remaining_val.to_string().into(),
157		banned,
158		email_verified,
159	})
160}
161
162#[cfg(test)]
163mod tests {
164	use super::*;
165
166	#[test]
167	fn test_subject_attrs_creation() {
168		let attrs = SubjectAttrs {
169			id_tag: "alice".into(),
170			roles: vec!["creator".into()],
171			tier: "standard".into(),
172			quota_remaining_bytes: "100000000".into(),
173			rate_limit_remaining: "50".into(),
174			banned: false,
175			email_verified: true,
176		};
177
178		assert_eq!(attrs.id_tag.as_ref(), "alice");
179		assert_eq!(attrs.tier.as_ref(), "standard");
180		assert!(!attrs.banned);
181		assert!(attrs.email_verified);
182	}
183
184	#[test]
185	fn test_subject_attrs_implements_attr_set() {
186		use crate::abac::AttrSet;
187
188		let attrs = SubjectAttrs {
189			id_tag: "bob".into(),
190			roles: vec!["member".into(), "creator".into()],
191			tier: "premium".into(),
192			quota_remaining_bytes: "500000000".into(),
193			rate_limit_remaining: "95".into(),
194			banned: false,
195			email_verified: true,
196		};
197
198		// Test get()
199		assert_eq!(attrs.get("id_tag"), Some("bob"));
200		assert_eq!(attrs.get("tier"), Some("premium"));
201		assert_eq!(attrs.get("banned"), Some("false"));
202
203		// Test get_list()
204		let roles = attrs.get_list("roles");
205		assert!(roles.is_some());
206		assert_eq!(roles.unwrap().len(), 2);
207
208		// Test has()
209		assert!(attrs.has("tier", "premium"));
210		assert!(!attrs.has("tier", "free"));
211
212		// Test contains()
213		assert!(attrs.contains("roles", "creator"));
214		assert!(!attrs.contains("roles", "admin"));
215	}
216}
217
218// vim: ts=4