catcolab_backend/
auth.rs

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/// Levels of permission that a user can have on a document.
12#[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/// Permissions of a user on a document.
24#[derive(Clone, Debug, Serialize, TS)]
25pub struct UserPermissions {
26    pub user: UserSummary,
27    pub level: PermissionLevel,
28}
29
30/// Permissions set on a document.
31#[derive(Clone, Debug, Serialize, TS)]
32pub struct Permissions {
33    /// Base permission level for any person, logged in or not.
34    pub anyone: Option<PermissionLevel>,
35
36    /// Permission level for the current user.
37    pub user: Option<PermissionLevel>,
38
39    /** Permission levels for all other users.
40
41    Only owners of the document have access to this information.
42     */
43    pub users: Option<Vec<UserPermissions>>,
44}
45
46impl Permissions {
47    /// Gets the highest level of permissions allowed.
48    pub fn max_level(&self) -> Option<PermissionLevel> {
49        self.anyone.into_iter().chain(self.user).reduce(std::cmp::max)
50    }
51}
52
53/// Returns an error if the user_id in the session does not exist in the DB, returns None otherwise
54///
55/// Used by the client to gracefully handle stale sessions
56pub 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
82/** Verify that user is authorized to access a ref at a given permission level.
83
84It is safe to proceed if the result is `Ok`; otherwise, the requested action
85should be aborted.
86 */
87pub 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
96/** Is the user authorized to access a ref at a given permission level?
97
98The result is an error if the ref does not exist.
99 */
100pub 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
111/// Gets the highest level of permissions allowed for a ref.
112pub 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    // Return 404 if the ref does not exist at all.
127    if level.is_none() {
128        ref_exists(ctx, ref_id).await?;
129    }
130
131    Ok(level)
132}
133
134/// Gets the permissions allowed for a ref.
135pub 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    // Return 404 if the ref does not exist at all.
149    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/// A new set of permissions to assign to a document.
195#[derive(Debug, Deserialize, TS)]
196pub struct NewPermissions {
197    /// Base permission level for any person, logged in or not.
198    pub anyone: Option<PermissionLevel>,
199
200    /** Permission levels for users.
201
202    A mapping from user IDs to permission levels.
203    */
204    pub users: HashMap<String, PermissionLevel>,
205}
206
207/** Replaces the set of permissions for a ref.
208
209Note that this function does not update/diff the permissions, it replaces them
210entirely. An exception is ownership which can never be revoked once granted.
211*/
212pub 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    // Because the first query deletes all permission entries for the ref
226    // *except* ownership, the second query will fail, and thus the whole
227    // transaction will fail and be rolled back, if the uniqueness constraint is
228    // violated by attempting to downgrade an ownership permission.
229    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
254/// Verify that the given ref exists.
255async 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
261/** Extracts an authenticated user from an HTTP request.
262
263Note that the `firebase_auth` crate has an Axum feature with similar
264functionality, but we don't use it because it doesn't integrate well with the
265RPC service.
266 */
267pub 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}