1use std::collections::HashMap;
2
3use firebase_auth::{FirebaseAuth, FirebaseUser};
4use serde::{Deserialize, Serialize};
5use ts_rs::TS;
6use uuid::Uuid;
7
8use super::app::{AppCtx, AppError, AppState};
9use super::user::UserSummary;
10
11#[derive(
13 Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, sqlx::Type, TS,
14)]
15#[sqlx(type_name = "permission_level", rename_all = "lowercase")]
16pub enum PermissionLevel {
17 Read,
18 Write,
19 Maintain,
20 Own,
21}
22
23#[derive(Clone, Debug, Serialize, TS)]
25pub struct UserPermissions {
26 pub user: UserSummary,
27 pub level: PermissionLevel,
28}
29
30#[derive(Clone, Debug, Serialize, TS)]
32pub struct Permissions {
33 pub anyone: Option<PermissionLevel>,
35
36 pub user: Option<PermissionLevel>,
38
39 pub users: Option<Vec<UserPermissions>>,
43}
44
45impl Permissions {
46 pub fn max_level(&self) -> Option<PermissionLevel> {
48 self.anyone.into_iter().chain(self.user).reduce(std::cmp::max)
49 }
50}
51
52pub async fn validate_session(ctx: AppCtx) -> Result<(), AppError> {
56 let user_id = match ctx.user.as_ref().map(|u| u.user_id.clone()) {
57 Some(id) => id,
58 None => {
59 return Ok(());
60 }
61 };
62
63 let exists = sqlx::query_scalar!(
64 r#"
65 SELECT EXISTS (
66 SELECT 1 FROM users WHERE id = $1
67 )
68 "#,
69 user_id
70 )
71 .fetch_one(&ctx.state.db)
72 .await?;
73
74 if !exists.unwrap_or(false) {
75 return Err(AppError::Unauthorized);
76 }
77
78 Ok(())
79}
80
81pub async fn authorize(ctx: &AppCtx, ref_id: Uuid, level: PermissionLevel) -> Result<(), AppError> {
86 let authorized = is_authorized(ctx, ref_id, level).await?;
87 if authorized {
88 Ok(())
89 } else {
90 Err(AppError::Forbidden(ref_id))
91 }
92}
93
94pub async fn is_authorized(
98 ctx: &AppCtx,
99 ref_id: Uuid,
100 level: PermissionLevel,
101) -> Result<bool, AppError> {
102 match max_permission_level(ctx, ref_id).await? {
103 Some(max_level) => Ok(level <= max_level),
104 None => Ok(false),
105 }
106}
107
108pub async fn max_permission_level(
110 ctx: &AppCtx,
111 ref_id: Uuid,
112) -> Result<Option<PermissionLevel>, AppError> {
113 let query = sqlx::query_scalar!(
114 r#"
115 SELECT MAX(level) AS "max: PermissionLevel" FROM permissions
116 WHERE object = $1 AND (subject IS NULL OR subject = $2)
117 "#,
118 ref_id,
119 ctx.user.as_ref().map(|user| user.user_id.clone())
120 );
121 let level = query.fetch_one(&ctx.state.db).await?;
122
123 if level.is_none() {
125 ref_exists(ctx, ref_id).await?;
126 }
127
128 Ok(level)
129}
130
131pub async fn permissions(ctx: &AppCtx, ref_id: Uuid) -> Result<Permissions, AppError> {
133 let query = sqlx::query!(
134 r#"
135 SELECT subject as "user_id", username, display_name,
136 level as "level: PermissionLevel"
137 FROM permissions
138 LEFT OUTER JOIN users ON id = subject
139 WHERE object = $1
140 "#,
141 ref_id
142 );
143 let mut entries = query.fetch_all(&ctx.state.db).await?;
144
145 if entries.is_empty() {
147 ref_exists(ctx, ref_id).await?;
148 }
149
150 let mut anyone = None;
151 if let Some(i) = entries.iter().position(|entry| entry.user_id.is_none()) {
152 anyone = Some(entries.swap_remove(i).level);
153 }
154
155 let user_id = ctx.user.as_ref().map(|user| user.user_id.clone());
156 let mut user = None;
157 if let Some(i) = entries.iter().position(|entry| entry.user_id == user_id) {
158 user = Some(entries.swap_remove(i).level);
159 }
160
161 let mut users = None;
162 if user == Some(PermissionLevel::Own) {
163 users = Some(
164 entries
165 .into_iter()
166 .filter_map(|entry| {
167 if let Some(user_id) = entry.user_id {
168 Some(UserPermissions {
169 user: UserSummary {
170 id: user_id,
171 username: entry.username,
172 display_name: entry.display_name,
173 },
174 level: entry.level,
175 })
176 } else {
177 None
178 }
179 })
180 .collect(),
181 );
182 }
183
184 Ok(Permissions {
185 anyone,
186 user,
187 users,
188 })
189}
190
191#[derive(Debug, Deserialize, TS)]
193pub struct NewPermissions {
194 pub anyone: Option<PermissionLevel>,
196
197 pub users: HashMap<String, PermissionLevel>,
201}
202
203pub async fn set_permissions(
208 state: &AppState,
209 ref_id: Uuid,
210 new: NewPermissions,
211) -> Result<(), AppError> {
212 let mut levels: Vec<_> = new.users.values().cloned().collect();
213 let mut subjects: Vec<_> = new.users.into_keys().map(Some).collect();
214 if let Some(anyone) = new.anyone {
215 subjects.push(None);
216 levels.push(anyone);
217 }
218 let objects: Vec<_> = std::iter::repeat_n(ref_id, subjects.len()).collect();
219
220 let mut transaction = state.db.begin().await?;
225
226 let delete_query = sqlx::query!(
227 "
228 DELETE FROM permissions WHERE object = $1 AND level < 'own'
229 ",
230 ref_id,
231 );
232 delete_query.execute(&mut *transaction).await?;
233
234 let insert_query = sqlx::query!(
235 "
236 INSERT INTO permissions(subject, object, level)
237 SELECT * FROM UNNEST($1::text[], $2::uuid[], $3::permission_level[])
238 ",
239 &subjects as &[Option<String>],
240 &objects,
241 &levels as &[PermissionLevel],
242 );
243 insert_query.execute(&mut *transaction).await?;
244
245 transaction.commit().await?;
246 Ok(())
247}
248
249async fn ref_exists(ctx: &AppCtx, ref_id: Uuid) -> Result<(), AppError> {
251 let query = sqlx::query_scalar!("SELECT 1 FROM refs WHERE id = $1", ref_id);
252 query.fetch_one(&ctx.state.db).await?;
253 Ok(())
254}
255
256pub fn authenticate_from_request<T>(
262 firebase_auth: &FirebaseAuth,
263 req: &hyper::Request<T>,
264) -> Result<Option<FirebaseUser>, String> {
265 let maybe_auth_header = req
266 .headers()
267 .get(http::header::AUTHORIZATION)
268 .and_then(|value| value.to_str().ok());
269
270 maybe_auth_header
271 .map(|auth_header| {
272 let bearer = auth_header
273 .strip_prefix("Bearer ")
274 .ok_or_else(|| "Missing Bearer token".to_string())?;
275
276 firebase_auth
277 .verify(bearer)
278 .map_err(|err| format!("Failed to verify token: {err}"))
279 })
280 .transpose()
281}