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}