postrust_server/build.rs
1//! Building the application a host serves.
2//!
3//! Everything between a loaded configuration and a router answering requests:
4//! the pool, the schema cache, the REST surface, GraphQL and the layers around
5//! them. The `postrust` binary serves the result on a socket; the Lambda
6//! adapter hands the same router to the Lambda runtime. One path, so a request
7//! is answered the same way wherever it arrives -- the Lambda adapter used to
8//! be a second copy of the request path, and that copy bound no parameters and
9//! switched to no role.
10
11use crate::app::{self, handle_request};
12use crate::custom;
13use crate::state::AppState;
14use anyhow::Result;
15#[cfg(feature = "graphql")]
16use axum::routing::{get, post};
17use axum::{
18 extract::DefaultBodyLimit,
19 http::{HeaderValue, Method},
20 response::Json,
21 routing::any,
22 Router,
23};
24use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
25use std::sync::Arc;
26use tokio::sync::RwLock;
27use tower_http::cors::{AllowOrigin, Any as CorsAny, CorsLayer};
28use tracing::{error, info};
29
30/// What the host running the application can do beyond answering requests.
31///
32/// A server process lives as long as it likes and owns its sockets. A Lambda
33/// sandbox is frozen between invocations, so a task listening for `NOTIFY` is
34/// asleep exactly when the notification arrives, and API Gateway does not
35/// upgrade a connection to a WebSocket at all.
36#[derive(Clone, Copy, Debug)]
37#[non_exhaustive]
38pub struct Host {
39 /// Long-lived background work: the `NOTIFY` schema reloader
40 /// (`db_channel_enabled`) and the subscription broker's listener.
41 pub background_tasks: bool,
42 /// Upgrading a connection to a WebSocket, which GraphQL subscriptions are
43 /// served over.
44 pub websockets: bool,
45}
46
47impl Host {
48 /// A long-running server process.
49 pub const SERVER: Host = Host {
50 background_tasks: true,
51 websockets: true,
52 };
53
54 /// An AWS Lambda function: no background work, no WebSockets.
55 pub const LAMBDA: Host = Host {
56 background_tasks: false,
57 websockets: false,
58 };
59}
60
61/// The application, ready to serve.
62#[non_exhaustive]
63pub struct App {
64 /// Every route the configuration enables, with its layers and state.
65 pub router: Router,
66 /// The state the routes share, for anything served beside them -- the
67 /// binary's admin port answers health checks from it.
68 pub state: Arc<AppState>,
69}
70
71/// Connect, load the schema cache and build the router.
72///
73/// Fails rather than serving something partial when the database cannot be
74/// reached or a setting cannot be honoured. GraphQL is the exception: a schema
75/// that cannot be built is logged and the rest is served without it.
76pub async fn build(config: postrust_core::AppConfig, host: Host) -> Result<App> {
77 info!("Database: {}", mask_db_uri(&config.db_uri));
78
79 // Create database pool
80 let connect_options: PgConnectOptions = config
81 .db_uri
82 .parse()
83 .map_err(|e| anyhow::anyhow!("invalid database URI: {e}"))?;
84
85 // Prepared statements are sqlx's default and stay on unless asked. Turning
86 // them off is for a connection pooler in transaction mode -- PgBouncer and
87 // the like -- where the server connection a statement was prepared on is
88 // not the one the next query lands on, and every reuse fails with
89 // "prepared statement does not exist".
90 let connect_options = if config.db_prepared_statements {
91 connect_options
92 } else {
93 info!("Prepared statements disabled");
94 connect_options.statement_cache_capacity(0)
95 };
96
97 let pool = PgPoolOptions::new()
98 .max_connections(config.db_pool_size)
99 .acquire_timeout(std::time::Duration::from_secs(config.db_pool_timeout))
100 .connect_with(connect_options)
101 .await?;
102
103 info!("Connected to database");
104
105 // Load schema cache
106 let schema_cache = postrust_core::SchemaCache::load_with_search_path(
107 &pool,
108 &config.db_schemas,
109 &config.db_extra_search_path,
110 )
111 .await?;
112 info!("{}", schema_cache.summary());
113
114 // Create app state
115 let state = Arc::new(AppState {
116 pool,
117 schema_cache: RwLock::new(schema_cache),
118 config: config.clone(),
119 jwt_config: postrust_auth::JwtConfig {
120 secret: config.jwt_secret.clone(),
121 secret_is_base64: config.jwt_secret_is_base64,
122 audience: config.jwt_aud.clone(),
123 role_claim_key: config.jwt_role_claim_key.clone(),
124 anon_role: config.db_anon_role.clone(),
125 },
126 jwt_cache: postrust_auth::JwtCache::new(
127 config.jwt_cache_enabled,
128 config.jwt_cache_max_lifetime,
129 ),
130 });
131
132 // `db_channel_enabled`: reload the schema cache when PostgreSQL says the
133 // schema changed, instead of on a restart. A migration ends with
134 // `NOTIFY pgrst, 'reload schema'` and the API picks the new shape up.
135 //
136 // The REST cache is swapped here; each reload then bumps `reloaded`, and
137 // the GraphQL schema, built from its own snapshot of the cache, is
138 // rebuilt from the new one when it sees the bump.
139 let (reloaded, schema_reloads) = tokio::sync::watch::channel(0u64);
140 if config.db_channel_enabled && host.background_tasks {
141 spawn_schema_reloader(state.clone(), config.db_channel.clone(), reloaded);
142 }
143 #[cfg(not(feature = "graphql"))]
144 drop(schema_reloads);
145
146 // Build REST API router (under /api prefix)
147 let api_router: Router<Arc<AppState>> = Router::new()
148 .route("/", any(handle_request))
149 .route("/{*path}", any(handle_request));
150
151 // Build main router
152 let mut app: Router<Arc<AppState>> = Router::new().nest("/api", api_router);
153
154 // Two endpoints every Hasura deployment has, and that the things around a
155 // deployment reach for without being asked to: `/healthz` is what a load
156 // balancer, a Kubernetes probe and `docker-compose` healthchecks are
157 // already configured to poll, and `/v1/version` is what a client library
158 // calls to decide which features to use. Serving them costs nothing and
159 // not serving them makes a drop-in replacement fail its first health
160 // check.
161 app = app
162 .route("/healthz", axum::routing::get(|| async { "OK" }))
163 .route(
164 "/v1/version",
165 axum::routing::get(|| async {
166 Json(serde_json::json!({ "version": env!("CARGO_PKG_VERSION") }))
167 }),
168 );
169
170 // Add custom routes (health checks, webhooks, etc.)
171 app = app.nest("/_", custom::custom_router());
172 info!("Custom routes enabled at /_");
173
174 // Add admin routes and GraphQL endpoint if feature is enabled
175 #[cfg(feature = "graphql")]
176 {
177 use async_graphql_axum::GraphQLBatchRequest as GqlRequest;
178 use axum::extract::State as AxumState;
179 use axum::http::HeaderMap;
180 use postrust_graphql::handler::GraphQLState;
181 use postrust_graphql::schema::SchemaConfig;
182
183 #[cfg(feature = "admin-ui")]
184 {
185 info!("Admin UI enabled at /admin");
186 app = app.nest("/admin", crate::admin::admin_router());
187 }
188
189 // Create GraphQL state with subscriptions enabled
190 let schema_cache_snapshot = state.schema_cache.read().await.clone();
191 let schema_cache_arc = Arc::new(schema_cache_snapshot);
192 // What Hasura keeps in metadata and a schema cannot carry: the names,
193 // and what each role may do. Absent, every name is derived exactly as
194 // before and there is no permission layer at all.
195 //
196 // Two spellings because the document outgrew the first one. `_NAMES`
197 // is what it was called when names were all it held, and a deployment
198 // already setting it should not have to change anything.
199 let names_var = ["PGRST_GRAPHQL_METADATA", "PGRST_GRAPHQL_NAMES"]
200 .into_iter()
201 .find_map(|name| std::env::var(name).ok().map(|value| (name, value)));
202
203 let graphql_names = match names_var {
204 Some((var, value)) => match postrust_graphql::names::NameOverrides::parse(&value) {
205 Ok(names) => {
206 if !names.is_empty() {
207 info!("GraphQL names given for {} tables", names.len());
208 }
209 if names.placed_functions() > 0 {
210 info!(
211 "GraphQL roots given for {} functions",
212 names.placed_functions()
213 );
214 }
215 if names.has_permissions() {
216 info!(
217 "GraphQL permissions given for {} roles, {} grants",
218 names.roles().len(),
219 names.granted()
220 );
221 }
222 names
223 }
224 Err(e) => {
225 // Serving the derived names instead would answer every
226 // request under a name the client does not send, which
227 // reads as a broken server rather than a bad setting. A
228 // permission read wrong is worse still: it would serve
229 // rows a rule was written to withhold.
230 tracing::error!("{}: {}", var, e);
231 return Err(anyhow::anyhow!("{}: {}", var, e));
232 }
233 },
234 None => postrust_graphql::names::NameOverrides::default(),
235 };
236
237 // An enum table's members are rows, not schema, so they are read here
238 // rather than reflected, and again each time the schema is rebuilt:
239 // a migration that adds a value notifies like one that adds a column.
240 let graphql_enum_values = read_enum_values(
241 &state.pool,
242 &*state.schema_cache.read().await,
243 &graphql_names,
244 )
245 .await;
246
247 // How often a live query re-reads itself with nothing having
248 // notified it. The notifications do the work; this is the floor
249 // under what a trigger cannot see -- a view, an embedded row on a
250 // table with no trigger, a predicate written against the clock.
251 let subscription_refresh_seconds = std::env::var("PGRST_SUBSCRIPTION_REFRESH")
252 .ok()
253 .and_then(|value| value.trim().parse::<u64>().ok())
254 .unwrap_or(30);
255
256 let mut graphql_config = SchemaConfig::default();
257 // A subscription is a WebSocket, so a host that cannot hold one
258 // does not advertise a `Subscription` root it cannot serve.
259 graphql_config.enable_subscriptions = host.websockets;
260 graphql_config.subscription_refresh_seconds = subscription_refresh_seconds;
261 graphql_config.max_rows = config.db_max_rows;
262 graphql_config.enable_federation = config.graphql_federation;
263 graphql_config.type_prefix = config.graphql_type_prefix.clone();
264 // The GraphQL schema was built for `public` whatever the server
265 // was told to expose, so a table in any other schema of
266 // `PGRST_DB_SCHEMAS` was reachable over REST and invisible over
267 // GraphQL.
268 graphql_config.exposed_schemas = config.db_schemas.clone();
269 graphql_config.enum_values = graphql_enum_values;
270 graphql_config.names = graphql_names;
271 // A schema that cannot be built is a reason to serve without GraphQL,
272 // not a reason to exit. The REST surface, the admin UI and the health
273 // endpoint do not depend on it, and taking the process down with it
274 // turns one unrepresentable table into a server that will not start.
275 // The endpoint is mounted all the same, answering that there is no
276 // schema, so the reload after a migration that fixes the table brings
277 // GraphQL up without a restart.
278 let graphql_state = match GraphQLState::new(
279 state.pool.clone(),
280 schema_cache_arc.clone(),
281 graphql_config.clone(),
282 ) {
283 Ok(graphql_state) => {
284 // Initialize subscription broker
285 if !host.websockets {
286 info!("GraphQL subscriptions disabled: this host cannot hold a WebSocket");
287 } else if let Err(e) = graphql_state.init_subscriptions().await {
288 tracing::warn!("Failed to initialize subscription broker: {}. Subscriptions may not work until triggers are created.", e);
289 } else {
290 info!("GraphQL subscriptions enabled");
291 }
292 info!("GraphQL endpoint enabled at /api/graphql");
293 Some(Arc::new(graphql_state))
294 }
295 Err(e) => {
296 tracing::error!(
297 "GraphQL schema could not be built, serving without it: {}",
298 e
299 );
300 None
301 }
302 };
303 {
304 // The schema in use, replaced whole when the database's changes.
305 // A request takes the current one once and keeps it, so an
306 // operation is never answered half from one schema and half from
307 // the next.
308 let current = Arc::new(std::sync::RwLock::new(graphql_state));
309 spawn_graphql_rebuilder(
310 state.clone(),
311 current.clone(),
312 graphql_config,
313 schema_reloads,
314 );
315
316 // Combined state for GraphQL routes (includes JWT config for auth)
317 #[derive(Clone)]
318 struct GraphQLAppState {
319 gql_state: Arc<std::sync::RwLock<Option<Arc<GraphQLState>>>>,
320 /// REST's state, for the JWT configuration and the cache of
321 /// verified tokens: one token is verified once, whichever
322 /// surface it is presented to.
323 rest: Arc<AppState>,
324 hasura_auth: postrust_auth::HasuraAuthConfig,
325 }
326
327 impl GraphQLAppState {
328 /// The GraphQL state to answer this operation from, if there
329 /// is one yet.
330 fn gql(&self) -> Option<Arc<GraphQLState>> {
331 self.gql_state
332 .read()
333 .unwrap_or_else(|e| e.into_inner())
334 .clone()
335 }
336 }
337
338 let graphql_app_state = GraphQLAppState {
339 gql_state: current,
340 rest: state.clone(),
341 hasura_auth: postrust_auth::HasuraAuthConfig {
342 admin_secret: config.hasura_admin_secret.clone(),
343 unauthorized_role: config.hasura_unauthorized_role.clone(),
344 },
345 };
346
347 // Wrapper handler that creates context from request with proper auth
348 /// A body that is a JSON *array* is a batch: several operations
349 /// in one request, answered with an array of responses in the
350 /// order they were sent. Each is its own operation with its own
351 /// transaction -- batching is a way to save round trips, not a
352 /// way to make several mutations atomic, which is what naming
353 /// several root fields in one mutation is for.
354 async fn handle_graphql(
355 AxumState(app_state): AxumState<GraphQLAppState>,
356 // Which of the three addresses this arrived at. The routes are
357 // nested, so the request's own path is `/` by the time it gets
358 // here and the original is the only thing that still says.
359 axum::extract::OriginalUri(uri): axum::extract::OriginalUri,
360 headers: HeaderMap,
361 req: GqlRequest,
362 ) -> (axum::http::StatusCode, Json<serde_json::Value>) {
363 // The status a refused write is answered with belongs to the
364 // endpoint rather than to the refusal: `/v1alpha1/graphql`
365 // reports it in the transport and `/v1/graphql` does not.
366 let endpoint = match uri.path().starts_with("/v1alpha1/") {
367 true => postrust_graphql::hasura::Endpoint::Legacy,
368 false => postrust_graphql::hasura::Endpoint::Current,
369 };
370 match req.0 {
371 async_graphql::BatchRequest::Single(request) => {
372 let body = one_graphql(app_state, headers, request).await;
373 let status = axum::http::StatusCode::from_u16(
374 postrust_graphql::hasura::status_for(&body, endpoint),
375 )
376 .unwrap_or(axum::http::StatusCode::OK);
377 (status, Json(body))
378 }
379 async_graphql::BatchRequest::Batch(requests) => {
380 let mut answers = Vec::with_capacity(requests.len());
381 for request in requests {
382 answers.push(
383 one_graphql(app_state.clone(), headers.clone(), request).await,
384 );
385 }
386 // A batch is answered 200 whatever its parts say: the
387 // transport carried every one of them, and which of
388 // them failed is in the array.
389 (
390 axum::http::StatusCode::OK,
391 Json(serde_json::Value::Array(answers)),
392 )
393 }
394 }
395 }
396
397 /// Who is asking, settled once: per request over HTTP, and per
398 /// connection over a WebSocket, where the token arrives in the
399 /// `connection_init` payload rather than on every operation.
400 #[derive(Clone)]
401 struct GraphQLIdentity {
402 auth_result: postrust_auth::AuthResult,
403 hasura_role: Option<String>,
404 session: std::collections::HashMap<String, String>,
405 elevated: bool,
406 headers: HeaderMap,
407 }
408
409 /// An operation ready to run against the schema its role answers
410 /// from, and the transaction its writes will share.
411 struct PreparedOperation {
412 schema: async_graphql::dynamic::Schema,
413 request: async_graphql::Request,
414 write: postrust_graphql::context::SharedWrite,
415 }
416
417 /// The caller's identity, or the Hasura-shaped body refusing it.
418 fn graphql_identity(
419 app_state: &GraphQLAppState,
420 headers: HeaderMap,
421 ) -> Result<GraphQLIdentity, serde_json::Value> {
422 // Extract auth header and authenticate
423 let auth_header = headers.get("authorization").and_then(|v| v.to_str().ok());
424
425 // Through the token cache where one is configured, as REST
426 // authenticates.
427 let jwt_config = &app_state.rest.jwt_config;
428 let token = match &app_state.rest.jwt_cache {
429 Some(cache) => cache.authenticate(auth_header, jwt_config),
430 None => postrust_auth::authenticate(auth_header, jwt_config),
431 };
432 // A token that was offered and verified. Not the same as
433 // `token.is_ok()`, which is also true of the anonymous
434 // fallback -- and an anonymous request is precisely the one
435 // that has not been authenticated.
436 let token_verified = auth_header.is_some() && token.is_ok();
437 // A token that was offered and is refused: REST answers each of
438 // these 401. Without a secret there is nothing to verify
439 // against, and the header is ignored as Hasura ignores it when
440 // JWT mode is off -- a router forwarding `Authorization` to
441 // every subgraph relies on that. With no header at all, and no
442 // anonymous role, the request goes on as it always has.
443 let token_refusal = match (&token, auth_header) {
444 (Ok(_) | Err(postrust_auth::JwtError::SecretMissing), _) | (Err(_), None) => {
445 None
446 }
447 // Verified, and naming no role, with no anonymous role to
448 // fall back to. Served as anyone, it would be served as a
449 // role nobody chose.
450 (Err(postrust_auth::JwtError::NoIdentity), Some(_)) => Some(
451 postrust_graphql::hasura::missing_role_claim(&jwt_config.role_claim_key),
452 ),
453 (Err(e), Some(_)) => {
454 Some(postrust_graphql::hasura::invalid_jwt(&e.to_string()))
455 }
456 };
457
458 let auth_result = match token {
459 Ok(auth) => auth,
460 Err(e) => {
461 tracing::debug!("GraphQL auth failed: {}, using anon role", e);
462 postrust_auth::AuthResult {
463 role: jwt_config
464 .anon_role
465 .clone()
466 .unwrap_or_else(|| "anon".to_string()),
467 claims: std::collections::HashMap::new(),
468 }
469 }
470 };
471
472 tracing::debug!(
473 "GraphQL request authenticated as role: {}",
474 auth_result.role
475 );
476
477 // Who the caller is in Hasura's sense, which is a different
478 // question from which database user the transaction runs as.
479 // The secret goes first: a caller that tried to be an
480 // administrator and got the secret wrong is refused rather
481 // than asked for a token.
482 let pairs: Vec<(&str, &str)> = headers
483 .iter()
484 .filter_map(|(name, value)| {
485 value.to_str().ok().map(|value| (name.as_str(), value))
486 })
487 .collect();
488
489 let (hasura_role, session, elevated) = match (
490 postrust_auth::hasura::from_admin_secret(&app_state.hasura_auth, &pairs[..]),
491 token_refusal,
492 ) {
493 (postrust_auth::SecretOutcome::Accepted(identity), _) => {
494 (Some(identity.role), identity.session, identity.elevated)
495 }
496 (postrust_auth::SecretOutcome::Rejected, _) => {
497 return Err(postrust_graphql::hasura::denied(
498 postrust_auth::hasura::SECRET_INVALID,
499 ));
500 }
501 // The secret settled nothing, so the token is refused
502 // -- and an administrator's secret beside it wins, as
503 // it does in Hasura.
504 (_, Some(refusal)) => return Err(refusal),
505 // Nothing settled by the secret. A verified token speaks
506 // next, and session variables come from its claims.
507 //
508 // One header is read here and only here: `X-Hasura-Role`,
509 // against the `x-hasura-allowed-roles` the token carries.
510 // That list is inside the signature, so it lets a caller
511 // choose among the identities it was issued and not add
512 // one. Every other `x-hasura-*` header is still ignored --
513 // on a server with no secret to gate them they would let
514 // any caller name its own identity.
515 _ if token_verified => {
516 match postrust_auth::hasura::role_for_token(&auth_result.claims, &pairs[..])
517 {
518 postrust_auth::hasura::TokenRole::Is(role) => (
519 role,
520 postrust_auth::hasura::session_from_claims(&auth_result.claims),
521 false,
522 ),
523 postrust_auth::hasura::TokenRole::NotAllowed => {
524 return Err(postrust_graphql::hasura::denied(
525 postrust_auth::hasura::ROLE_NOT_ALLOWED,
526 ));
527 }
528 }
529 }
530 // A secret is configured and nothing authenticated this
531 // request. Answering it as a stranger is a choice the
532 // deployment makes; refusing is the default.
533 _ if app_state.hasura_auth.is_configured() => {
534 match postrust_auth::hasura::unauthenticated(&app_state.hasura_auth) {
535 Some(identity) => (Some(identity.role), identity.session, false),
536 None => {
537 return Err(postrust_graphql::hasura::denied(
538 postrust_auth::hasura::SECRET_MISSING,
539 ));
540 }
541 }
542 }
543 // No Hasura auth configured at all: unchanged from before
544 // any of this existed.
545 _ => (
546 postrust_auth::hasura::role_from_claims(&auth_result.claims),
547 postrust_auth::hasura::session_from_claims(&auth_result.claims),
548 false,
549 ),
550 };
551
552 Ok(GraphQLIdentity {
553 auth_result,
554 hasura_role,
555 session,
556 elevated,
557 headers,
558 })
559 }
560
561 /// Everything between knowing who is asking and running what they
562 /// asked: the checks Hasura makes before reading the document, the
563 /// schema the role answers from, and the context every resolver
564 /// reads the identity from.
565 fn prepare_operation(
566 app_state: &GraphQLAppState,
567 identity: &GraphQLIdentity,
568 req: async_graphql::Request,
569 ) -> Result<PreparedOperation, serde_json::Value> {
570 let GraphQLIdentity {
571 auth_result,
572 hasura_role,
573 session,
574 elevated,
575 headers,
576 } = identity.clone();
577 let Some(gql) = app_state.gql() else {
578 return Err(postrust_graphql::hasura::unavailable(
579 "the GraphQL schema could not be built; see the server log",
580 ));
581 };
582 let pairs: Vec<(&str, &str)> = headers
583 .iter()
584 .filter_map(|(name, value)| {
585 value.to_str().ok().map(|value| (name.as_str(), value))
586 })
587 .collect();
588
589 // A protocol neither server implements, which only one of
590 // them says. See `hasura::persisted_query`.
591 if postrust_graphql::hasura::persisted_query(&req) {
592 return Err(postrust_graphql::hasura::not_supported(
593 "PersistedQueryNotSupported",
594 ));
595 }
596
597 // A header Hasura reads itself, before it reads the
598 // document. `x-hasura-use-backend-only-permissions: random` is
599 // not false, and treating it as false would send a
600 // backend-only write down the path meant for everyone else on
601 // a header the client thought it had set.
602 if let Some(message) = postrust_auth::hasura::unreadable_boolean_header(&pairs[..])
603 {
604 return Err(postrust_graphql::hasura::malformed(&message));
605 }
606
607 // Which schema answers. A permission is a statement about what
608 // exists, so the role decides the shape of the API before it
609 // decides anything about a row: a role the document does not
610 // name has no schema at all, and is refused here rather than
611 // being answered from someone else's.
612 let Some(schema) = gql.schema_for(
613 hasura_role.as_deref(),
614 postrust_auth::hasura::backend_only_requested(&pairs[..], elevated),
615 ) else {
616 return Err(postrust_graphql::hasura::denied(&format!(
617 "role \"{}\" is not defined in the permissions",
618 hasura_role.as_deref().unwrap_or_default()
619 )));
620 };
621
622 // Create SchemaCacheRef from the static Arc<SchemaCache>
623 //
624 // The unreduced cache, deliberately. The schema above decides
625 // what may be asked for; this is what a resolver looks a type
626 // up in once something has been asked, and a superset can only
627 // answer the same questions.
628 let schema_cache_ref = postrust_core::schema_cache::SchemaCacheRef::from_static(
629 (*gql.schema_cache).clone(),
630 );
631
632 // Every write in this operation goes into one transaction, and
633 // this is the half that decides its fate: a mutation naming
634 // several root fields is all-or-nothing, so the second one
635 // failing has to take the first one's rows with it. Nothing to
636 // settle for a query, which never opens it.
637 let write: postrust_graphql::context::SharedWrite = Arc::default();
638
639 let gql_ctx = postrust_graphql::context::GraphQLContext::new(
640 gql.pool.clone(),
641 schema_cache_ref,
642 auth_result,
643 )
644 .with_session(session)
645 .with_identity(hasura_role.clone(), elevated)
646 .with_write(Arc::clone(&write));
647
648 // A role may be told it cannot read the schema as data. Not a
649 // permission on a table, so it is not settled by which schema
650 // answers -- the schema is the thing being withheld.
651 //
652 // Not withheld from a caller holding the admin secret, whatever
653 // role it then names. That is measured rather than assumed: a
654 // v2.50.1 reference given `set_graphql_schema_introspection_
655 // options` answers `__schema` for the named role when the
656 // secret is sent beside the role header, and answers it from
657 // that role's own restricted schema. Reading the schema is an
658 // administrator's to do; the permissions still apply to it.
659 let introspection_disabled_for = hasura_role
660 .as_deref()
661 .filter(|_| !elevated)
662 .filter(|role| gql.config.names.introspection_disabled(role));
663 let prepared = postrust_graphql::hasura::prepare(
664 Some(schema),
665 req,
666 introspection_disabled_for,
667 );
668 let request = match prepared {
669 Ok(request) => request,
670 // Refused before it ran: nothing was written, so there is
671 // nothing to settle.
672 Err((_, errors)) => {
673 let mut response = async_graphql::Response::new(async_graphql::Value::Null);
674 response.errors = errors;
675 return Err(postrust_graphql::hasura::envelope(response));
676 }
677 };
678 let request = request
679 .data(gql_ctx)
680 .data(gql.pool.clone())
681 .data(Arc::clone(&gql.broker));
682 Ok(PreparedOperation {
683 schema: schema.clone(),
684 request,
685 write,
686 })
687 }
688
689 /// Settle an operation's writes before its answer goes out.
690 async fn settle_write(
691 write: &postrust_graphql::context::SharedWrite,
692 response: &mut async_graphql::Response,
693 ) {
694 if let Some(tx) = write.lock().await.take() {
695 let settled = if response.errors.is_empty() {
696 tx.commit().await
697 } else {
698 tx.rollback().await
699 };
700 // A commit that fails is a mutation that did not happen,
701 // however well every statement in it went. Saying so is
702 // the whole point of running them together.
703 if let Err(e) = settled {
704 tracing::error!("GraphQL mutation could not be settled: {}", e);
705 response
706 .errors
707 .push(async_graphql::ServerError::new(e.to_string(), None));
708 response.data = async_graphql::Value::Null;
709 }
710 }
711 }
712
713 async fn one_graphql(
714 app_state: GraphQLAppState,
715 headers: HeaderMap,
716 req: async_graphql::Request,
717 ) -> serde_json::Value {
718 let identity = match graphql_identity(&app_state, headers) {
719 Ok(identity) => identity,
720 Err(refusal) => return refusal,
721 };
722 let PreparedOperation {
723 schema,
724 request,
725 write,
726 } = match prepare_operation(&app_state, &identity, req) {
727 Ok(operation) => operation,
728 Err(refusal) => return refusal,
729 };
730 let mut response = schema.execute(request).await;
731 settle_write(&write, &mut response).await;
732 postrust_graphql::hasura::envelope(response)
733 }
734
735 /// A Hasura-shaped body as the response a WebSocket carries, so a
736 /// subscription is refused, and its errors read, exactly as the
737 /// same operation over HTTP would be.
738 fn hasura_response(body: serde_json::Value) -> async_graphql::Response {
739 let data = body
740 .get("data")
741 .cloned()
742 .and_then(|data| async_graphql::Value::from_json(data).ok())
743 .unwrap_or(async_graphql::Value::Null);
744 let mut response = async_graphql::Response::new(data);
745 for error in body
746 .get("errors")
747 .and_then(serde_json::Value::as_array)
748 .into_iter()
749 .flatten()
750 {
751 let message = error
752 .get("message")
753 .and_then(serde_json::Value::as_str)
754 .unwrap_or_default();
755 let mut server_error = async_graphql::ServerError::new(message, None);
756 if let Some(fields) = error
757 .get("extensions")
758 .and_then(serde_json::Value::as_object)
759 {
760 let mut extensions = async_graphql::ErrorExtensionValues::default();
761 for (name, value) in fields {
762 if let Ok(value) = async_graphql::Value::from_json(value.clone()) {
763 extensions.set(name, value);
764 }
765 }
766 server_error.extensions = Some(extensions);
767 }
768 response.errors.push(server_error);
769 }
770 response
771 }
772
773 /// The headers a WebSocket connection is authenticated with: the
774 /// upgrade request's, overlaid with those the `connection_init`
775 /// payload carries. A browser cannot set headers on a WebSocket,
776 /// so a client puts `Authorization` in the payload -- Hasura's own
777 /// clients under `headers`, others at the top level.
778 fn connection_headers(
779 mut headers: HeaderMap,
780 payload: &serde_json::Value,
781 ) -> HeaderMap {
782 let nested = payload
783 .get("headers")
784 .and_then(serde_json::Value::as_object);
785 for fields in [payload.as_object(), nested].into_iter().flatten() {
786 for (name, value) in fields {
787 let Some(value) = value.as_str() else {
788 continue;
789 };
790 if let (Ok(name), Ok(value)) = (
791 axum::http::HeaderName::from_bytes(name.as_bytes()),
792 axum::http::HeaderValue::from_str(value),
793 ) {
794 headers.insert(name, value);
795 }
796 }
797 }
798 headers
799 }
800
801 /// Runs every WebSocket operation -- query, mutation or
802 /// subscription -- as the identity its connection was initialised
803 /// with, through the same preparation an HTTP request gets.
804 #[derive(Clone)]
805 struct WebSocketExecutor(GraphQLAppState);
806
807 impl async_graphql::Executor for WebSocketExecutor {
808 async fn execute(
809 &self,
810 request: async_graphql::Request,
811 ) -> async_graphql::Response {
812 use async_graphql::futures_util::StreamExt;
813 // Not reached over a WebSocket, which runs everything as a
814 // stream; answered from the stream so the two cannot differ.
815 self.execute_stream(request, None)
816 .next()
817 .await
818 .unwrap_or_else(|| async_graphql::Response::new(async_graphql::Value::Null))
819 }
820
821 fn execute_stream(
822 &self,
823 request: async_graphql::Request,
824 session_data: Option<Arc<async_graphql::Data>>,
825 ) -> async_graphql::futures_util::stream::BoxStream<'static, async_graphql::Response>
826 {
827 use async_graphql::futures_util::{stream, StreamExt};
828 let identity = session_data
829 .as_deref()
830 .and_then(|data| data.get(&std::any::TypeId::of::<GraphQLIdentity>()))
831 .and_then(|identity| identity.downcast_ref::<GraphQLIdentity>())
832 .cloned();
833 // `connection_init` refuses a connection it cannot
834 // identify, so an operation without an identity is one the
835 // protocol let through before initialising. Refused rather
836 // than run as nobody in particular.
837 let Some(identity) = identity else {
838 let refusal =
839 postrust_graphql::hasura::denied("the connection was not initialised");
840 return stream::once(async move { hasura_response(refusal) }).boxed();
841 };
842 match prepare_operation(&self.0, &identity, request) {
843 Err(refusal) => {
844 stream::once(async move { hasura_response(refusal) }).boxed()
845 }
846 Ok(PreparedOperation {
847 schema,
848 request,
849 write,
850 }) => {
851 async_graphql::Executor::execute_stream(&schema, request, session_data)
852 .then(move |mut response| {
853 let write = Arc::clone(&write);
854 async move {
855 settle_write(&write, &mut response).await;
856 hasura_response(postrust_graphql::hasura::envelope(
857 response,
858 ))
859 }
860 })
861 .boxed()
862 }
863 }
864 }
865 }
866
867 /// GraphQL over a WebSocket, authenticated as HTTP is. The
868 /// identity is settled once, from `connection_init`, and a
869 /// connection that would have been refused over HTTP -- a wrong
870 /// admin secret, a role the token does not allow -- is refused
871 /// there rather than served as the anonymous role.
872 async fn graphql_ws(
873 AxumState(app_state): AxumState<GraphQLAppState>,
874 protocol: async_graphql_axum::GraphQLProtocol,
875 headers: HeaderMap,
876 ws: axum::extract::ws::WebSocketUpgrade,
877 ) -> axum::response::Response {
878 ws.protocols(async_graphql::http::ALL_WEBSOCKET_PROTOCOLS)
879 .on_upgrade(move |socket| async move {
880 let init_state = app_state.clone();
881 async_graphql_axum::GraphQLWebSocket::new(
882 socket,
883 WebSocketExecutor(app_state),
884 protocol,
885 )
886 .on_connection_init(move |payload| async move {
887 match graphql_identity(
888 &init_state,
889 connection_headers(headers, &payload),
890 ) {
891 Ok(identity) => {
892 let mut data = async_graphql::Data::default();
893 data.insert(identity);
894 Ok(data)
895 }
896 Err(refusal) => Err(async_graphql::Error::new(
897 refusal
898 .pointer("/errors/0/message")
899 .and_then(serde_json::Value::as_str)
900 .unwrap_or("connection refused"),
901 )),
902 }
903 })
904 .serve()
905 .await
906 })
907 }
908
909 // Add GraphQL routes with WebSocket support for subscriptions
910 let graphql_router = Router::new()
911 .route("/", post(handle_graphql))
912 .route("/", get(postrust_graphql::handler::graphql_playground))
913 .with_state(graphql_app_state.clone());
914
915 // The same state as HTTP, because a WebSocket is authenticated the
916 // same way: `postrust_graphql::handler::graphql_ws_handler` carries
917 // no caller identity, so every subscription it served ran without
918 // a role, a session or claims.
919 let ws_router = Router::new()
920 .route("/ws", get(graphql_ws))
921 .with_state(graphql_app_state);
922
923 let graphql_app = match host.websockets {
924 true => graphql_router.merge(ws_router),
925 false => graphql_router,
926 };
927 // `/v1/graphql` is where a Hasura client sends its queries, and it is
928 // the only address most of them can be told about: the endpoint is
929 // baked into generated clients and codegen configs. `/api/graphql`
930 // keeps working for anything already pointed at it, and
931 // `/v1alpha1/graphql` is the address Hasura served before `/v1`
932 // and still answers on -- a client old enough to have been
933 // pointed there is exactly the one that cannot be repointed.
934 app = app
935 .nest("/v1/graphql", graphql_app.clone())
936 .nest("/v1alpha1/graphql", graphql_app.clone())
937 .nest("/api/graphql", graphql_app);
938 }
939 }
940
941 // PostgREST compatibility mode: also serve the REST surface at the root so
942 // canonical PostgREST paths (`/rpc/<name>`, `/<table>`) work in addition to
943 // the `/api`-prefixed paths. Explicit routes (`/`, `/_`, `/admin`, `/api`)
944 // still take precedence; only otherwise-unmatched paths hit this fallback.
945 if config.compat_mode {
946 app = app.fallback(handle_request);
947 info!("PostgREST compatibility mode enabled: REST surface also served at /");
948
949 // Key ordering is fixed when the binary is compiled, so asking for
950 // compatibility at runtime cannot turn it on. Say so, rather than
951 // leaving someone to discover the difference by diffing responses.
952 if !cfg!(feature = "compat-key-order") {
953 tracing::warn!(
954 "compatibility mode: object keys will be returned in alphabetical order, \
955 not in select order as PostgREST returns them. Build with \
956 --features compat-key-order to match, at a cost of up to 15% throughput \
957 on wide rows."
958 );
959 }
960 }
961
962 // Add root info endpoint.
963 //
964 // Not in compatibility mode: there `/` is part of the API surface -- it is
965 // where PostgREST serves the schema description, and where it reports an
966 // `Accept-Profile` naming a schema that is not exposed. A directory of
967 // this server's own endpoints in its place answers a different question
968 // from the one asked.
969 if !config.compat_mode {
970 app = app.route(
971 "/",
972 axum::routing::get(|| async {
973 Json(serde_json::json!({
974 "name": "postrust",
975 "version": env!("CARGO_PKG_VERSION"),
976 "api": "/api",
977 "custom": "/_",
978 "health": "/_/health",
979 "admin": "/admin",
980 "docs": "/admin/swagger"
981 }))
982 }),
983 );
984 }
985
986 // An empty list is "any origin", which is the default and what
987 // `PGRST_SERVER_CORS_ORIGINS="*"` asks for. Anything that is not a valid
988 // header value is dropped with a warning rather than quietly widening the
989 // policy back to `*` -- a typo in an origin should cost that origin, not
990 // the restriction.
991 let allow_origin = if config.server_cors_origins.is_empty() {
992 AllowOrigin::any()
993 } else {
994 let origins: Vec<HeaderValue> = config
995 .server_cors_origins
996 .iter()
997 .filter_map(|o| match HeaderValue::from_str(o) {
998 Ok(v) => Some(v),
999 Err(_) => {
1000 tracing::warn!("Ignoring CORS origin {:?}: not a valid header value", o);
1001 None
1002 }
1003 })
1004 .collect();
1005 info!("CORS restricted to {} origin(s)", origins.len());
1006 AllowOrigin::list(origins)
1007 };
1008
1009 // Apply CORS and state
1010 let app = app
1011 .layer(
1012 CorsLayer::new()
1013 .allow_origin(allow_origin)
1014 .allow_methods([
1015 Method::GET,
1016 Method::POST,
1017 Method::PUT,
1018 Method::PATCH,
1019 Method::DELETE,
1020 Method::OPTIONS,
1021 Method::HEAD,
1022 ])
1023 .allow_headers(CorsAny)
1024 .expose_headers(CorsAny),
1025 )
1026 // For the routes whose handlers take a body *extractor* -- GraphQL,
1027 // the admin surface -- which is what this layer reaches. The REST
1028 // handler takes the whole `Request` and reads the body itself, so it
1029 // applies `max_body_size` directly; see `postrust_engine::handle`.
1030 .layer(DefaultBodyLimit::max(config.max_body_size))
1031 // Outermost, so it runs before the CORS layer -- which answers every
1032 // OPTIONS itself and never calls what it wraps, so nothing downstream
1033 // of it can say what a resource allows.
1034 .layer(axum::middleware::from_fn_with_state(
1035 state.clone(),
1036 app::options_allow,
1037 ))
1038 .with_state(state.clone());
1039
1040 Ok(App { router: app, state })
1041}
1042
1043/// Reload the REST schema cache on `NOTIFY <channel>`.
1044///
1045/// Reconnects on its own: a listening connection is dropped by a restart of
1046/// the database or anything between, and a reloader that gives up on the first
1047/// error is one that stops working silently partway through a deployment.
1048fn spawn_schema_reloader(
1049 state: Arc<AppState>,
1050 channel: String,
1051 reloaded: tokio::sync::watch::Sender<u64>,
1052) {
1053 const RETRY: std::time::Duration = std::time::Duration::from_secs(5);
1054
1055 tokio::spawn(async move {
1056 loop {
1057 let mut listener = match sqlx::postgres::PgListener::connect_with(&state.pool).await {
1058 Ok(listener) => listener,
1059 Err(e) => {
1060 error!("Schema reloader could not connect: {}; retrying", e);
1061 tokio::time::sleep(RETRY).await;
1062 continue;
1063 }
1064 };
1065
1066 if let Err(e) = listener.listen(&channel).await {
1067 error!("Schema reloader could not LISTEN on {}: {}", channel, e);
1068 tokio::time::sleep(RETRY).await;
1069 continue;
1070 }
1071 info!("Listening on channel {} for schema reloads", channel);
1072
1073 // Inner loop until the connection fails, then reconnect.
1074 loop {
1075 match listener.recv().await {
1076 Ok(notification) => {
1077 info!("Schema reload requested: {:?}", notification.payload());
1078 match state.reload_schema().await {
1079 Ok(()) => {
1080 info!(
1081 "Schema cache reloaded: {}",
1082 state.schema_cache().await.summary()
1083 );
1084 reloaded.send_modify(|n| *n += 1);
1085 }
1086 // The old cache is kept. Serving the previous
1087 // schema is better than serving none, and the next
1088 // notification tries again.
1089 Err(e) => error!("Schema cache reload failed: {}", e),
1090 }
1091 }
1092 Err(e) => {
1093 error!("Schema reloader lost its connection: {}; reconnecting", e);
1094 break;
1095 }
1096 }
1097 }
1098 }
1099 });
1100}
1101
1102/// Rebuild the GraphQL schema each time the REST schema cache is reloaded.
1103///
1104/// The new state is built beside the old one and swapped in only once it is
1105/// whole: a schema that cannot be built leaves the previous one serving, as a
1106/// failed reload of the REST cache does. Enum tables are read again, since a
1107/// migration may add a value; the permission document is kept from start-up,
1108/// since it is not part of what a migration notifies about. The subscription
1109/// broker carries over to the new state, so a subscription opened on the old
1110/// schema keeps receiving notifications, and listens on any table the new
1111/// one adds.
1112#[cfg(feature = "graphql")]
1113fn spawn_graphql_rebuilder(
1114 state: Arc<AppState>,
1115 current: Arc<std::sync::RwLock<Option<Arc<postrust_graphql::handler::GraphQLState>>>>,
1116 mut config: postrust_graphql::schema::SchemaConfig,
1117 mut reloads: tokio::sync::watch::Receiver<u64>,
1118) {
1119 use postrust_graphql::handler::GraphQLState;
1120
1121 tokio::spawn(async move {
1122 while reloads.changed().await.is_ok() {
1123 let cache = Arc::new(state.schema_cache().await.clone());
1124 config.enum_values = read_enum_values(&state.pool, &cache, &config.names).await;
1125 let mut next = match GraphQLState::new(state.pool.clone(), cache, config.clone()) {
1126 Ok(next) => next,
1127 Err(e) => {
1128 error!(
1129 "GraphQL schema rebuild failed, keeping the previous one: {}",
1130 e
1131 );
1132 continue;
1133 }
1134 };
1135 let previous = current.read().unwrap_or_else(|e| e.into_inner()).clone();
1136 if let Err(e) = next.continue_subscriptions(previous.as_deref()).await {
1137 // The tables already listened to still are; a new one is
1138 // answered by the refresh until the next reload retries it.
1139 tracing::warn!("Subscriptions on the rebuilt schema: {}", e);
1140 }
1141 *current.write().unwrap_or_else(|e| e.into_inner()) = Some(Arc::new(next));
1142 info!("GraphQL schema rebuilt");
1143 }
1144 });
1145}
1146
1147/// The members of every table the GraphQL metadata marks as an enumeration,
1148/// keyed "schema.table". A table that cannot be read is left out, with a
1149/// warning, and becomes an ordinary table type.
1150#[cfg(feature = "graphql")]
1151async fn read_enum_values(
1152 pool: &sqlx::PgPool,
1153 cache: &postrust_core::schema_cache::SchemaCache,
1154 names: &postrust_graphql::names::NameOverrides,
1155) -> std::collections::HashMap<String, Vec<(String, Option<String>)>> {
1156 let mut values_of = std::collections::HashMap::new();
1157 for (schema, table) in names.enum_tables() {
1158 let qi = postrust_core::api_request::QualifiedIdentifier::new(&schema, &table);
1159 let Some(definition) = cache.get_table(&qi) else {
1160 tracing::warn!(
1161 "{}.{} is marked as an enumeration but was not found",
1162 schema,
1163 table
1164 );
1165 continue;
1166 };
1167 // One column identifies a member. A composite key names no
1168 // single value, so there is nothing to call the member.
1169 let [key_column] = definition.pk_cols.as_slice() else {
1170 tracing::warn!(
1171 "{}.{} is marked as an enumeration but its primary key is not one column",
1172 schema,
1173 table
1174 );
1175 continue;
1176 };
1177 // Hasura's convention, and a useful one: a `comment` column
1178 // describes each value. Read as a role that may read the
1179 // table, which a NOINHERIT authenticator is not by itself.
1180 match postrust_core::schema_cache::enum_values(
1181 pool,
1182 &schema,
1183 &table,
1184 key_column,
1185 definition.get_column("comment").is_some(),
1186 )
1187 .await
1188 {
1189 Ok(values) => {
1190 values_of.insert(format!("{}.{}", schema, table), values);
1191 }
1192 Err(e) => {
1193 tracing::warn!("cannot read the values of {}.{}: {}", schema, table, e)
1194 }
1195 }
1196 }
1197 values_of
1198}
1199
1200/// Mask database URI for logging.
1201fn mask_db_uri(uri: &str) -> String {
1202 if let Some(at_pos) = uri.find('@') {
1203 if let Some(proto_end) = uri.find("://") {
1204 return format!("{}://***@{}", &uri[..proto_end], &uri[at_pos + 1..]);
1205 }
1206 }
1207 uri.to_string()
1208}