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>>,
44}
45
46impl Permissions {
47 pub fn max_level(&self) -> Option<PermissionLevel> {
49 self.anyone.into_iter().chain(self.user).reduce(std::cmp::max)
50 }
51}
52
53pub async fn validate_session(ctx: AppCtx) -> Result<(), AppError> {
57 let user_id = match ctx.user.as_ref().map(|u| u.user_id.clone()) {
58 Some(id) => id,
59 None => {
60 return Ok(());
61 }
62 };
63
64 let exists = sqlx::query_scalar!(
65 r#"
66 SELECT EXISTS (
67 SELECT 1 FROM users WHERE id = $1
68 )
69 "#,
70 user_id
71 )
72 .fetch_one(&ctx.state.db)
73 .await?;
74
75 if !exists.unwrap_or(false) {
76 return Err(AppError::Unauthorized);
77 }
78
79 Ok(())
80}
81
82pub async fn authorize(ctx: &AppCtx, ref_id: Uuid, level: PermissionLevel) -> Result<(), AppError> {
88 let authorized = is_authorized(ctx, ref_id, level).await?;
89 if authorized {
90 Ok(())
91 } else {
92 Err(AppError::Forbidden(ref_id))
93 }
94}
95
96pub async fn is_authorized(
101 ctx: &AppCtx,
102 ref_id: Uuid,
103 level: PermissionLevel,
104) -> Result<bool, AppError> {
105 match max_permission_level(ctx, ref_id).await? {
106 Some(max_level) => Ok(level <= max_level),
107 None => Ok(false),
108 }
109}
110
111pub async fn max_permission_level(
113 ctx: &AppCtx,
114 ref_id: Uuid,
115) -> Result<Option<PermissionLevel>, AppError> {
116 let query = sqlx::query_scalar!(
117 r#"
118 SELECT MAX(level) AS "max: PermissionLevel" FROM permissions
119 WHERE object = $1 AND (subject IS NULL OR subject = $2)
120 "#,
121 ref_id,
122 ctx.user.as_ref().map(|user| user.user_id.clone())
123 );
124 let level = query.fetch_one(&ctx.state.db).await?;
125
126 if level.is_none() {
128 ref_exists(ctx, ref_id).await?;
129 }
130
131 Ok(level)
132}
133
134pub async fn permissions(ctx: &AppCtx, ref_id: Uuid) -> Result<Permissions, AppError> {
136 let query = sqlx::query!(
137 r#"
138 SELECT subject as "user_id", username, display_name,
139 level as "level: PermissionLevel"
140 FROM permissions
141 LEFT OUTER JOIN users ON id = subject
142 WHERE object = $1
143 "#,
144 ref_id
145 );
146 let mut entries = query.fetch_all(&ctx.state.db).await?;
147
148 if entries.is_empty() {
150 ref_exists(ctx, ref_id).await?;
151 }
152
153 let mut anyone = None;
154 if let Some(i) = entries.iter().position(|entry| entry.user_id.is_none()) {
155 anyone = Some(entries.swap_remove(i).level);
156 }
157
158 let user_id = ctx.user.as_ref().map(|user| user.user_id.clone());
159 let mut user = None;
160 if let Some(i) = entries.iter().position(|entry| entry.user_id == user_id) {
161 user = Some(entries.swap_remove(i).level);
162 }
163
164 let mut users = None;
165 if user == Some(PermissionLevel::Own) {
166 users = Some(
167 entries
168 .into_iter()
169 .filter_map(|entry| {
170 if let Some(user_id) = entry.user_id {
171 Some(UserPermissions {
172 user: UserSummary {
173 id: user_id,
174 username: entry.username,
175 display_name: entry.display_name,
176 },
177 level: entry.level,
178 })
179 } else {
180 None
181 }
182 })
183 .collect(),
184 );
185 }
186
187 Ok(Permissions {
188 anyone,
189 user,
190 users,
191 })
192}
193
194#[derive(Debug, Deserialize, TS)]
196pub struct NewPermissions {
197 pub anyone: Option<PermissionLevel>,
199
200 pub users: HashMap<String, PermissionLevel>,
205}
206
207pub async fn set_permissions(
213 state: &AppState,
214 ref_id: Uuid,
215 new: NewPermissions,
216) -> Result<(), AppError> {
217 let mut levels: Vec<_> = new.users.values().cloned().collect();
218 let mut subjects: Vec<_> = new.users.into_keys().map(Some).collect();
219 if let Some(anyone) = new.anyone {
220 subjects.push(None);
221 levels.push(anyone);
222 }
223 let objects: Vec<_> = std::iter::repeat_n(ref_id, subjects.len()).collect();
224
225 let mut transaction = state.db.begin().await?;
230
231 let delete_query = sqlx::query!(
232 "
233 DELETE FROM permissions WHERE object = $1 AND level < 'own'
234 ",
235 ref_id,
236 );
237 delete_query.execute(&mut *transaction).await?;
238
239 let insert_query = sqlx::query!(
240 "
241 INSERT INTO permissions(subject, object, level)
242 SELECT * FROM UNNEST($1::text[], $2::uuid[], $3::permission_level[])
243 ",
244 &subjects as &[Option<String>],
245 &objects,
246 &levels as &[PermissionLevel],
247 );
248 insert_query.execute(&mut *transaction).await?;
249
250 transaction.commit().await?;
251 Ok(())
252}
253
254async fn ref_exists(ctx: &AppCtx, ref_id: Uuid) -> Result<(), AppError> {
256 let query = sqlx::query_scalar!("SELECT 1 FROM refs WHERE id = $1", ref_id);
257 query.fetch_one(&ctx.state.db).await?;
258 Ok(())
259}
260
261pub fn authenticate_from_request<T>(
268 firebase_auth: &FirebaseAuth,
269 req: &hyper::Request<T>,
270) -> Result<Option<FirebaseUser>, String> {
271 let maybe_auth_header = req
272 .headers()
273 .get(http::header::AUTHORIZATION)
274 .and_then(|value| value.to_str().ok());
275
276 maybe_auth_header
277 .map(|auth_header| {
278 let bearer = auth_header
279 .strip_prefix("Bearer ")
280 .ok_or_else(|| "Missing Bearer token".to_string())?;
281
282 firebase_auth
283 .verify(bearer)
284 .map_err(|err| format!("Failed to verify token: {}", err))
285 })
286 .transpose()
287}