postrust_server/state.rs
1//! Application state.
2
3use crate::error::Error;
4use crate::user::User;
5use postrust_auth::{JwtCache, JwtConfig, JwtKeyError};
6use postrust_core::{AppConfig, IsolationLevel, SchemaCache};
7use postrust_engine::State;
8use sqlx::{PgPool, Postgres, Transaction};
9use tokio::sync::{RwLock, RwLockReadGuard};
10
11/// Shared application state.
12///
13/// `#[non_exhaustive]` from 3.0.0: build one with [`AppState::new`], then
14/// assign any field that should differ from what the configuration says.
15#[non_exhaustive]
16pub struct AppState {
17 /// Database connection pool
18 pub pool: PgPool,
19 /// Cached schema metadata
20 pub schema_cache: RwLock<SchemaCache>,
21 /// Application configuration
22 pub config: AppConfig,
23 /// JWT configuration
24 pub jwt_config: JwtConfig,
25 /// Cache of validated tokens, when `jwt_cache_enabled` asks for one.
26 pub jwt_cache: Option<JwtCache>,
27}
28
29impl AppState {
30 /// State for `config`, serving `schema_cache` through `pool`.
31 ///
32 /// The JWT configuration and token cache are derived from `config`, as
33 /// [`build`](crate::build) derives them. Fails when the configured JWT
34 /// secret does not parse; see [`postrust_engine::jwt_config`]. With
35 /// `PGRST_JWT_JWKS_URL` set the keys are fetched when first needed, by
36 /// the request that needs them; [`build`](crate::build) also fetches them
37 /// before serving, and keeps them fresh in the background.
38 pub fn new(
39 pool: PgPool,
40 schema_cache: SchemaCache,
41 config: AppConfig,
42 ) -> Result<Self, JwtKeyError> {
43 let jwt_config = crate::jwks::attach(postrust_engine::jwt_config(&config)?, &config)?;
44 Ok(Self::with_jwt(pool, schema_cache, config, jwt_config))
45 }
46
47 /// [`AppState::new`], with the JWT configuration already made from
48 /// `config`.
49 pub(crate) fn with_jwt(
50 pool: PgPool,
51 schema_cache: SchemaCache,
52 config: AppConfig,
53 jwt_config: JwtConfig,
54 ) -> Self {
55 Self {
56 pool,
57 schema_cache: RwLock::new(schema_cache),
58 jwt_config,
59 jwt_cache: JwtCache::new(config.jwt_cache_enabled, config.jwt_cache_max_lifetime),
60 config,
61 }
62 }
63
64 /// Get a read lock on the schema cache.
65 pub async fn schema_cache(&self) -> RwLockReadGuard<'_, SchemaCache> {
66 self.schema_cache.read().await
67 }
68
69 /// Reload the schema cache. Used by the `db_channel_enabled` listener.
70 pub async fn reload_schema(&self) -> Result<(), postrust_core::Error> {
71 let new_cache = SchemaCache::load_with_search_path(
72 &self.pool,
73 &self.config.db_schemas,
74 &self.config.db_extra_search_path,
75 )
76 .await?;
77 let mut guard = self.schema_cache.write().await;
78 *guard = new_cache;
79 Ok(())
80 }
81
82 /// Get the default schema.
83 pub fn default_schema(&self) -> &str {
84 self.config.default_schema()
85 }
86
87 /// Get exposed schemas.
88 pub fn schemas(&self) -> &[String] {
89 &self.config.db_schemas
90 }
91
92 /// A transaction on the pool that runs as `user`, as a REST request
93 /// would: read-write, at the isolation level the configuration gives the
94 /// role, with `db_pre_request` run. See [`AppState::begin_as_with`].
95 pub async fn begin_as(&self, user: &User) -> Result<Transaction<'static, Postgres>, Error> {
96 self.begin_as_with(user, TxOptions::default()).await
97 }
98
99 /// A transaction on the pool that runs as `user`.
100 ///
101 /// The pool is logged in as the authenticator, and a query sent on it
102 /// runs as the authenticator: with every grant it has, and with no
103 /// `request.jwt.claims` for a row-level security policy to read, so the
104 /// policies written for the API's roles do not apply. This makes the
105 /// transaction the caller's before you send anything on it. The same
106 /// statements REST sends for a request, worked out by the same
107 /// [`Session`](postrust_core::Session): `SET TRANSACTION` where one is
108 /// asked for, the role, the claims, the facts about the request `user`
109 /// carries, the `search_path` and `app.settings.*`, and `db_pre_request`.
110 ///
111 /// Every one of them is local to the transaction. Commit it or drop it --
112 /// a dropped one is rolled back -- and the connection goes back to the
113 /// pool as the authenticator, with nothing set.
114 ///
115 /// What this does not do is make your handler untrusted code. It keeps
116 /// what a request *sends* inside the policy: every statement you run with
117 /// the caller's values in it runs as the caller. But the handler runs as
118 /// the authenticator, which may always switch back -- `RESET ROLE` on the
119 /// transaction is enough -- and it holds the pool. Treat it as you would a
120 /// `SECURITY DEFINER` function: code you wrote and reviewed, not something
121 /// the client controls.
122 ///
123 /// Fails as REST fails: a connection that cannot be had from the pool in
124 /// time is logged at error and answered 500 (`PGRST500`), and a session the database
125 /// refuses -- a role that does not exist, a pre-request function that
126 /// raises -- is answered as the database's error.
127 pub async fn begin_as_with(
128 &self,
129 user: &User,
130 options: TxOptions,
131 ) -> Result<Transaction<'static, Postgres>, Error> {
132 // Logged here, at error, as the REST pipeline logs it: the pool
133 // running dry or the database out of reach is the operator's to fix,
134 // and the client is told only that the service is unavailable. The
135 // line carries the cause, and the request span's `request_id` and
136 // `trace_id` with it.
137 let mut tx = self.pool.begin().await.map_err(|e| {
138 let e = postrust_core::db::DbError::from(e);
139 tracing::error!("Could not begin a transaction: {}", e);
140 postrust_core::Error::ConnectionPool(e.to_string())
141 })?;
142
143 user.session(&self.config)
144 .read_only(options.read_only)
145 .isolation(options.isolation)
146 .pre_request(options.run_pre_request)
147 .apply_sqlx(&mut tx)
148 .await
149 .map_err(|e| {
150 tracing::debug!("Refused before the query: {}", e);
151 Error::from(e)
152 })?;
153 Ok(tx)
154 }
155}
156
157/// How [`AppState::begin_as_with`] begins a transaction.
158///
159/// `#[non_exhaustive]`, so a knob can be added in a minor release: start from
160/// the default and change what should differ.
161///
162/// ```
163/// use postrust_core::IsolationLevel;
164/// use postrust_server::TxOptions;
165///
166/// let options = TxOptions::default()
167/// .read_only(true)
168/// .isolation(IsolationLevel::RepeatableRead);
169/// assert!(options.read_only && options.run_pre_request);
170/// ```
171#[derive(Clone, Debug, PartialEq, Eq)]
172#[non_exhaustive]
173pub struct TxOptions {
174 /// Whether the transaction is read-only, which PostgreSQL enforces for
175 /// everything the transaction reaches -- a volatile function, a trigger.
176 /// A write is refused with `25006`, answered 405 as REST answers it.
177 /// Off by default.
178 pub read_only: bool,
179 /// The isolation level, over the role's (`role_settings`) and the
180 /// server's (`PGRST_DB_TX_ISOLATION`). `None`, the default, leaves the
181 /// choice to them, as REST does.
182 pub isolation: Option<IsolationLevel>,
183 /// Whether to call `db_pre_request`, where one is configured. On by
184 /// default, as it is for every REST request: it is commonly where a
185 /// deployment refuses a caller, and a route that skips it skips that.
186 pub run_pre_request: bool,
187}
188
189impl Default for TxOptions {
190 fn default() -> Self {
191 Self {
192 read_only: false,
193 isolation: None,
194 run_pre_request: true,
195 }
196 }
197}
198
199impl TxOptions {
200 /// With `read_only` set. See [`TxOptions::read_only`](#structfield.read_only).
201 pub fn read_only(mut self, read_only: bool) -> Self {
202 self.read_only = read_only;
203 self
204 }
205
206 /// At `isolation`, whatever the configuration says.
207 pub fn isolation(mut self, isolation: IsolationLevel) -> Self {
208 self.isolation = Some(isolation);
209 self
210 }
211
212 /// With `run_pre_request` set.
213 pub fn run_pre_request(mut self, run_pre_request: bool) -> Self {
214 self.run_pre_request = run_pre_request;
215 self
216 }
217}
218
219impl State for AppState {
220 type Db = PgPool;
221 type SchemaCache<'a> = RwLockReadGuard<'a, SchemaCache>;
222
223 fn db(&self) -> &PgPool {
224 &self.pool
225 }
226
227 async fn schema_cache(&self) -> RwLockReadGuard<'_, SchemaCache> {
228 self.schema_cache.read().await
229 }
230
231 fn config(&self) -> &AppConfig {
232 &self.config
233 }
234
235 fn jwt_config(&self) -> &JwtConfig {
236 &self.jwt_config
237 }
238
239 fn jwt_cache(&self) -> Option<&JwtCache> {
240 self.jwt_cache.as_ref()
241 }
242}