1use axum::{
4 extract::{Path, Request, State},
5 middleware::Next,
6 response::Response,
7};
8
9use cloudillo_core::{
10 abac::Environment,
11 extract::{IdTag, OptionalAuth},
12 middleware::PermissionCheckOutput,
13};
14use cloudillo_types::auth_adapter::AuthCtx;
15use cloudillo_types::types::ActionAttrs;
16
17use crate::prelude::*;
18
19pub fn check_perm_action(
29 action: &'static str,
30) -> impl Fn(
31 State<App>,
32 TnId,
33 IdTag,
34 OptionalAuth,
35 Path<String>,
36 Request,
37 Next,
38) -> PermissionCheckOutput
39 + Clone {
40 move |state, tn_id, id_tag, auth, path, req, next| {
41 Box::pin(check_action_permission(state, tn_id, id_tag, auth, path, req, next, action))
42 }
43}
44
45#[allow(clippy::too_many_arguments)]
46async fn check_action_permission(
47 State(app): State<App>,
48 tn_id: TnId,
49 IdTag(tenant_id_tag): IdTag,
50 OptionalAuth(maybe_auth_ctx): OptionalAuth,
51 Path(action_id): Path<String>,
52 req: Request,
53 next: Next,
54 action: &str,
55) -> Result<Response, Error> {
56 use tracing::warn;
57
58 let (auth_ctx, subject_id_tag) = if let Some(auth_ctx) = maybe_auth_ctx {
60 let id_tag = auth_ctx.id_tag.clone();
61 (auth_ctx, id_tag)
62 } else {
63 let guest_ctx =
65 AuthCtx { tn_id, id_tag: "guest".into(), roles: vec![].into(), scope: None };
66 (guest_ctx, "guest".into())
67 };
68
69 let attrs = load_action_attrs(&app, tn_id, &action_id, &subject_id_tag, &tenant_id_tag).await?;
71
72 let environment = Environment::new();
74 let checker = app.permission_checker.read().await;
75
76 let full_action = format!("action:{}", action);
78
79 if !checker.has_permission(&auth_ctx, &full_action, &attrs, &environment) {
80 warn!(
81 subject = %auth_ctx.id_tag,
82 action = action,
83 action_id = %action_id,
84 visibility = attrs.visibility,
85 issuer_id_tag = %attrs.issuer_id_tag,
86 action_type = attrs.typ,
87 "Action permission denied"
88 );
89 return Err(Error::PermissionDenied);
90 }
91
92 Ok(next.run(req).await)
93}
94
95async fn load_action_attrs(
97 app: &App,
98 tn_id: TnId,
99 action_id: &str,
100 subject_id_tag: &str,
101 tenant_id_tag: &str,
102) -> ClResult<ActionAttrs> {
103 use cloudillo_core::abac::VisibilityLevel;
104 use tracing::debug;
105
106 let action_view = app.meta_adapter.get_action(tn_id, action_id).await?;
108
109 let action_view = action_view.ok_or(Error::NotFound)?;
110
111 let audience_tag = action_view
113 .audience
114 .as_ref()
115 .map(|p| vec![p.id_tag.clone()])
116 .unwrap_or_default();
117
118 let visibility: Box<str> = VisibilityLevel::from_char(action_view.visibility).as_str().into();
120
121 let (following, connected) = if subject_id_tag != "guest" && !subject_id_tag.is_empty() {
123 let opts = cloudillo_types::meta_adapter::ListProfileOptions {
125 id_tag: Some(subject_id_tag.to_string()),
126 ..Default::default()
127 };
128 match app.meta_adapter.list_profiles(tn_id, &opts).await {
129 Ok(profiles) => {
130 if let Some(profile) = profiles.first() {
131 let following = profile.following;
132 let connected = profile.connected.is_connected();
133 debug!(
134 subject = subject_id_tag,
135 issuer = %action_view.issuer.id_tag,
136 following = following,
137 connected = connected,
138 "Loaded relationship status for action permission check"
139 );
140 (following, connected)
141 } else {
142 debug!(subject = subject_id_tag, "Profile not found, assuming no relationship");
143 (false, false)
144 }
145 }
146 Err(e) => {
147 debug!(
148 subject = subject_id_tag,
149 error = %e,
150 "Failed to load profile, assuming no relationship"
151 );
152 (false, false)
153 }
154 }
155 } else {
156 (false, false)
157 };
158
159 Ok(ActionAttrs {
160 typ: action_view.typ,
161 sub_typ: action_view.sub_typ,
162 tenant_id_tag: tenant_id_tag.into(),
163 issuer_id_tag: action_view.issuer.id_tag,
164 parent_id: action_view.parent_id,
165 root_id: action_view.root_id,
166 audience_tag,
167 tags: vec![], visibility,
169 following,
170 connected,
171 })
172}