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