Skip to main content

cloudillo_profile/
perm.rs

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