1use 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::{IdTag, OptionalAuth};
12use cloudillo_core::file_access;
13use cloudillo_core::middleware::PermissionCheckOutput;
14use cloudillo_types::auth_adapter::AuthCtx;
15use cloudillo_types::types::FileAttrs;
16
17pub fn check_perm_file(
27 action: &'static str,
28) -> impl Fn(
29 State<App>,
30 IdTag,
31 TnId,
32 OptionalAuth,
33 Path<String>,
34 Request,
35 Next,
36) -> PermissionCheckOutput
37 + Clone {
38 move |state, id_tag, tn_id, auth, path, req, next| {
39 Box::pin(check_file_permission(state, id_tag, tn_id, auth, path, req, next, action))
40 }
41}
42
43#[allow(clippy::too_many_arguments)]
44async fn check_file_permission(
45 State(app): State<App>,
46 IdTag(tenant_id_tag): IdTag,
47 tn_id: TnId,
48 OptionalAuth(maybe_auth_ctx): OptionalAuth,
49 Path(file_id): Path<String>,
50 req: Request,
51 next: Next,
52 action: &str,
53) -> Result<Response, Error> {
54 use tracing::warn;
55
56 let (auth_ctx, subject_id_tag) = if let Some(auth_ctx) = maybe_auth_ctx {
58 let id_tag = auth_ctx.id_tag.clone();
59 (auth_ctx, id_tag)
60 } else {
61 let guest_ctx =
63 AuthCtx { tn_id, id_tag: "guest".into(), roles: vec![].into(), scope: None };
64 (guest_ctx, "guest".into())
65 };
66
67 let attrs = load_file_attrs(
69 &app,
70 tn_id,
71 &file_id,
72 &subject_id_tag,
73 &tenant_id_tag,
74 &auth_ctx.roles,
75 auth_ctx.scope.as_deref(),
76 )
77 .await?;
78
79 let environment = Environment::new();
81 let checker = app.permission_checker.read().await;
82
83 let full_action = format!("file:{}", action);
85
86 if !checker.has_permission(&auth_ctx, &full_action, &attrs, &environment) {
87 warn!(
88 subject = %auth_ctx.id_tag,
89 action = %full_action,
90 file_id = %file_id,
91 visibility = attrs.visibility,
92 owner_id_tag = %attrs.owner_id_tag,
93 access_level = ?attrs.access_level,
94 "File permission denied"
95 );
96 return Err(Error::PermissionDenied);
97 }
98
99 Ok(next.run(req).await)
100}
101
102async fn load_file_attrs(
104 app: &App,
105 tn_id: TnId,
106 file_or_variant_id: &str,
107 subject_id_tag: &str,
108 tenant_id_tag: &str,
109 subject_roles: &[Box<str>],
110 scope: Option<&str>,
111) -> ClResult<FileAttrs> {
112 use cloudillo_core::abac::VisibilityLevel;
113 use std::borrow::Cow;
114 use tracing::debug;
115
116 let file_id: Cow<str> = if file_or_variant_id.starts_with('b') {
118 debug!("Looking up file_id for variant_id: {}", file_or_variant_id);
120 let fid = app.meta_adapter.read_file_id_by_variant(tn_id, file_or_variant_id).await?;
121 debug!("Found file_id: {} for variant_id: {}", fid, file_or_variant_id);
122 Cow::Owned(fid.to_string())
123 } else {
124 Cow::Borrowed(file_or_variant_id)
125 };
126
127 let file_view = app.meta_adapter.read_file(tn_id, &file_id).await?;
129
130 let file_view = file_view.ok_or(Error::NotFound)?;
131
132 let owner_id_tag = file_view
135 .owner
136 .as_ref()
137 .and_then(|p| if p.id_tag.is_empty() { None } else { Some(p.id_tag.clone()) })
138 .unwrap_or_else(|| {
139 debug!("File has no owner, using tenant_id_tag: {}", tenant_id_tag);
140 tenant_id_tag.into()
141 });
142
143 let ctx = file_access::FileAccessCtx {
145 user_id_tag: subject_id_tag,
146 tenant_id_tag,
147 user_roles: subject_roles,
148 };
149 let access_level =
150 file_access::get_access_level_with_scope(app, tn_id, &file_id, &owner_id_tag, &ctx, scope)
151 .await;
152
153 let visibility: Box<str> = VisibilityLevel::from_char(file_view.visibility).as_str().into();
155
156 let (following, connected) = if subject_id_tag != "guest" && !subject_id_tag.is_empty() {
158 let opts = cloudillo_types::meta_adapter::ListProfileOptions {
160 id_tag: Some(subject_id_tag.to_string()),
161 ..Default::default()
162 };
163 match app.meta_adapter.list_profiles(tn_id, &opts).await {
164 Ok(profiles) => {
165 if let Some(profile) = profiles.first() {
166 let following = profile.following;
167 let connected = profile.connected.is_connected();
168 debug!(
169 subject = subject_id_tag,
170 owner = %owner_id_tag,
171 following = following,
172 connected = connected,
173 "Loaded relationship status for file permission check"
174 );
175 (following, connected)
176 } else {
177 debug!(subject = subject_id_tag, "Profile not found, assuming no relationship");
178 (false, false)
179 }
180 }
181 Err(e) => {
182 debug!(
183 subject = subject_id_tag,
184 error = %e,
185 "Failed to load profile, assuming no relationship"
186 );
187 (false, false)
188 }
189 }
190 } else {
191 (false, false)
192 };
193
194 Ok(FileAttrs {
195 file_id: file_view.file_id,
196 owner_id_tag,
197 mime_type: file_view.content_type.unwrap_or_else(|| "application/octet-stream".into()),
198 tags: file_view.tags.unwrap_or_default(),
199 visibility,
200 access_level,
201 following,
202 connected,
203 })
204}
205
206