grpc_graphql_gateway 1.2.4

A Rust implementation of gRPC-GraphQL gateway - generates GraphQL execution code from gRPC services
# Query Whitelisting

Query Whitelisting (also known as **Stored Operations** or **Persisted Operations**) is a critical security feature that restricts which GraphQL queries can be executed. This is essential for public-facing GraphQL APIs and required for many compliance standards.

## Why Query Whitelisting?

### Security Benefits

- **Prevents Arbitrary Queries**: Only pre-approved queries can be executed
- **Reduces Attack Surface**: Prevents schema exploration and DoS attacks
- **Compliance**: Required for PCI-DSS, HIPAA, SOC 2, and other standards
- **Performance**: Known queries can be optimized and monitored
- **Audit Trail**: Track exactly which queries are being used

### Common Use Cases

1. **Public APIs**: Prevent malicious actors from crafting expensive queries
2. **Mobile Applications**: Apps typically have a fixed set of queries
3. **Third-Party Integrations**: Control exactly what partners can query
4. **Compliance Requirements**: Meet security standards for regulated industries

## Configuration

### Basic Setup

```rust
use grpc_graphql_gateway::{Gateway, QueryWhitelistConfig, WhitelistMode};
use std::collections::HashMap;

let mut allowed_queries = HashMap::new();
allowed_queries.insert(
    "getUserById".to_string(),
    "query getUserById($id: ID!) { user(id: $id) { id name } }".to_string()
);

let gateway = Gateway::builder()
    .with_query_whitelist(QueryWhitelistConfig {
        mode: WhitelistMode::Enforce,
        allowed_queries,
        allow_introspection: false,
    })
    .build()?;
```

### Loading from JSON File

For production deployments, it's recommended to load queries from a configuration file:

```rust
let config = QueryWhitelistConfig::from_json_file(
    "config/allowed_queries.json",
    WhitelistMode::Enforce
)?;

let gateway = Gateway::builder()
    .with_query_whitelist(config)
    .build()?;
```

**Example JSON file** (`allowed_queries.json`):

```json
{
  "getUserById": "query getUserById($id: ID!) { user(id: $id) { id name email } }",
  "listProducts": "query { products { id name price } }",
  "createOrder": "mutation createOrder($input: OrderInput!) { createOrder(input: $input) { id } }"
}
```

## Enforcement Modes

### Enforce Mode (Production)

Rejects non-whitelisted queries with an error.

```rust
QueryWhitelistConfig {
    mode: WhitelistMode::Enforce,
    // ...
}
```

**Error response:**
```json
{
  "errors": [{
    "message": "Query not in whitelist: Operation 'unknownQuery' (hash: 1234abcd...)",
    "extensions": {
      "code": "QUERY_NOT_WHITELISTED"
    }
  }]
}
```

### Warn Mode (Staging)

Logs warnings but allows all queries. Useful for testing and identifying missing queries.

```rust
QueryWhitelistConfig {
    mode: WhitelistMode::Warn,
    // ...
}
```

**Server log:**
```
WARN grpc_graphql_gateway::query_whitelist: Query not in whitelist (allowed in Warn mode): Query hash: 0eb2d2f2e9111722
```

### Disabled Mode (Development)

No whitelist checking. Same as not configuring a whitelist.

```rust
QueryWhitelistConfig::disabled()
```

## Validation Methods

The whitelist supports two validation methods that can be used together:

### 1. Hash-Based Validation

Queries are validated by their SHA-256 hash. This is automatic and requires no client changes.

```graphql
# This query's hash is calculated automatically
query { user(id: "123") { name } }
```

#### Query Normalization (v0.3.7+)

The gateway normalizes queries before hashing, so semantically equivalent queries produce the same hash. This means the following queries all match the same whitelist entry:

```graphql
# Original
query { hello(name: "World") { message } }

# With extra whitespace
query   {   hello( name: "World" )   { message } }

# With comments stripped
query { # This is ignored
  hello(name: "World") { message }
}

# Multi-line format
query {
  hello(name: "World") {
    message
  }
}
```

**Normalization rules:**
- Comments (`#` line comments and `"""` block comments) are removed
- Whitespace is collapsed (multiple spaces → single space)
- Whitespace around punctuation (`{`, `}`, `(`, `)`, `:`, etc.) is removed
- String literals are preserved exactly
- Newlines are treated as whitespace

### 2. Operation ID Validation

Clients can explicitly reference queries by ID using GraphQL extensions:

**Client request:**
```json
{
  "query": "query getUserById($id: ID!) { user(id: $id) { name } }",
  "variables": {"id": "123"},
  "extensions": {
    "operationId": "getUserById"
  }
}
```

The gateway validates the `operationId` against the whitelist.

## Introspection Control

You can optionally allow introspection queries even in Enforce mode:

```rust
QueryWhitelistConfig {
    mode: WhitelistMode::Enforce,
    allowed_queries: queries,
    allow_introspection: true,  // Allow __schema and __type queries
}
```

This is useful for development and staging environments where developers need to explore the schema.

## Runtime Management

The whitelist supports runtime modifications for dynamic use cases:

```rust
// Get whitelist reference
let whitelist = gateway.mux().query_whitelist().unwrap();

// Register new query at runtime
whitelist.register_query(
    "newQuery".to_string(),
    "query { newField }".to_string()
);

// Remove a query
whitelist.remove_query("oldQuery");

// Get statistics
let stats = whitelist.stats();
println!("Total allowed queries: {}", stats.total_queries);
println!("Mode: {:?}", stats.mode);
```

## Best Practices

### 1. Use Enforce Mode in Production

Always use `WhitelistMode::Enforce` in production environments:

```rust
let mode = if std::env::var("ENV")? == "production" {
    WhitelistMode::Enforce
} else {
    WhitelistMode::Warn
};
```

### 2. Start with Warn Mode

When first implementing whitelisting:

1. Deploy with `Warn` mode in staging
2. Monitor logs to identify all queries
3. Add missing queries to whitelist
4. Switch to `Enforce` mode once complete

### 3. Version Control Your Whitelist

Store `allowed_queries.json` in version control alongside your application code.

### 4. Automated Query Extraction

For frontend applications, consider using tools to automatically extract queries from your codebase:

- **GraphQL Code Generator**: Extract queries from React/Vue components
- **Apollo CLI**: Generate persisted query manifests
- **Relay Compiler**: Built-in persisted query support

### 5. CI/CD Integration

Validate the whitelist file in your CI pipeline:

```bash
# Validate JSON syntax
jq empty allowed_queries.json

# Run gateway with test queries
cargo test --test query_whitelist_validation
```

## Working with APQ

Query Whitelisting and Automatic Persisted Queries (APQ) serve different purposes and work well together:

| Feature | Purpose | Security Level |
|---------|---------|----------------|
| **APQ** | Bandwidth optimization (caches any query) | Low |
| **Whitelist** | Security (only allows pre-approved queries) | High |
| **Both** | Bandwidth savings + Security | Maximum |

**Example configuration with both:**

```rust
Gateway::builder()
    // APQ for bandwidth optimization
    .with_persisted_queries(PersistedQueryConfig {
        cache_size: 1000,
        ttl: Some(Duration::from_secs(3600)),
    })
    // Whitelist for security
    .with_query_whitelist(QueryWhitelistConfig {
        mode: WhitelistMode::Enforce,
        allowed_queries: load_queries()?,
        allow_introspection: false,
    })
    .build()?
```

## Migration Guide

### Step 1: Inventory Queries

Use Warn mode to identify all queries currently in use:

```rust
.with_query_whitelist(QueryWhitelistConfig {
    mode: WhitelistMode::Warn,
    allowed_queries: HashMap::new(),
    allow_introspection: true,
})
```

Monitor logs for 1-2 weeks to capture all query variations.

### Step 2: Build Whitelist

Extract unique query hashes from logs and build your whitelist file.

### Step 3: Test in Staging

Deploy with the whitelist in Warn mode to staging:

```bash
# Monitor for any warnings
grep "Query not in whitelist" /var/log/gateway.log
```

### Step 4: Production Deployment

Once confident, switch to Enforce mode:

```rust
.with_query_whitelist(QueryWhitelistConfig {
    mode: WhitelistMode::Enforce,
    allowed_queries: load_queries()?,
    allow_introspection: false,  // Disable in production
})
```

## Troubleshooting

### Query Rejected Despite Being in Whitelist

**Problem**: Query is in the whitelist but still gets rejected.

**Solution**: Ensure the query string exactly matches, including whitespace. Consider normalizing queries or using operation IDs.

### Too Many Warnings in Warn Mode

**Problem**: Logs are flooded with warnings.

**Solution**: This is expected when first implementing. Collect all unique queries and add them to the whitelist.

### Performance Impact

**Problem**: Concerned about validation overhead.

**Solution**: Hash calculation is fast (SHA-256). For 1000 RPS, overhead is <1ms. Consider caching if needed.

## Example: Complete Production Setup

```rust
use grpc_graphql_gateway::{Gateway, QueryWhitelistConfig, WhitelistMode};
use std::path::Path;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Determine mode from environment
    let is_production = std::env::var("ENV")
        .map(|e| e == "production")
        .unwrap_or(false);
    
    // Load whitelist configuration
    let whitelist_config = if Path::new("config/allowed_queries.json").exists() {
        QueryWhitelistConfig::from_json_file(
            "config/allowed_queries.json",
            if is_production {
                WhitelistMode::Enforce
            } else {
                WhitelistMode::Warn
            }
        )?
    } else {
        QueryWhitelistConfig::disabled()
    };
    
    // Build gateway with production settings
    let gateway = Gateway::builder()
        .with_descriptor_set_bytes(DESCRIPTORS)
        .with_query_whitelist(whitelist_config)
        .with_response_cache(CacheConfig::default())
        .with_circuit_breaker(CircuitBreakerConfig::default())
        .with_compression(CompressionConfig::default())
        .build()?;
    
    gateway.serve("0.0.0.0:8888").await?;
    Ok(())
}
```

## See Also

- [Introspection Control]../advanced/introspection.md - Disabling schema introspection
- [Automatic Persisted Queries]../performance/apq.md - Bandwidth optimization
- [DoS Protection]./dos-protection.md - Query depth and complexity limits