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 owner_id_tag == ctx.tenant_id_tag && !ctx.user_roles.is_empty() {
58 if ctx
59 .user_roles
60 .iter()
61 .any(|r| matches!(r.as_ref(), "leader" | "moderator" | "contributor"))
62 {
63 return AccessLevel::Write;
64 }
65 return AccessLevel::Read;
67 }
68
69 let action_key = format!("FSHR:{}:{}", file_id, ctx.user_id_tag);
71
72 match app.meta_adapter.get_action_by_key(tn_id, &action_key).await {
73 Ok(Some(action)) => {
74 if action.typ.as_ref() == "FSHR" {
76 if action.sub_typ.as_ref().map(|s| s.as_ref()) == Some("WRITE") {
78 AccessLevel::Write
79 } else {
80 AccessLevel::Read
81 }
82 } else {
83 AccessLevel::None
84 }
85 }
86 Ok(None) => AccessLevel::None,
87 Err(_) => AccessLevel::None,
88 }
89}
90
91pub async fn get_access_level_with_scope(
99 app: &App,
100 tn_id: TnId,
101 file_id: &str,
102 owner_id_tag: &str,
103 ctx: &FileAccessCtx<'_>,
104 scope: Option<&str>,
105) -> AccessLevel {
106 if let Some(scope_str) = scope {
108 if let Some(token_scope) = TokenScope::parse(scope_str) {
110 match &token_scope {
111 TokenScope::File { file_id: scope_file_id, access } => {
112 if scope_file_id == file_id {
114 return *access;
115 }
116 return AccessLevel::None;
119 }
120 }
121 }
122 }
124
125 get_access_level(app, tn_id, file_id, owner_id_tag, ctx).await
127}
128
129pub async fn check_file_access_with_scope(
138 app: &App,
139 tn_id: TnId,
140 file_id: &str,
141 ctx: &FileAccessCtx<'_>,
142 scope: Option<&str>,
143) -> Result<FileAccessResult, FileAccessError> {
144 use tracing::debug;
145
146 let file_view = match app.meta_adapter.read_file(tn_id, file_id).await {
148 Ok(Some(f)) => f,
149 Ok(None) => return Err(FileAccessError::NotFound),
150 Err(e) => return Err(FileAccessError::InternalError(e.to_string())),
151 };
152
153 let owner_id_tag = file_view
156 .owner
157 .as_ref()
158 .and_then(|p| if p.id_tag.is_empty() { None } else { Some(p.id_tag.as_ref()) })
159 .unwrap_or(ctx.tenant_id_tag);
160
161 debug!(file_id = file_id, user = ctx.user_id_tag, owner = owner_id_tag, scope = ?scope, "Checking file access");
162
163 let access_level =
165 get_access_level_with_scope(app, tn_id, file_id, owner_id_tag, ctx, scope).await;
166
167 if access_level == AccessLevel::None {
168 return Err(FileAccessError::AccessDenied);
169 }
170
171 let read_only = access_level == AccessLevel::Read;
172
173 Ok(FileAccessResult { file_view, access_level, read_only })
174}
175
176