cloudillo_core/
file_access.rs1use crate::prelude::*;
9use cloudillo_types::meta_adapter::FileView;
10use cloudillo_types::types::{AccessLevel, TokenScope};
11
12pub struct FileAccessResult {
14 pub file_view: FileView,
15 pub access_level: AccessLevel,
16 pub read_only: bool,
17}
18
19pub enum FileAccessError {
21 NotFound,
22 AccessDenied,
23 InternalError(String),
24}
25
26pub struct FileAccessCtx<'a> {
28 pub user_id_tag: &'a str,
29 pub tenant_id_tag: &'a str,
30 pub user_roles: &'a [Box<str>],
31}
32
33pub async fn get_access_level(
42 app: &App,
43 tn_id: TnId,
44 file_id: &str,
45 owner_id_tag: &str,
46 ctx: &FileAccessCtx<'_>,
47) -> AccessLevel {
48 if ctx.user_id_tag == owner_id_tag {
50 return AccessLevel::Write;
51 }
52
53 if let Ok(Some(perm)) = app
55 .meta_adapter
56 .check_share_access(tn_id, 'F', file_id, 'U', ctx.user_id_tag)
57 .await
58 {
59 return AccessLevel::from_perm_char(perm);
60 }
61
62 if owner_id_tag == ctx.tenant_id_tag && !ctx.user_roles.is_empty() {
67 if ctx
68 .user_roles
69 .iter()
70 .any(|r| matches!(r.as_ref(), "leader" | "moderator" | "contributor"))
71 {
72 return AccessLevel::Write;
73 }
74 return AccessLevel::Read;
76 }
77
78 let action_key = format!("FSHR:{}:{}", file_id, ctx.user_id_tag);
80
81 match app.meta_adapter.get_action_by_key(tn_id, &action_key).await {
82 Ok(Some(action)) => {
83 if action.typ.as_ref() == "FSHR" {
85 if action.sub_typ.as_ref().map(AsRef::as_ref) == Some("WRITE") {
87 AccessLevel::Write
88 } else {
89 AccessLevel::Read
90 }
91 } else {
92 AccessLevel::None
93 }
94 }
95 Ok(None) | Err(_) => AccessLevel::None,
96 }
97}
98
99pub async fn get_access_level_with_scope(
108 app: &App,
109 tn_id: TnId,
110 file_id: &str,
111 owner_id_tag: &str,
112 ctx: &FileAccessCtx<'_>,
113 scope: Option<&str>,
114 root_id: Option<&str>,
115) -> AccessLevel {
116 if let Some(scope_str) = scope {
118 if let Some(token_scope) = TokenScope::parse(scope_str) {
120 match &token_scope {
121 TokenScope::File { file_id: scope_file_id, access } => {
122 if scope_file_id == file_id {
124 return *access;
125 }
126
127 if let Some(root) = root_id {
130 if scope_file_id.as_str() == root {
131 return *access;
132 }
133 }
134
135 if let Ok(Some(perm)) = app
140 .meta_adapter
141 .check_share_access(tn_id, 'F', scope_file_id, 'F', file_id)
142 .await
143 {
144 return (*access).min(AccessLevel::from_perm_char(perm));
146 }
147
148 return AccessLevel::None;
150 }
151 TokenScope::ApkgPublish => {
152 return AccessLevel::None;
154 }
155 }
156 }
157 return AccessLevel::None;
159 }
160
161 get_access_level(app, tn_id, file_id, owner_id_tag, ctx).await
163}
164
165pub async fn check_file_access_with_scope(
174 app: &App,
175 tn_id: TnId,
176 file_id: &str,
177 ctx: &FileAccessCtx<'_>,
178 scope: Option<&str>,
179 via: Option<&str>,
180) -> Result<FileAccessResult, FileAccessError> {
181 use tracing::debug;
182
183 let file_view = match app.meta_adapter.read_file(tn_id, file_id).await {
185 Ok(Some(f)) => f,
186 Ok(None) => return Err(FileAccessError::NotFound),
187 Err(e) => return Err(FileAccessError::InternalError(e.to_string())),
188 };
189
190 let owner_id_tag = file_view
193 .owner
194 .as_ref()
195 .and_then(|p| if p.id_tag.is_empty() { None } else { Some(p.id_tag.as_ref()) })
196 .unwrap_or(ctx.tenant_id_tag);
197
198 debug!(file_id = file_id, user = ctx.user_id_tag, owner = owner_id_tag, scope = ?scope, "Checking file access");
199
200 let mut access_level = get_access_level_with_scope(
202 app,
203 tn_id,
204 file_id,
205 owner_id_tag,
206 ctx,
207 scope,
208 file_view.root_id.as_deref(),
209 )
210 .await;
211
212 if access_level == AccessLevel::None && file_view.visibility == Some('P') {
214 access_level = AccessLevel::Read;
215 }
216
217 if let Some(via_file_id) = via {
219 if scope.is_none() && access_level != AccessLevel::None {
220 match app.meta_adapter.check_share_access(tn_id, 'F', via_file_id, 'F', file_id).await {
221 Ok(Some(perm)) => {
222 access_level = access_level.min(AccessLevel::from_perm_char(perm));
223 }
224 Ok(None) | Err(_) => {
225 access_level = AccessLevel::None;
227 }
228 }
229 }
230 }
231
232 if access_level == AccessLevel::None {
233 return Err(FileAccessError::AccessDenied);
234 }
235
236 let read_only = access_level == AccessLevel::Read;
237
238 Ok(FileAccessResult { file_view, access_level, read_only })
239}
240
241pub enum ScopeCheck {
243 NoScope,
245 Allowed(AccessLevel),
247 Denied,
249}
250
251pub fn check_scope_allows_file(
257 scope: Option<&str>,
258 file_id: &str,
259 root_id: Option<&str>,
260) -> ScopeCheck {
261 let Some(scope_str) = scope else { return ScopeCheck::NoScope };
262 let Some(token_scope) = TokenScope::parse(scope_str) else { return ScopeCheck::Denied };
264 match &token_scope {
265 TokenScope::File { file_id: scope_file_id, access } => {
266 if scope_file_id == file_id {
268 return ScopeCheck::Allowed(*access);
269 }
270 if let Some(root) = root_id {
272 if scope_file_id.as_str() == root {
273 return ScopeCheck::Allowed(*access);
274 }
275 }
276 ScopeCheck::Denied
277 }
278 TokenScope::ApkgPublish => ScopeCheck::Denied,
279 }
280}
281
282pub fn check_scope_allows_create(scope: Option<&str>, root_id: Option<&str>) -> Result<(), Error> {
291 let Some(scope_str) = scope else { return Ok(()) };
292 let Some(token_scope) = TokenScope::parse(scope_str) else {
294 return Err(Error::PermissionDenied);
295 };
296 match &token_scope {
297 TokenScope::File { file_id: scope_file_id, access } => {
298 if *access != AccessLevel::Write {
299 return Err(Error::PermissionDenied);
300 }
301 match root_id {
302 Some(root) if root == scope_file_id => Ok(()),
303 _ => Err(Error::PermissionDenied),
304 }
305 }
306 TokenScope::ApkgPublish => Ok(()), }
308}
309
310