Skip to main content

cloudillo_profile/
perm.rs

1//! Profile permission middleware for ABAC
2
3use axum::{
4	extract::{Path, Request, State},
5	middleware::Next,
6	response::Response,
7};
8
9use crate::prelude::*;
10use cloudillo_core::abac::Environment;
11use cloudillo_core::extract::Auth;
12use cloudillo_core::middleware::PermissionCheckOutput;
13use cloudillo_types::types::ProfileAttrs;
14
15/// Middleware factory for profile permission checks
16///
17/// Returns a middleware function that validates profile permissions via ABAC
18///
19/// # Arguments
20/// * `action` - The permission action to check (e.g., "read", "write")
21///
22/// # Returns
23/// A cloneable middleware function with return type `PermissionCheckOutput`
24pub fn check_perm_profile(
25	action: &'static str,
26) -> impl Fn(State<App>, Auth, Path<String>, Request, Next) -> PermissionCheckOutput + Clone {
27	move |state, auth, path, req, next| {
28		Box::pin(check_profile_permission(state, auth, path, req, next, action))
29	}
30}
31
32async fn check_profile_permission(
33	State(app): State<App>,
34	Auth(auth_ctx): Auth,
35	Path(id_tag): Path<String>,
36	req: Request,
37	next: Next,
38	action: &str,
39) -> Result<Response, Error> {
40	use tracing::warn;
41
42	// Load profile attributes (STUB - Phase 3 will implement)
43	let attrs = load_profile_attrs(&app, auth_ctx.tn_id, &id_tag, &auth_ctx.id_tag).await?;
44
45	// Check permission
46	let environment = Environment::new();
47	let checker = app.permission_checker.read().await;
48
49	// Format action as "profile:operation" for ABAC checker
50	let full_action = format!("profile:{}", action);
51
52	if !checker.has_permission(&auth_ctx, &full_action, &attrs, &environment) {
53		warn!(
54			subject = %auth_ctx.id_tag,
55			action = action,
56			target_id_tag = %id_tag,
57			owner_id_tag = %attrs.tenant_tag,
58			profile_type = attrs.profile_type,
59			roles = ?attrs.roles,
60			status = attrs.status,
61			"Profile permission denied"
62		);
63		return Err(Error::PermissionDenied);
64	}
65
66	Ok(next.run(req).await)
67}
68
69// Load profile attributes from MetaAdapter
70async fn load_profile_attrs(
71	app: &App,
72	tn_id: TnId,
73	id_tag: &str,
74	subject_id_tag: &str,
75) -> ClResult<ProfileAttrs> {
76	// Query subject's roles in this tenant
77	let subject_roles = app
78		.meta_adapter
79		.read_profile_roles(tn_id, subject_id_tag)
80		.await
81		.ok()
82		.flatten()
83		.map(|roles| roles.into_vec())
84		.unwrap_or_default();
85
86	// Get profile data from MetaAdapter - if not found, return default attrs
87	match app.meta_adapter.get_profile_info(tn_id, id_tag).await {
88		Ok(profile_data) => {
89			// Determine if subject is following or connected to target
90			// For now, default to false - in Phase 4 this will query relationship metadata
91			let following = false;
92			let connected = false;
93
94			Ok(ProfileAttrs {
95				id_tag: profile_data.id_tag,
96				profile_type: profile_data.r#type,
97				tenant_tag: id_tag.into(), // tenant_tag refers to the profile owner
98				roles: subject_roles,
99				status: "active".into(), // TODO: Query actual profile status from MetaAdapter
100				following,
101				connected,
102				visibility: "public".into(), // Profiles are publicly readable
103			})
104		}
105		Err(Error::NotFound) => {
106			// Profile doesn't exist locally - return default attrs
107			// This allows read operations to proceed (handler will return empty object)
108			Ok(ProfileAttrs {
109				id_tag: id_tag.into(),
110				profile_type: "person".into(),
111				tenant_tag: id_tag.into(),
112				roles: subject_roles,
113				status: "unknown".into(),
114				following: false,
115				connected: false,
116				visibility: "public".into(), // Profiles are publicly readable
117			})
118		}
119		Err(e) => Err(e),
120	}
121}
122
123// vim: ts=4