Skip to main content

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}