Skip to main content

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                jwt_config: postrust_auth::JwtConfig,
339                hasura_auth: postrust_auth::HasuraAuthConfig,
340            }
341
342            let graphql_app_state = GraphQLAppState {
343                gql_state: graphql_state.clone(),
344                jwt_config: state.jwt_config.clone(),
345                hasura_auth: postrust_auth::HasuraAuthConfig {
346                    admin_secret: config.hasura_admin_secret.clone(),
347                    unauthorized_role: config.hasura_unauthorized_role.clone(),
348                },
349            };
350
351            // Wrapper handler that creates context from request with proper auth
352            /// A body that is a JSON *array* is a batch: several operations
353            /// in one request, answered with an array of responses in the
354            /// order they were sent. Each is its own operation with its own
355            /// transaction -- batching is a way to save round trips, not a
356            /// way to make several mutations atomic, which is what naming
357            /// several root fields in one mutation is for.
358            async fn handle_graphql(
359                AxumState(app_state): AxumState<GraphQLAppState>,
360                // Which of the three addresses this arrived at. The routes are
361                // nested, so the request's own path is `/` by the time it gets
362                // here and the original is the only thing that still says.
363                axum::extract::OriginalUri(uri): axum::extract::OriginalUri,
364                headers: HeaderMap,
365                req: GqlRequest,
366            ) -> (axum::http::StatusCode, Json<serde_json::Value>) {
367                // The status a refused write is answered with belongs to the
368                // endpoint rather than to the refusal: `/v1alpha1/graphql`
369                // reports it in the transport and `/v1/graphql` does not.
370                let endpoint = match uri.path().starts_with("/v1alpha1/") {
371                    true => postrust_graphql::hasura::Endpoint::Legacy,
372                    false => postrust_graphql::hasura::Endpoint::Current,
373                };
374                match req.0 {
375                    async_graphql::BatchRequest::Single(request) => {
376                        let body = one_graphql(app_state, headers, request).await;
377                        let status = axum::http::StatusCode::from_u16(
378                            postrust_graphql::hasura::status_for(&body, endpoint),
379                        )
380                        .unwrap_or(axum::http::StatusCode::OK);
381                        (status, Json(body))
382                    }
383                    async_graphql::BatchRequest::Batch(requests) => {
384                        let mut answers = Vec::with_capacity(requests.len());
385                        for request in requests {
386                            answers.push(
387                                one_graphql(app_state.clone(), headers.clone(), request).await,
388                            );
389                        }
390                        // A batch is answered 200 whatever its parts say: the
391                        // transport carried every one of them, and which of
392                        // them failed is in the array.
393                        (
394                            axum::http::StatusCode::OK,
395                            Json(serde_json::Value::Array(answers)),
396                        )
397                    }
398                }
399            }
400
401            /// Who is asking, settled once: per request over HTTP, and per
402            /// connection over a WebSocket, where the token arrives in the
403            /// `connection_init` payload rather than on every operation.
404            #[derive(Clone)]
405            struct GraphQLIdentity {
406                auth_result: postrust_auth::AuthResult,
407                hasura_role: Option<String>,
408                session: std::collections::HashMap<String, String>,
409                elevated: bool,
410                headers: HeaderMap,
411            }
412
413            /// An operation ready to run against the schema its role answers
414            /// from, and the transaction its writes will share.
415            struct PreparedOperation {
416                schema: async_graphql::dynamic::Schema,
417                request: async_graphql::Request,
418                write: postrust_graphql::context::SharedWrite,
419            }
420
421            /// The caller's identity, or the Hasura-shaped body refusing it.
422            fn graphql_identity(
423                app_state: &GraphQLAppState,
424                headers: HeaderMap,
425            ) -> Result<GraphQLIdentity, serde_json::Value> {
426                // Extract auth header and authenticate
427                let auth_header = headers.get("authorization").and_then(|v| v.to_str().ok());
428
429                let token = postrust_auth::authenticate(auth_header, &app_state.jwt_config);
430                // A token that was offered and verified. Not the same as
431                // `token.is_ok()`, which is also true of the anonymous
432                // fallback -- and an anonymous request is precisely the one
433                // that has not been authenticated.
434                let token_verified = auth_header.is_some() && token.is_ok();
435
436                let auth_result = match token {
437                    Ok(auth) => auth,
438                    Err(e) => {
439                        tracing::debug!("GraphQL auth failed: {}, using anon role", e);
440                        postrust_auth::AuthResult {
441                            role: app_state
442                                .jwt_config
443                                .anon_role
444                                .clone()
445                                .unwrap_or_else(|| "anon".to_string()),
446                            claims: std::collections::HashMap::new(),
447                        }
448                    }
449                };
450
451                tracing::debug!(
452                    "GraphQL request authenticated as role: {}",
453                    auth_result.role
454                );
455
456                // Who the caller is in Hasura's sense, which is a different
457                // question from which database user the transaction runs as.
458                // The secret goes first: a caller that tried to be an
459                // administrator and got the secret wrong is refused rather
460                // than asked for a token.
461                let pairs: Vec<(&str, &str)> = headers
462                    .iter()
463                    .filter_map(|(name, value)| {
464                        value.to_str().ok().map(|value| (name.as_str(), value))
465                    })
466                    .collect();
467
468                let (hasura_role, session, elevated) =
469                    match postrust_auth::hasura::from_admin_secret(
470                        &app_state.hasura_auth,
471                        &pairs[..],
472                    ) {
473                        postrust_auth::SecretOutcome::Accepted(identity) => {
474                            (Some(identity.role), identity.session, identity.elevated)
475                        }
476                        postrust_auth::SecretOutcome::Rejected => {
477                            return Err(postrust_graphql::hasura::denied(
478                                postrust_auth::hasura::SECRET_INVALID,
479                            ));
480                        }
481                        // Nothing settled by the secret. A verified token speaks
482                        // next, and session variables come from its claims.
483                        //
484                        // One header is read here and only here: `X-Hasura-Role`,
485                        // against the `x-hasura-allowed-roles` the token carries.
486                        // That list is inside the signature, so it lets a caller
487                        // choose among the identities it was issued and not add
488                        // one. Every other `x-hasura-*` header is still ignored --
489                        // on a server with no secret to gate them they would let
490                        // any caller name its own identity.
491                        _ if token_verified => {
492                            match postrust_auth::hasura::role_for_token(
493                                &auth_result.claims,
494                                &pairs[..],
495                            ) {
496                                postrust_auth::hasura::TokenRole::Is(role) => (
497                                    role,
498                                    postrust_auth::hasura::session_from_claims(&auth_result.claims),
499                                    false,
500                                ),
501                                postrust_auth::hasura::TokenRole::NotAllowed => {
502                                    return Err(postrust_graphql::hasura::denied(
503                                        postrust_auth::hasura::ROLE_NOT_ALLOWED,
504                                    ));
505                                }
506                            }
507                        }
508                        // A secret is configured and nothing authenticated this
509                        // request. Answering it as a stranger is a choice the
510                        // deployment makes; refusing is the default.
511                        _ if app_state.hasura_auth.is_configured() => {
512                            match postrust_auth::hasura::unauthenticated(&app_state.hasura_auth) {
513                                Some(identity) => (Some(identity.role), identity.session, false),
514                                None => {
515                                    return Err(postrust_graphql::hasura::denied(
516                                        postrust_auth::hasura::SECRET_MISSING,
517                                    ));
518                                }
519                            }
520                        }
521                        // No Hasura auth configured at all: unchanged from before
522                        // any of this existed.
523                        _ => (
524                            postrust_auth::hasura::role_from_claims(&auth_result.claims),
525                            postrust_auth::hasura::session_from_claims(&auth_result.claims),
526                            false,
527                        ),
528                    };
529
530                Ok(GraphQLIdentity {
531                    auth_result,
532                    hasura_role,
533                    session,
534                    elevated,
535                    headers,
536                })
537            }
538
539            /// Everything between knowing who is asking and running what they
540            /// asked: the checks Hasura makes before reading the document, the
541            /// schema the role answers from, and the context every resolver
542            /// reads the identity from.
543            fn prepare_operation(
544                app_state: &GraphQLAppState,
545                identity: &GraphQLIdentity,
546                req: async_graphql::Request,
547            ) -> Result<PreparedOperation, serde_json::Value> {
548                let GraphQLIdentity {
549                    auth_result,
550                    hasura_role,
551                    session,
552                    elevated,
553                    headers,
554                } = identity.clone();
555                let pairs: Vec<(&str, &str)> = headers
556                    .iter()
557                    .filter_map(|(name, value)| {
558                        value.to_str().ok().map(|value| (name.as_str(), value))
559                    })
560                    .collect();
561
562                // A protocol neither server implements, which only one of
563                // them says. See `hasura::persisted_query`.
564                if postrust_graphql::hasura::persisted_query(&req) {
565                    return Err(postrust_graphql::hasura::not_supported(
566                        "PersistedQueryNotSupported",
567                    ));
568                }
569
570                // A header Hasura reads itself, before it reads the
571                // document. `x-hasura-use-backend-only-permissions: random` is
572                // not false, and treating it as false would send a
573                // backend-only write down the path meant for everyone else on
574                // a header the client thought it had set.
575                if let Some(message) = postrust_auth::hasura::unreadable_boolean_header(&pairs[..])
576                {
577                    return Err(postrust_graphql::hasura::malformed(&message));
578                }
579
580                // Which schema answers. A permission is a statement about what
581                // exists, so the role decides the shape of the API before it
582                // decides anything about a row: a role the document does not
583                // name has no schema at all, and is refused here rather than
584                // being answered from someone else's.
585                let Some(schema) = app_state.gql_state.schema_for(
586                    hasura_role.as_deref(),
587                    postrust_auth::hasura::backend_only_requested(&pairs[..], elevated),
588                ) else {
589                    return Err(postrust_graphql::hasura::denied(&format!(
590                        "role \"{}\" is not defined in the permissions",
591                        hasura_role.as_deref().unwrap_or_default()
592                    )));
593                };
594
595                // Create SchemaCacheRef from the static Arc<SchemaCache>
596                //
597                // The unreduced cache, deliberately. The schema above decides
598                // what may be asked for; this is what a resolver looks a type
599                // up in once something has been asked, and a superset can only
600                // answer the same questions.
601                let schema_cache_ref = postrust_core::schema_cache::SchemaCacheRef::from_static(
602                    (*app_state.gql_state.schema_cache).clone(),
603                );
604
605                // Every write in this operation goes into one transaction, and
606                // this is the half that decides its fate: a mutation naming
607                // several root fields is all-or-nothing, so the second one
608                // failing has to take the first one's rows with it. Nothing to
609                // settle for a query, which never opens it.
610                let write: postrust_graphql::context::SharedWrite = Arc::default();
611
612                let gql_ctx = postrust_graphql::context::GraphQLContext::new(
613                    app_state.gql_state.pool.clone(),
614                    schema_cache_ref,
615                    auth_result,
616                )
617                .with_session(session)
618                .with_identity(hasura_role.clone(), elevated)
619                .with_write(Arc::clone(&write));
620
621                // A role may be told it cannot read the schema as data. Not a
622                // permission on a table, so it is not settled by which schema
623                // answers -- the schema is the thing being withheld.
624                //
625                // Not withheld from a caller holding the admin secret, whatever
626                // role it then names. That is measured rather than assumed: a
627                // v2.50.1 reference given `set_graphql_schema_introspection_
628                // options` answers `__schema` for the named role when the
629                // secret is sent beside the role header, and answers it from
630                // that role's own restricted schema. Reading the schema is an
631                // administrator's to do; the permissions still apply to it.
632                let introspection_disabled_for =
633                    hasura_role.as_deref().filter(|_| !elevated).filter(|role| {
634                        app_state
635                            .gql_state
636                            .config
637                            .names
638                            .introspection_disabled(role)
639                    });
640                let prepared = postrust_graphql::hasura::prepare(
641                    Some(schema),
642                    req,
643                    introspection_disabled_for,
644                );
645                let request = match prepared {
646                    Ok(request) => request,
647                    // Refused before it ran: nothing was written, so there is
648                    // nothing to settle.
649                    Err((_, errors)) => {
650                        let mut response = async_graphql::Response::new(async_graphql::Value::Null);
651                        response.errors = errors;
652                        return Err(postrust_graphql::hasura::envelope(response));
653                    }
654                };
655                let request = request
656                    .data(gql_ctx)
657                    .data(app_state.gql_state.pool.clone())
658                    .data(Arc::clone(&app_state.gql_state.broker));
659                Ok(PreparedOperation {
660                    schema: schema.clone(),
661                    request,
662                    write,
663                })
664            }
665
666            /// Settle an operation's writes before its answer goes out.
667            async fn settle_write(
668                write: &postrust_graphql::context::SharedWrite,
669                response: &mut async_graphql::Response,
670            ) {
671                if let Some(tx) = write.lock().await.take() {
672                    let settled = if response.errors.is_empty() {
673                        tx.commit().await
674                    } else {
675                        tx.rollback().await
676                    };
677                    // A commit that fails is a mutation that did not happen,
678                    // however well every statement in it went. Saying so is
679                    // the whole point of running them together.
680                    if let Err(e) = settled {
681                        tracing::error!("GraphQL mutation could not be settled: {}", e);
682                        response
683                            .errors
684                            .push(async_graphql::ServerError::new(e.to_string(), None));
685                        response.data = async_graphql::Value::Null;
686                    }
687                }
688            }
689
690            async fn one_graphql(
691                app_state: GraphQLAppState,
692                headers: HeaderMap,
693                req: async_graphql::Request,
694            ) -> serde_json::Value {
695                let identity = match graphql_identity(&app_state, headers) {
696                    Ok(identity) => identity,
697                    Err(refusal) => return refusal,
698                };
699                let PreparedOperation {
700                    schema,
701                    request,
702                    write,
703                } = match prepare_operation(&app_state, &identity, req) {
704                    Ok(operation) => operation,
705                    Err(refusal) => return refusal,
706                };
707                let mut response = schema.execute(request).await;
708                settle_write(&write, &mut response).await;
709                postrust_graphql::hasura::envelope(response)
710            }
711
712            /// A Hasura-shaped body as the response a WebSocket carries, so a
713            /// subscription is refused, and its errors read, exactly as the
714            /// same operation over HTTP would be.
715            fn hasura_response(body: serde_json::Value) -> async_graphql::Response {
716                let data = body
717                    .get("data")
718                    .cloned()
719                    .and_then(|data| async_graphql::Value::from_json(data).ok())
720                    .unwrap_or(async_graphql::Value::Null);
721                let mut response = async_graphql::Response::new(data);
722                for error in body
723                    .get("errors")
724                    .and_then(serde_json::Value::as_array)
725                    .into_iter()
726                    .flatten()
727                {
728                    let message = error
729                        .get("message")
730                        .and_then(serde_json::Value::as_str)
731                        .unwrap_or_default();
732                    let mut server_error = async_graphql::ServerError::new(message, None);
733                    if let Some(fields) = error
734                        .get("extensions")
735                        .and_then(serde_json::Value::as_object)
736                    {
737                        let mut extensions = async_graphql::ErrorExtensionValues::default();
738                        for (name, value) in fields {
739                            if let Ok(value) = async_graphql::Value::from_json(value.clone()) {
740                                extensions.set(name, value);
741                            }
742                        }
743                        server_error.extensions = Some(extensions);
744                    }
745                    response.errors.push(server_error);
746                }
747                response
748            }
749
750            /// The headers a WebSocket connection is authenticated with: the
751            /// upgrade request's, overlaid with those the `connection_init`
752            /// payload carries. A browser cannot set headers on a WebSocket,
753            /// so a client puts `Authorization` in the payload -- Hasura's own
754            /// clients under `headers`, others at the top level.
755            fn connection_headers(
756                mut headers: HeaderMap,
757                payload: &serde_json::Value,
758            ) -> HeaderMap {
759                let nested = payload
760                    .get("headers")
761                    .and_then(serde_json::Value::as_object);
762                for fields in [payload.as_object(), nested].into_iter().flatten() {
763                    for (name, value) in fields {
764                        let Some(value) = value.as_str() else {
765                            continue;
766                        };
767                        if let (Ok(name), Ok(value)) = (
768                            axum::http::HeaderName::from_bytes(name.as_bytes()),
769                            axum::http::HeaderValue::from_str(value),
770                        ) {
771                            headers.insert(name, value);
772                        }
773                    }
774                }
775                headers
776            }
777
778            /// Runs every WebSocket operation -- query, mutation or
779            /// subscription -- as the identity its connection was initialised
780            /// with, through the same preparation an HTTP request gets.
781            #[derive(Clone)]
782            struct WebSocketExecutor(GraphQLAppState);
783
784            impl async_graphql::Executor for WebSocketExecutor {
785                async fn execute(
786                    &self,
787                    request: async_graphql::Request,
788                ) -> async_graphql::Response {
789                    use async_graphql::futures_util::StreamExt;
790                    // Not reached over a WebSocket, which runs everything as a
791                    // stream; answered from the stream so the two cannot differ.
792                    self.execute_stream(request, None)
793                        .next()
794                        .await
795                        .unwrap_or_else(|| async_graphql::Response::new(async_graphql::Value::Null))
796                }
797
798                fn execute_stream(
799                    &self,
800                    request: async_graphql::Request,
801                    session_data: Option<Arc<async_graphql::Data>>,
802                ) -> async_graphql::futures_util::stream::BoxStream<'static, async_graphql::Response>
803                {
804                    use async_graphql::futures_util::{stream, StreamExt};
805                    let identity = session_data
806                        .as_deref()
807                        .and_then(|data| data.get(&std::any::TypeId::of::<GraphQLIdentity>()))
808                        .and_then(|identity| identity.downcast_ref::<GraphQLIdentity>())
809                        .cloned();
810                    // `connection_init` refuses a connection it cannot
811                    // identify, so an operation without an identity is one the
812                    // protocol let through before initialising. Refused rather
813                    // than run as nobody in particular.
814                    let Some(identity) = identity else {
815                        let refusal =
816                            postrust_graphql::hasura::denied("the connection was not initialised");
817                        return stream::once(async move { hasura_response(refusal) }).boxed();
818                    };
819                    match prepare_operation(&self.0, &identity, request) {
820                        Err(refusal) => {
821                            stream::once(async move { hasura_response(refusal) }).boxed()
822                        }
823                        Ok(PreparedOperation {
824                            schema,
825                            request,
826                            write,
827                        }) => {
828                            async_graphql::Executor::execute_stream(&schema, request, session_data)
829                                .then(move |mut response| {
830                                    let write = Arc::clone(&write);
831                                    async move {
832                                        settle_write(&write, &mut response).await;
833                                        hasura_response(postrust_graphql::hasura::envelope(
834                                            response,
835                                        ))
836                                    }
837                                })
838                                .boxed()
839                        }
840                    }
841                }
842            }
843
844            /// GraphQL over a WebSocket, authenticated as HTTP is. The
845            /// identity is settled once, from `connection_init`, and a
846            /// connection that would have been refused over HTTP -- a wrong
847            /// admin secret, a role the token does not allow -- is refused
848            /// there rather than served as the anonymous role.
849            async fn graphql_ws(
850                AxumState(app_state): AxumState<GraphQLAppState>,
851                protocol: async_graphql_axum::GraphQLProtocol,
852                headers: HeaderMap,
853                ws: axum::extract::ws::WebSocketUpgrade,
854            ) -> axum::response::Response {
855                ws.protocols(async_graphql::http::ALL_WEBSOCKET_PROTOCOLS)
856                    .on_upgrade(move |socket| async move {
857                        let init_state = app_state.clone();
858                        async_graphql_axum::GraphQLWebSocket::new(
859                            socket,
860                            WebSocketExecutor(app_state),
861                            protocol,
862                        )
863                        .on_connection_init(move |payload| async move {
864                            match graphql_identity(
865                                &init_state,
866                                connection_headers(headers, &payload),
867                            ) {
868                                Ok(identity) => {
869                                    let mut data = async_graphql::Data::default();
870                                    data.insert(identity);
871                                    Ok(data)
872                                }
873                                Err(refusal) => Err(async_graphql::Error::new(
874                                    refusal
875                                        .pointer("/errors/0/message")
876                                        .and_then(serde_json::Value::as_str)
877                                        .unwrap_or("connection refused"),
878                                )),
879                            }
880                        })
881                        .serve()
882                        .await
883                    })
884            }
885
886            // Add GraphQL routes with WebSocket support for subscriptions
887            let graphql_router = Router::new()
888                .route("/", post(handle_graphql))
889                .route("/", get(postrust_graphql::handler::graphql_playground))
890                .with_state(graphql_app_state.clone());
891
892            // The same state as HTTP, because a WebSocket is authenticated the
893            // same way: `postrust_graphql::handler::graphql_ws_handler` carries
894            // no caller identity, so every subscription it served ran without
895            // a role, a session or claims.
896            let ws_router = Router::new()
897                .route("/ws", get(graphql_ws))
898                .with_state(graphql_app_state);
899
900            let graphql_app = match host.websockets {
901                true => graphql_router.merge(ws_router),
902                false => graphql_router,
903            };
904            // `/v1/graphql` is where a Hasura client sends its queries, and it is
905            // the only address most of them can be told about: the endpoint is
906            // baked into generated clients and codegen configs. `/api/graphql`
907            // keeps working for anything already pointed at it, and
908            // `/v1alpha1/graphql` is the address Hasura served before `/v1`
909            // and still answers on -- a client old enough to have been
910            // pointed there is exactly the one that cannot be repointed.
911            app = app
912                .nest("/v1/graphql", graphql_app.clone())
913                .nest("/v1alpha1/graphql", graphql_app.clone())
914                .nest("/api/graphql", graphql_app);
915        }
916    }
917
918    // PostgREST compatibility mode: also serve the REST surface at the root so
919    // canonical PostgREST paths (`/rpc/<name>`, `/<table>`) work in addition to
920    // the `/api`-prefixed paths. Explicit routes (`/`, `/_`, `/admin`, `/api`)
921    // still take precedence; only otherwise-unmatched paths hit this fallback.
922    if config.compat_mode {
923        app = app.fallback(handle_request);
924        info!("PostgREST compatibility mode enabled: REST surface also served at /");
925
926        // Key ordering is fixed when the binary is compiled, so asking for
927        // compatibility at runtime cannot turn it on. Say so, rather than
928        // leaving someone to discover the difference by diffing responses.
929        if !cfg!(feature = "compat-key-order") {
930            tracing::warn!(
931                "compatibility mode: object keys will be returned in alphabetical order, \
932                 not in select order as PostgREST returns them. Build with \
933                 --features compat-key-order to match, at a cost of up to 15% throughput \
934                 on wide rows."
935            );
936        }
937    }
938
939    // Add root info endpoint.
940    //
941    // Not in compatibility mode: there `/` is part of the API surface -- it is
942    // where PostgREST serves the schema description, and where it reports an
943    // `Accept-Profile` naming a schema that is not exposed. A directory of
944    // this server's own endpoints in its place answers a different question
945    // from the one asked.
946    if !config.compat_mode {
947        app = app.route(
948            "/",
949            axum::routing::get(|| async {
950                Json(serde_json::json!({
951                    "name": "postrust",
952                    "version": env!("CARGO_PKG_VERSION"),
953                    "api": "/api",
954                    "custom": "/_",
955                    "health": "/_/health",
956                    "admin": "/admin",
957                    "docs": "/admin/swagger"
958                }))
959            }),
960        );
961    }
962
963    // An empty list is "any origin", which is the default and what
964    // `PGRST_SERVER_CORS_ORIGINS="*"` asks for. Anything that is not a valid
965    // header value is dropped with a warning rather than quietly widening the
966    // policy back to `*` -- a typo in an origin should cost that origin, not
967    // the restriction.
968    let allow_origin = if config.server_cors_origins.is_empty() {
969        AllowOrigin::any()
970    } else {
971        let origins: Vec<HeaderValue> = config
972            .server_cors_origins
973            .iter()
974            .filter_map(|o| match HeaderValue::from_str(o) {
975                Ok(v) => Some(v),
976                Err(_) => {
977                    tracing::warn!("Ignoring CORS origin {:?}: not a valid header value", o);
978                    None
979                }
980            })
981            .collect();
982        info!("CORS restricted to {} origin(s)", origins.len());
983        AllowOrigin::list(origins)
984    };
985
986    // Apply CORS and state
987    let app = app
988        .layer(
989            CorsLayer::new()
990                .allow_origin(allow_origin)
991                .allow_methods([
992                    Method::GET,
993                    Method::POST,
994                    Method::PUT,
995                    Method::PATCH,
996                    Method::DELETE,
997                    Method::OPTIONS,
998                    Method::HEAD,
999                ])
1000                .allow_headers(CorsAny)
1001                .expose_headers(CorsAny),
1002        )
1003        // For the routes whose handlers take a body *extractor* -- GraphQL,
1004        // the admin surface -- which is what this layer reaches. The REST
1005        // handler takes the whole `Request` and reads the body itself, so it
1006        // applies `max_body_size` directly; see `app::process_request`.
1007        .layer(DefaultBodyLimit::max(config.max_body_size))
1008        // Outermost, so it runs before the CORS layer -- which answers every
1009        // OPTIONS itself and never calls what it wraps, so nothing downstream
1010        // of it can say what a resource allows.
1011        .layer(axum::middleware::from_fn_with_state(
1012            state.clone(),
1013            app::options_allow,
1014        ))
1015        .with_state(state.clone());
1016
1017    Ok(App { router: app, state })
1018}
1019
1020/// Reload the REST schema cache on `NOTIFY <channel>`.
1021///
1022/// Reconnects on its own: a listening connection is dropped by a restart of
1023/// the database or anything between, and a reloader that gives up on the first
1024/// error is one that stops working silently partway through a deployment.
1025fn spawn_schema_reloader(state: Arc<AppState>, channel: String) {
1026    const RETRY: std::time::Duration = std::time::Duration::from_secs(5);
1027
1028    tokio::spawn(async move {
1029        loop {
1030            let mut listener = match sqlx::postgres::PgListener::connect_with(&state.pool).await {
1031                Ok(listener) => listener,
1032                Err(e) => {
1033                    error!("Schema reloader could not connect: {}; retrying", e);
1034                    tokio::time::sleep(RETRY).await;
1035                    continue;
1036                }
1037            };
1038
1039            if let Err(e) = listener.listen(&channel).await {
1040                error!("Schema reloader could not LISTEN on {}: {}", channel, e);
1041                tokio::time::sleep(RETRY).await;
1042                continue;
1043            }
1044            info!("Listening on channel {} for schema reloads", channel);
1045
1046            // Inner loop until the connection fails, then reconnect.
1047            loop {
1048                match listener.recv().await {
1049                    Ok(notification) => {
1050                        info!("Schema reload requested: {:?}", notification.payload());
1051                        match state.reload_schema().await {
1052                            Ok(()) => {
1053                                info!(
1054                                    "Schema cache reloaded: {}",
1055                                    state.schema_cache().await.summary()
1056                                )
1057                            }
1058                            // The old cache is kept. Serving the previous
1059                            // schema is better than serving none, and the next
1060                            // notification tries again.
1061                            Err(e) => error!("Schema cache reload failed: {}", e),
1062                        }
1063                    }
1064                    Err(e) => {
1065                        error!("Schema reloader lost its connection: {}; reconnecting", e);
1066                        break;
1067                    }
1068                }
1069            }
1070        }
1071    });
1072}
1073
1074/// Mask database URI for logging.
1075fn mask_db_uri(uri: &str) -> String {
1076    if let Some(at_pos) = uri.find('@') {
1077        if let Some(proto_end) = uri.find("://") {
1078            return format!("{}://***@{}", &uri[..proto_end], &uri[at_pos + 1..]);
1079        }
1080    }
1081    uri.to_string()
1082}