Skip to main content

postrust_server/
user.rs

1//! The caller of a route of your own, as the REST API would see them.
2//!
3//! A hand-written route nested beside the API is reached by the same clients
4//! with the same tokens, and a table's row-level security policy was written
5//! for the role and claims the API runs a request as. [`User`] is the API's
6//! answer to "who is this?", taken from a request to your route: the token is
7//! checked by the function REST checks it with
8//! ([`postrust_engine::authenticate`]), through the same cache, with the same
9//! anonymous role and the same refusals. Handed to
10//! [`AppState::begin_as`](crate::AppState::begin_as), it is a transaction the
11//! policy applies to.
12
13use crate::error::Error;
14use crate::state::AppState;
15use axum::extract::{FromRequestParts, OptionalFromRequestParts, OriginalUri};
16use http::request::Parts;
17use indexmap::IndexMap;
18use postrust_auth::AuthResult;
19use postrust_core::trace::{TraceContext, TraceId};
20use std::collections::HashMap;
21use std::sync::Arc;
22
23/// Who a request to a route of your own is, authenticated as REST does.
24///
25/// ```no_run
26/// use axum::{extract::State, Json};
27/// use postrust_server::{AppState, User};
28/// use serde_json::{json, Value};
29/// use std::sync::Arc;
30///
31/// async fn summary(
32///     user: User,
33///     State(state): State<Arc<AppState>>,
34/// ) -> Result<Json<Value>, postrust_server::Error> {
35///     let mut tx = state.begin_as(&user).await?;
36///     let n: i64 = sqlx::query_scalar("select count(*) from todos")
37///         .fetch_one(&mut *tx)
38///         .await?;
39///     tx.commit().await?;
40///     Ok(Json(json!({ "todos": n })))
41/// }
42/// ```
43///
44/// As an extractor, `User` reads the `Authorization` header. A request with
45/// no token is the anonymous role (`PGRST_DB_ANON_ROLE`), or refused with
46/// PostgREST's 401 where there is none; a token that does not verify is
47/// refused with the status and body the API gives it -- PGRST301, PGRST303 and
48/// the rest, with the `WWW-Authenticate` challenge.
49///
50/// # `Option<User>`
51///
52/// `Option<User>` is for a route that does something different for a caller
53/// who has not signed in, whatever the server's anonymous role: it is `None`
54/// when the request sends no `Authorization` header at all, and `Some` with
55/// the token's user when it sends one that verifies. A token that does *not*
56/// verify is still refused, not read as `None`: a client whose token has
57/// expired should be told so, not quietly answered as somebody else. A route
58/// that wants the anonymous role for a request with no token takes `User`
59/// itself; see [`User::is_anonymous`].
60///
61/// # What else it carries
62///
63/// What REST tells the database about a request besides who sent it, so that
64/// a policy or function reading `request.headers`, `request.cookies`,
65/// `request.method`, `request.path` or `request.trace_id` sees the same thing
66/// in a transaction of yours that it sees in REST's:
67///
68/// - the method, and the path as the client sent it -- with the prefix your
69///   router was nested at, where REST's `request.path` is without `/api`,
70///   since that prefix is the API's mount rather than part of the resource;
71/// - every header, by its lowercase name, and the cookies;
72/// - the trace the request belongs to, from the server's request span.
73///
74/// Not the schema a `Accept-Profile` header names, nor the time zone a
75/// `Prefer: timezone` asks for: those are REST's request syntax, and a route
76/// of yours has its own. Its transaction's `search_path` starts at the default
77/// schema.
78#[derive(Clone, Debug)]
79#[non_exhaustive]
80pub struct User {
81    auth: AuthResult,
82    anonymous: bool,
83    method: String,
84    path: String,
85    headers: IndexMap<String, String>,
86    cookies: IndexMap<String, String>,
87    trace_id: Option<TraceId>,
88}
89
90impl User {
91    /// The database role the request runs as: the token's role claim, or the
92    /// anonymous role.
93    pub fn role(&self) -> &str {
94        &self.auth.role
95    }
96
97    /// The token's claims, every one of them, as `request.jwt.claims` carries
98    /// them. Empty for an anonymous request.
99    pub fn claims(&self) -> &HashMap<String, serde_json::Value> {
100        &self.auth.claims
101    }
102
103    /// One of the token's claims, where it has it.
104    pub fn claim(&self, name: &str) -> Option<&serde_json::Value> {
105        self.auth.claims.get(name)
106    }
107
108    /// Whether the request sent no token, and runs as the anonymous role.
109    pub fn is_anonymous(&self) -> bool {
110        self.anonymous
111    }
112
113    /// The trace the request belongs to, where the server's request span
114    /// gave it one.
115    pub fn trace_id(&self) -> Option<TraceId> {
116        self.trace_id
117    }
118
119    /// The transaction-making half of this, for
120    /// [`AppState::begin_as_with`](crate::AppState::begin_as_with).
121    pub(crate) fn session<'a>(
122        &self,
123        config: &'a postrust_core::AppConfig,
124    ) -> postrust_core::Session<'a> {
125        postrust_core::Session::new(config, self.auth.role.clone(), self.auth.claim_settings())
126            .request(self.method.clone(), self.path.clone())
127            .headers(&self.headers)
128            .cookies(&self.cookies)
129            .trace_id(self.trace_id.map(|t| t.to_string()))
130    }
131
132    /// The user `parts` authenticates as.
133    async fn from_parts(parts: &Parts, state: &AppState) -> Result<Self, Error> {
134        let auth = postrust_engine::authenticate(state, &parts.headers).await?;
135        // The path the client sent: a nested router sees its own prefix
136        // stripped from `parts.uri`, and axum keeps the original beside it.
137        let path = parts
138            .extensions
139            .get::<OriginalUri>()
140            .map(|uri| uri.path())
141            .unwrap_or_else(|| parts.uri.path())
142            .to_string();
143        Ok(Self {
144            auth,
145            anonymous: postrust_engine::authorization(&parts.headers).is_none(),
146            method: parts.method.to_string(),
147            path,
148            headers: request_headers(&parts.headers),
149            cookies: request_cookies(&parts.headers),
150            trace_id: parts
151                .extensions
152                .get::<TraceContext>()
153                .map(|context| context.trace_id),
154        })
155    }
156}
157
158impl FromRequestParts<Arc<AppState>> for User {
159    type Rejection = Error;
160
161    async fn from_request_parts(
162        parts: &mut Parts,
163        state: &Arc<AppState>,
164    ) -> Result<Self, Self::Rejection> {
165        Self::from_parts(parts, state).await
166    }
167}
168
169impl OptionalFromRequestParts<Arc<AppState>> for User {
170    type Rejection = Error;
171
172    async fn from_request_parts(
173        parts: &mut Parts,
174        state: &Arc<AppState>,
175    ) -> Result<Option<Self>, Self::Rejection> {
176        match postrust_engine::authorization(&parts.headers) {
177            None => Ok(None),
178            Some(_) => Self::from_parts(parts, state).await.map(Some),
179        }
180    }
181}
182
183/// Every header, as REST's `request.headers` has them: by name, lowercase as
184/// `http` keeps them, and only those whose value is visible ASCII. Of a
185/// header sent more than once the last is kept, as REST keeps it.
186fn request_headers(headers: &http::HeaderMap) -> IndexMap<String, String> {
187    headers
188        .iter()
189        .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.to_string(), v.to_string())))
190        .collect()
191}
192
193/// The `Cookie` header's pairs, as REST's `request.cookies` has them.
194fn request_cookies(headers: &http::HeaderMap) -> IndexMap<String, String> {
195    headers
196        .get(http::header::COOKIE)
197        .and_then(|v| v.to_str().ok())
198        .map(|s| {
199            s.split(';')
200                .filter_map(|cookie| {
201                    let (key, value) = cookie.trim().split_once('=')?;
202                    Some((key.to_string(), value.to_string()))
203                })
204                .collect()
205        })
206        .unwrap_or_default()
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    /// The two maps are written out here, where REST's are private to
214    /// `parse_request`. This is what keeps them the same: the request REST
215    /// would see, and what each makes of it.
216    #[test]
217    fn headers_and_cookies_are_read_as_rest_reads_them() {
218        let request = http::Request::get("/items")
219            .header("accept", "application/json")
220            .header("X-Tenant", "acme")
221            .header("x-repeated", "one")
222            .header("x-repeated", "two")
223            .header("cookie", "session=abc; theme=dark;broken; a=b=c")
224            .body(Vec::<u8>::new())
225            .unwrap();
226        let rest = postrust_core::parse_request(&request, "public", &["public".to_string()], None)
227            .expect("REST reads the request");
228
229        assert_eq!(request_headers(request.headers()), rest.headers);
230        assert_eq!(request_cookies(request.headers()), rest.cookies);
231        assert_eq!(rest.cookies.get("a").map(String::as_str), Some("b=c"));
232    }
233}