cloudillo_core/
file_access.rs1use 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 get_access_level(
45 app: &App,
46 tn_id: TnId,
47 file_id: &str,
48 owner_id_tag: &str,
49 ctx: &FileAccessCtx<'_>,
50) -> AccessLevel {
51 if ctx.user_id_tag == owner_id_tag {
53 return AccessLevel::Write;
54 }
55
56 if let Ok(Some(perm)) = app
58 .meta_adapter
59 .check_share_access(tn_id, 'F', file_id, 'U', ctx.user_id_tag)
60 .await
61 {
62 return AccessLevel::from_perm_char(perm);
63 }
64
65 if owner_id_tag == ctx.tenant_id_tag && !ctx.user_roles.is_empty() {
70 if ctx
71 .user_roles
72 .iter()
73 .any(|r| matches!(r.as_ref(), "leader" | "moderator" | "contributor"))
74 {
75 return AccessLevel::Write;
76 }
77 return AccessLevel::Read;
79 }
80
81 let action_key = format!("FSHR:{}:{}", file_id, ctx.user_id_tag);
83
84 match app.meta_adapter.get_action_by_key(tn_id, &action_key).await {
85 Ok(Some(action)) => {
86 if action.typ.as_ref() == "FSHR" {
88 match action.sub_typ.as_ref().map(AsRef::as_ref) {
90 Some("WRITE") => AccessLevel::Write,
91 Some("COMMENT") => AccessLevel::Comment,
92 _ => AccessLevel::Read,
93 }
94 } else {
95 AccessLevel::None
96 }
97 }
98 Ok(None) | Err(_) => AccessLevel::None,
99 }
100}
101
102pub async fn get_access_level_with_scope(
111 app: &App,
112 tn_id: TnId,
113 file_id: &str,
114 owner_id_tag: &str,
115 ctx: &FileAccessCtx<'_>,
116 scope: Option<&str>,
117 root_id: Option<&str>,
118) -> AccessLevel {
119 if let Some(scope_str) = scope {
121 if let Some(token_scope) = TokenScope::parse(scope_str) {
123 match &token_scope {
124 TokenScope::File { file_id: scope_file_id, access } => {
125 if scope_file_id == file_id {
127 return *access;
128 }
129
130 if let Some(root) = root_id {
133 if scope_file_id.as_str() == root {
134 return *access;
135 }
136 }
137
138 if let Ok(Some(perm)) = app
143 .meta_adapter
144 .check_share_access(tn_id, 'F', scope_file_id, 'F', file_id)
145 .await
146 {
147 return (*access).min(AccessLevel::from_perm_char(perm));
149 }
150
151 return AccessLevel::None;
153 }
154 TokenScope::ApkgPublish => {
155 return AccessLevel::None;
157 }
158 }
159 }
160 return AccessLevel::None;
162 }
163
164 get_access_level(app, tn_id, file_id, owner_id_tag, ctx).await
166}
167
168pub async fn check_file_access_with_scope(
177 app: &App,
178 tn_id: TnId,
179 file_id: &str,
180 ctx: &FileAccessCtx<'_>,
181 scope: Option<&str>,
182 via: Option<&str>,
183) -> Result<FileAccessResult, FileAccessError> {
184 use tracing::debug;
185
186 let file_view = match app.meta_adapter.read_file(tn_id, file_id).await {
188 Ok(Some(f)) => f,
189 Ok(None) => return Err(FileAccessError::NotFound),
190 Err(e) => return Err(FileAccessError::InternalError(e.to_string())),
191 };
192
193 let owner_id_tag = file_view
196 .owner
197 .as_ref()
198 .and_then(|p| if p.id_tag.is_empty() { None } else { Some(p.id_tag.as_ref()) })
199 .unwrap_or(ctx.tenant_id_tag);
200
201 debug!(file_id = file_id, user = ctx.user_id_tag, owner = owner_id_tag, scope = ?scope, "Checking file access");
202
203 let mut access_level = get_access_level_with_scope(
205 app,
206 tn_id,
207 file_id,
208 owner_id_tag,
209 ctx,
210 scope,
211 file_view.root_id.as_deref(),
212 )
213 .await;
214
215 if access_level == AccessLevel::None && file_view.visibility == Some('P') {
217 access_level = AccessLevel::Read;
218 }
219
220 if let Some(via_file_id) = via {
222 if scope.is_none() && access_level != AccessLevel::None {
223 match app.meta_adapter.check_share_access(tn_id, 'F', via_file_id, 'F', file_id).await {
224 Ok(Some(perm)) => {
225 access_level = access_level.min(AccessLevel::from_perm_char(perm));
226 }
227 Ok(None) | Err(_) => {
228 access_level = AccessLevel::None;
230 }
231 }
232 }
233 }
234
235 if access_level == AccessLevel::None {
236 return Err(FileAccessError::AccessDenied);
237 }
238
239 let read_only = access_level != AccessLevel::Write && access_level != AccessLevel::Admin;
240
241 Ok(FileAccessResult { file_view, access_level, read_only })
242}
243
244pub enum ScopeCheck {
246 NoScope,
248 Allowed(AccessLevel),
250 Denied,
252}
253
254pub fn check_scope_allows_file(
260 scope: Option<&str>,
261 file_id: &str,
262 root_id: Option<&str>,
263) -> ScopeCheck {
264 let Some(scope_str) = scope else { return ScopeCheck::NoScope };
265 let Some(token_scope) = TokenScope::parse(scope_str) else { return ScopeCheck::Denied };
267 match &token_scope {
268 TokenScope::File { file_id: scope_file_id, access } => {
269 if scope_file_id == file_id {
271 return ScopeCheck::Allowed(*access);
272 }
273 if let Some(root) = root_id {
275 if scope_file_id.as_str() == root {
276 return ScopeCheck::Allowed(*access);
277 }
278 }
279 ScopeCheck::Denied
280 }
281 TokenScope::ApkgPublish => ScopeCheck::Denied,
282 }
283}
284
285pub fn check_scope_allows_create(scope: Option<&str>, root_id: Option<&str>) -> Result<(), Error> {
294 let Some(scope_str) = scope else { return Ok(()) };
295 let Some(token_scope) = TokenScope::parse(scope_str) else {
297 return Err(Error::PermissionDenied);
298 };
299 match &token_scope {
300 TokenScope::File { file_id: scope_file_id, access } => {
301 if *access != AccessLevel::Write {
302 return Err(Error::PermissionDenied);
303 }
304 match root_id {
305 Some(root) if root == scope_file_id => Ok(()),
306 _ => Err(Error::PermissionDenied),
307 }
308 }
309 TokenScope::ApkgPublish => Ok(()), }
311}
312
313