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    pub users: Option<Vec<UserPermissions>>,
43}
44
45impl Permissions {
46    /// Gets the highest level of permissions allowed.
47    pub fn max_level(&self) -> Option<PermissionLevel> {
48        self.anyone.into_iter().chain(self.user).reduce(std::cmp::max)
49    }
50}
51
52/// Returns an error if the user_id in the session does not exist in the DB, returns None otherwise
53///
54/// Used by the client to gracefully handle stale sessions
55pub 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
81/// Verify that user is authorized to access a ref at a given permission level.
82///
83/// It is safe to proceed if the result is `Ok`; otherwise, the requested action
84/// should be aborted.
85pub 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
94/// Is the user authorized to access a ref at a given permission level?
95///
96/// The result is an error if the ref does not exist.
97pub 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
108/// Gets the highest level of permissions allowed for a ref.
109pub 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    // Return 404 if the ref does not exist at all.
124    if level.is_none() {
125        ref_exists(ctx, ref_id).await?;
126    }
127
128    Ok(level)
129}
130
131/// Gets the permissions allowed for a ref.
132pub 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    // Return 404 if the ref does not exist at all.
146    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/// A new set of permissions to assign to a document.
192#[derive(Debug, Deserialize, TS)]
193pub struct NewPermissions {
194    /// Base permission level for any person, logged in or not.
195    pub anyone: Option<PermissionLevel>,
196
197    /// Permission levels for users.
198    ///
199    /// A mapping from user IDs to permission levels.
200    pub users: HashMap<String, PermissionLevel>,
201}
202
203/// Replaces the set of permissions for a ref.
204///
205/// Note that this function does not update/diff the permissions, it replaces them
206/// entirely. An exception is ownership which can never be revoked once granted.
207pub 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    // Because the first query deletes all permission entries for the ref
221    // *except* ownership, the second query will fail, and thus the whole
222    // transaction will fail and be rolled back, if the uniqueness constraint is
223    // violated by attempting to downgrade an ownership permission.
224    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
249/// Verify that the given ref exists.
250async 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
256/// Extracts an authenticated user from an HTTP request.
257///
258/// Note that the `firebase_auth` crate has an Axum feature with similar
259/// functionality, but we don't use it because it doesn't integrate well with the
260/// RPC service.
261pub 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}