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