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                /// 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}