Skip to main content

postrust_engine/
lib.rs

1//! The REST request pipeline of [Postrust](https://postrust.org/): an HTTP
2//! request in, the response the PostgREST-dialect API gives it out.
3//!
4//! No web framework is involved. The server (`postrust-server`) wraps this in
5//! axum, and the Lambda adapter reaches it through the server. It is meant to
6//! be shared with a host that cannot run axum -- a Cloudflare Worker -- too,
7//! once the database interface is driver-neutral; it does not build for wasm32
8//! yet, since it still talks to PostgreSQL through sqlx. See issue #50.
9//!
10//! ```text
11//! let response = postrust_engine::handle(&state, request).await;
12//! ```
13
14#![warn(missing_docs)]
15#![allow(clippy::result_large_err)]
16
17mod handler;
18pub mod observe;
19mod state;
20
21pub use handler::{allow, authenticate, authorization, error_response, handle};
22pub use state::State;
23
24use postrust_auth::jwks::{Fetch, JwksBuilder};
25use postrust_auth::{Jwks, JwtConfig, JwtKeyError, JwtKeys};
26use postrust_core::AppConfig;
27
28/// The JWT configuration an [`AppConfig`] describes.
29///
30/// Every host builds its [`State`] from one `AppConfig`; this is how each of
31/// them derives the JWT half, so the server, Lambda and the Worker cannot read
32/// the same settings differently.
33///
34/// Fails when `PGRST_JWT_SECRET` is a JWK or JWKS that does not parse, or is
35/// not the base64 `PGRST_JWT_SECRET_IS_BASE64` says it is. That is a
36/// configuration error, for a host to report before it serves anything.
37///
38/// Keys from `PGRST_JWT_JWKS_URL` are not here: fetching them takes an HTTP
39/// client, which is the host's. A host that has one attaches them with
40/// [`jwks_builder`] and [`JwtConfig::with_jwks`].
41pub fn jwt_config(config: &AppConfig) -> Result<JwtConfig, JwtKeyError> {
42    let keys = JwtKeys::from_config(config.jwt_secret.as_deref(), config.jwt_secret_is_base64)?;
43    Ok(jwt_config_with_keys(config, keys))
44}
45
46/// As [`jwt_config`], with keys the host has already parsed.
47///
48/// For a host that keeps parsed keys longer than one configuration lives: a
49/// Worker reads its configuration on every request, and decodes a JWKS only
50/// when the secret changes.
51pub fn jwt_config_with_keys(config: &AppConfig, keys: Option<JwtKeys>) -> JwtConfig {
52    let mut jwt = JwtConfig::default();
53    jwt.keys = keys;
54    jwt.audience = config.jwt_aud.clone();
55    jwt.issuer = config.jwt_iss.clone();
56    jwt.role_claim_key = config.jwt_role_claim_key.clone();
57    jwt.anon_role = config.db_anon_role.clone();
58    jwt
59}
60
61/// The JWKS `PGRST_JWT_JWKS_URL` names, to be fetched through `fetch`, or
62/// `None` when it is not set.
63///
64/// `keys` are the ones parsed from `PGRST_JWT_SECRET`, which stay accepted
65/// beside the fetched ones. Every host builds its [`Jwks`] here, so that the
66/// URL, the refresh interval and how the two sources combine are read the
67/// same way everywhere; the host adds what only it has (a store, a task).
68pub fn jwks_builder(
69    config: &AppConfig,
70    keys: Option<&JwtKeys>,
71    fetch: impl Fetch + 'static,
72) -> Option<JwksBuilder> {
73    let url = config.jwt_jwks_url.as_deref()?;
74    Some(
75        Jwks::builder(url, fetch)
76            .fixed(keys)
77            .refresh(std::time::Duration::from_secs(config.jwt_jwks_refresh)),
78    )
79}