1use crate::prelude::*;
12use cloudillo_types::meta_adapter::FileView;
13use cloudillo_types::types::{AccessLevel, TokenScope};
14
15pub struct FileAccessResult {
17 pub file_view: FileView,
18 pub access_level: AccessLevel,
19 pub read_only: bool,
20}
21
22pub enum FileAccessError {
24 NotFound,
25 AccessDenied,
26 InternalError(String),
27}
28
29pub struct FileAccessCtx<'a> {
31 pub user_id_tag: &'a str,
32 pub tenant_id_tag: &'a str,
33 pub user_roles: &'a [Box<str>],
34}
35
36pub async fn walk_parent_chain_for_share(
42 app: &App,
43 tn_id: TnId,
44 file_id: &str,
45 user_id_tag: &str,
46) -> Option<AccessLevel> {
47 let mut current_id = file_id.to_string();
48 let mut depth = 0;
49 while depth < 20 {
50 match app.meta_adapter.read_file(tn_id, ¤t_id).await {
51 Ok(Some(file)) => {
52 if let Some(ref parent_id) = file.parent_id {
53 if let Ok(Some(perm)) = app
54 .meta_adapter
55 .check_share_access(tn_id, 'F', parent_id, 'U', user_id_tag)
56 .await
57 {
58 return Some(AccessLevel::from_perm_char(perm));
59 }
60 current_id = parent_id.to_string();
61 depth += 1;
62 } else {
63 break;
64 }
65 }
66 _ => break,
67 }
68 }
69 None
70}
71
72pub async fn check_share_for_file(
75 app: &App,
76 tn_id: TnId,
77 file_id: &str,
78 user_id_tag: &str,
79) -> Option<AccessLevel> {
80 if let Ok(Some(perm)) =
81 app.meta_adapter.check_share_access(tn_id, 'F', file_id, 'U', user_id_tag).await
82 {
83 return Some(AccessLevel::from_perm_char(perm));
84 }
85 walk_parent_chain_for_share(app, tn_id, file_id, user_id_tag).await
86}
87
88pub async fn get_access_level(
97 app: &App,
98 tn_id: TnId,
99 file_id: &str,
100 owner_id_tag: &str,
101 ctx: &FileAccessCtx<'_>,
102 inherited_share: Option<AccessLevel>,
103) -> AccessLevel {
104 if ctx.user_id_tag == owner_id_tag {
106 return AccessLevel::Write;
107 }
108
109 if let Ok(Some(perm)) = app
111 .meta_adapter
112 .check_share_access(tn_id, 'F', file_id, 'U', ctx.user_id_tag)
113 .await
114 {
115 return AccessLevel::from_perm_char(perm);
116 }
117 if let Some(level) = inherited_share {
119 return level;
120 }
121 if let Some(level) = walk_parent_chain_for_share(app, tn_id, file_id, ctx.user_id_tag).await {
123 return level;
124 }
125
126 if owner_id_tag == ctx.tenant_id_tag && !ctx.user_roles.is_empty() {
131 if ctx
132 .user_roles
133 .iter()
134 .any(|r| matches!(r.as_ref(), "leader" | "moderator" | "contributor"))
135 {
136 return AccessLevel::Write;
137 }
138 return AccessLevel::Read;
140 }
141
142 let action_key = format!("FSHR:{}:{}", file_id, ctx.user_id_tag);
144
145 match app.meta_adapter.get_action_by_key(tn_id, &action_key).await {
146 Ok(Some(action)) => {
147 if action.typ.as_ref() == "FSHR" {
149 match action.sub_typ.as_ref().map(AsRef::as_ref) {
151 Some("WRITE") => AccessLevel::Write,
152 Some("COMMENT") => AccessLevel::Comment,
153 _ => AccessLevel::Read,
154 }
155 } else {
156 AccessLevel::None
157 }
158 }
159 Ok(None) | Err(_) => AccessLevel::None,
160 }
161}
162
163pub async fn get_access_level_with_scope(
172 app: &App,
173 tn_id: TnId,
174 file_id: &str,
175 owner_id_tag: &str,
176 ctx: &FileAccessCtx<'_>,
177 scope: Option<&str>,
178 root_id: Option<&str>,
179) -> AccessLevel {
180 if let Some(scope_str) = scope {
182 if let Some(token_scope) = TokenScope::parse(scope_str) {
184 match &token_scope {
185 TokenScope::File { file_id: scope_file_id, access } => {
186 if scope_file_id == file_id {
188 return *access;
189 }
190
191 if let Some(root) = root_id
194 && scope_file_id.as_str() == root
195 {
196 return *access;
197 }
198
199 if let Ok(Some(perm)) = app
204 .meta_adapter
205 .check_share_access(tn_id, 'F', scope_file_id, 'F', file_id)
206 .await
207 {
208 return (*access).min(AccessLevel::from_perm_char(perm));
210 }
211
212 return AccessLevel::None;
214 }
215 TokenScope::ApkgPublish => {
216 return AccessLevel::None;
218 }
219 }
220 }
221 return AccessLevel::None;
223 }
224
225 get_access_level(app, tn_id, file_id, owner_id_tag, ctx, None).await
227}
228
229pub async fn check_file_access_with_scope(
238 app: &App,
239 tn_id: TnId,
240 file_id: &str,
241 ctx: &FileAccessCtx<'_>,
242 scope: Option<&str>,
243 via: Option<&str>,
244) -> Result<FileAccessResult, FileAccessError> {
245 use tracing::debug;
246
247 let file_view = match app.meta_adapter.read_file(tn_id, file_id).await {
249 Ok(Some(f)) => f,
250 Ok(None) => return Err(FileAccessError::NotFound),
251 Err(e) => return Err(FileAccessError::InternalError(e.to_string())),
252 };
253
254 let owner_id_tag = file_view
257 .owner
258 .as_ref()
259 .and_then(|p| if p.id_tag.is_empty() { None } else { Some(p.id_tag.as_ref()) })
260 .unwrap_or(ctx.tenant_id_tag);
261
262 debug!(file_id = file_id, user = ctx.user_id_tag, owner = owner_id_tag, scope = ?scope, "Checking file access");
263
264 let mut access_level = get_access_level_with_scope(
266 app,
267 tn_id,
268 file_id,
269 owner_id_tag,
270 ctx,
271 scope,
272 file_view.root_id.as_deref(),
273 )
274 .await;
275
276 if access_level == AccessLevel::None && file_view.visibility == Some('P') {
278 access_level = AccessLevel::Read;
279 }
280
281 if let Some(via_file_id) = via
283 && scope.is_none()
284 && access_level != AccessLevel::None
285 {
286 match app.meta_adapter.check_share_access(tn_id, 'F', via_file_id, 'F', file_id).await {
287 Ok(Some(perm)) => {
288 access_level = access_level.min(AccessLevel::from_perm_char(perm));
289 }
290 Ok(None) | Err(_) => {
291 access_level = AccessLevel::None;
293 }
294 }
295 }
296
297 if access_level == AccessLevel::None {
298 return Err(FileAccessError::AccessDenied);
299 }
300
301 let read_only = access_level != AccessLevel::Write && access_level != AccessLevel::Admin;
302
303 Ok(FileAccessResult { file_view, access_level, read_only })
304}
305
306pub enum ScopeCheck {
308 NoScope,
310 Allowed(AccessLevel),
312 Denied,
314}
315
316pub fn check_scope_allows_file(
322 scope: Option<&str>,
323 file_id: &str,
324 root_id: Option<&str>,
325) -> ScopeCheck {
326 let Some(scope_str) = scope else { return ScopeCheck::NoScope };
327 let Some(token_scope) = TokenScope::parse(scope_str) else { return ScopeCheck::Denied };
329 match &token_scope {
330 TokenScope::File { file_id: scope_file_id, access } => {
331 if scope_file_id == file_id {
333 return ScopeCheck::Allowed(*access);
334 }
335 if let Some(root) = root_id
337 && scope_file_id.as_str() == root
338 {
339 return ScopeCheck::Allowed(*access);
340 }
341 ScopeCheck::Denied
342 }
343 TokenScope::ApkgPublish => ScopeCheck::Denied,
344 }
345}
346
347pub fn check_scope_allows_create(scope: Option<&str>, root_id: Option<&str>) -> Result<(), Error> {
356 let Some(scope_str) = scope else { return Ok(()) };
357 let Some(token_scope) = TokenScope::parse(scope_str) else {
359 return Err(Error::PermissionDenied);
360 };
361 match &token_scope {
362 TokenScope::File { file_id: scope_file_id, access } => {
363 if *access != AccessLevel::Write {
364 return Err(Error::PermissionDenied);
365 }
366 match root_id {
367 Some(root) if root == scope_file_id => Ok(()),
368 _ => Err(Error::PermissionDenied),
369 }
370 }
371 TokenScope::ApkgPublish => Ok(()), }
373}
374
375