fraiseql_cli/config/toml_schema/security.rs
1//! Security configuration types for `[security.*]` and `[auth]` TOML sections.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7/// Security configuration
8#[derive(Debug, Clone, Deserialize, Serialize)]
9#[serde(default, deny_unknown_fields)]
10pub struct SecuritySettings {
11 /// Default policy to apply if none specified
12 pub default_policy: Option<String>,
13 /// Custom authorization rules
14 pub rules: Vec<AuthorizationRule>,
15 /// Authorization policies
16 pub policies: Vec<AuthorizationPolicy>,
17 /// Field-level authorization rules
18 pub field_auth: Vec<FieldAuthRule>,
19 /// Enterprise security configuration (legacy flags)
20 pub enterprise: EnterpriseSecurityConfig,
21 /// Error sanitization — controls what detail clients see in error responses
22 pub error_sanitization: Option<ErrorSanitizationTomlConfig>,
23 /// Rate limiting — per-endpoint request caps
24 pub rate_limiting: Option<RateLimitingSecurityConfig>,
25 /// State encryption — AEAD encryption for OAuth state and PKCE blobs
26 pub state_encryption: Option<StateEncryptionConfig>,
27 /// PKCE — Proof Key for Code Exchange for OAuth Authorization Code flows
28 pub pkce: Option<PkceConfig>,
29 /// API key authentication — static or database-backed key-based auth
30 pub api_keys: Option<ApiKeySecurityConfig>,
31 /// Token revocation — reject JWTs by `jti` after revocation
32 pub token_revocation: Option<TokenRevocationSecurityConfig>,
33 /// Trusted documents — query allowlist (strict or permissive mode)
34 pub trusted_documents: Option<TrustedDocumentsConfig>,
35 /// Force persisted-operations-only enforcement (#379).
36 ///
37 /// When `true`, the runtime forces the trusted-document store into `strict` mode
38 /// (reject any non-persisted operation) regardless of `[security.trusted_documents].mode`.
39 /// Requires a configured trusted-documents manifest to have any effect.
40 pub persisted_queries_only: bool,
41}
42
43impl Default for SecuritySettings {
44 fn default() -> Self {
45 Self {
46 default_policy: Some("authenticated".to_string()),
47 rules: vec![],
48 policies: vec![],
49 field_auth: vec![],
50 enterprise: EnterpriseSecurityConfig::default(),
51 error_sanitization: None,
52 rate_limiting: None,
53 state_encryption: None,
54 pkce: None,
55 api_keys: None,
56 token_revocation: None,
57 trusted_documents: None,
58 persisted_queries_only: false,
59 }
60 }
61}
62
63/// Authorization rule (custom expressions)
64#[derive(Debug, Clone, Deserialize, Serialize)]
65#[serde(deny_unknown_fields)]
66pub struct AuthorizationRule {
67 /// Rule name
68 pub name: String,
69 /// Rule expression or condition
70 pub rule: String,
71 /// Rule description
72 pub description: Option<String>,
73 /// Whether rule result can be cached
74 #[serde(default)]
75 pub cacheable: bool,
76 /// Cache time-to-live in seconds
77 pub cache_ttl_seconds: Option<u32>,
78}
79
80/// Authorization policy (RBAC/ABAC)
81#[derive(Debug, Clone, Deserialize, Serialize)]
82#[serde(deny_unknown_fields)]
83pub struct AuthorizationPolicy {
84 /// Policy name
85 pub name: String,
86 /// Policy type (RBAC, ABAC, CUSTOM, HYBRID)
87 #[serde(rename = "type")]
88 pub policy_type: String,
89 /// Optional rule expression
90 pub rule: Option<String>,
91 /// Roles this policy applies to
92 pub roles: Vec<String>,
93 /// Combination strategy (ANY, ALL, EXACTLY)
94 pub strategy: Option<String>,
95 /// Attributes for attribute-based access control
96 #[serde(default)]
97 pub attributes: Vec<String>,
98 /// Policy description
99 pub description: Option<String>,
100 /// Cache time-to-live in seconds
101 pub cache_ttl_seconds: Option<u32>,
102}
103
104/// Field-level authorization rule
105#[derive(Debug, Clone, Deserialize, Serialize)]
106#[serde(deny_unknown_fields)]
107pub struct FieldAuthRule {
108 /// Type name this rule applies to
109 pub type_name: String,
110 /// Field name this rule applies to
111 pub field_name: String,
112 /// Policy to enforce
113 pub policy: String,
114}
115
116/// Enterprise security configuration
117#[derive(Debug, Clone, Deserialize, Serialize)]
118#[serde(default, deny_unknown_fields)]
119pub struct EnterpriseSecurityConfig {
120 /// Enable rate limiting
121 pub rate_limiting_enabled: bool,
122 /// Max requests per auth endpoint
123 pub auth_endpoint_max_requests: u32,
124 /// Rate limit window in seconds
125 pub auth_endpoint_window_seconds: u64,
126 /// Enable audit logging
127 pub audit_logging_enabled: bool,
128 /// Audit log backend service
129 pub audit_log_backend: String,
130 /// Audit log retention in days
131 pub audit_retention_days: u32,
132 /// Enable error sanitization
133 pub error_sanitization: bool,
134 /// Hide implementation details in errors
135 pub hide_implementation_details: bool,
136 /// Enable constant-time token comparison
137 pub constant_time_comparison: bool,
138 /// Enable PKCE for OAuth flows
139 pub pkce_enabled: bool,
140}
141
142impl Default for EnterpriseSecurityConfig {
143 fn default() -> Self {
144 Self {
145 rate_limiting_enabled: true,
146 auth_endpoint_max_requests: 100,
147 auth_endpoint_window_seconds: 60,
148 audit_logging_enabled: true,
149 audit_log_backend: "postgresql".to_string(),
150 audit_retention_days: 365,
151 error_sanitization: true,
152 hide_implementation_details: true,
153 constant_time_comparison: true,
154 pkce_enabled: true,
155 }
156 }
157}
158
159/// Controls how much error detail is exposed to API clients.
160/// When enabled, internal error messages, SQL, and stack traces are stripped.
161///
162/// Note: named `ErrorSanitizationTomlConfig` to avoid collision with the identically-named
163/// struct in `config::security` which serves `FraiseQLConfig`.
164#[derive(Debug, Clone, Deserialize, Serialize)]
165#[serde(default, deny_unknown_fields)]
166pub struct ErrorSanitizationTomlConfig {
167 /// Enable error sanitization (default: false — opt-in)
168 pub enabled: bool,
169 /// Strip stack traces, SQL fragments, file paths (default: true)
170 #[serde(default = "default_true")]
171 pub hide_implementation_details: bool,
172 /// Replace raw database error messages with a generic message (default: true)
173 #[serde(default = "default_true")]
174 pub sanitize_database_errors: bool,
175 /// Replacement message shown to clients when an internal error is sanitized
176 pub custom_error_message: Option<String>,
177}
178
179impl Default for ErrorSanitizationTomlConfig {
180 fn default() -> Self {
181 Self {
182 enabled: false,
183 hide_implementation_details: true,
184 sanitize_database_errors: true,
185 custom_error_message: None,
186 }
187 }
188}
189
190/// Per-endpoint and global rate limiting configuration for `[security.rate_limiting]`.
191#[derive(Debug, Clone, Deserialize, Serialize)]
192#[serde(default, deny_unknown_fields)]
193pub struct RateLimitingSecurityConfig {
194 /// Enable rate limiting
195 pub enabled: bool,
196 /// Global request rate cap (requests per second, per IP)
197 pub requests_per_second: u32,
198 /// Burst allowance above the steady-state rate
199 pub burst_size: u32,
200 /// Auth initiation endpoint — max requests per window
201 pub auth_start_max_requests: u32,
202 /// Auth initiation window in seconds
203 pub auth_start_window_secs: u64,
204 /// OAuth callback endpoint — max requests per window
205 pub auth_callback_max_requests: u32,
206 /// OAuth callback window in seconds
207 pub auth_callback_window_secs: u64,
208 /// Token refresh endpoint — max requests per window
209 pub auth_refresh_max_requests: u32,
210 /// Token refresh window in seconds
211 pub auth_refresh_window_secs: u64,
212 /// Logout endpoint — max requests per window
213 pub auth_logout_max_requests: u32,
214 /// Logout window in seconds
215 pub auth_logout_window_secs: u64,
216 /// Failed login attempts before lockout
217 pub failed_login_max_attempts: u32,
218 /// Duration of failed-login lockout in seconds
219 pub failed_login_lockout_secs: u64,
220 /// Per-authenticated-user request rate in requests/second.
221 /// Defaults to 10× `requests_per_second` if not set.
222 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub requests_per_second_per_user: Option<u32>,
224 /// Redis URL for distributed rate limiting (optional — falls back to in-memory)
225 pub redis_url: Option<String>,
226 /// Trust `X-Real-IP` / `X-Forwarded-For` headers for client IP extraction.
227 ///
228 /// Set to `true` only when FraiseQL is deployed behind a trusted reverse proxy
229 /// (e.g. nginx, Cloudflare, AWS ALB) that sets these headers.
230 /// Enabling without a trusted proxy allows clients to spoof their IP address.
231 #[serde(default)]
232 pub trust_proxy_headers: bool,
233 /// CIDR ranges trusted as proxy IPs (e.g. `["10.0.0.0/8", "172.16.0.0/12"]`).
234 ///
235 /// When set and `trust_proxy_headers = true`, `X-Forwarded-For` is only honoured
236 /// when the direct connection IP falls within one of these ranges — requests from
237 /// outside them use the connection IP directly, preventing clients from spoofing
238 /// their address. Use `["0.0.0.0/0"]` to trust every proxy IP explicitly.
239 ///
240 /// When omitted with `trust_proxy_headers = true`, all proxy IPs are trusted
241 /// (less secure — the server emits a startup warning). CIDR strings are validated
242 /// at compile time.
243 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub trusted_proxy_cidrs: Option<Vec<String>>,
245}
246
247impl Default for RateLimitingSecurityConfig {
248 fn default() -> Self {
249 Self {
250 enabled: false,
251 requests_per_second: 100,
252 requests_per_second_per_user: None,
253 burst_size: 200,
254 auth_start_max_requests: 5,
255 auth_start_window_secs: 60,
256 auth_callback_max_requests: 10,
257 auth_callback_window_secs: 60,
258 auth_refresh_max_requests: 20,
259 auth_refresh_window_secs: 300,
260 auth_logout_max_requests: 30,
261 auth_logout_window_secs: 60,
262 failed_login_max_attempts: 10,
263 failed_login_lockout_secs: 900,
264 redis_url: None,
265 trust_proxy_headers: false,
266 trusted_proxy_cidrs: None,
267 }
268 }
269}
270
271/// AEAD algorithm for OAuth state and PKCE state blobs.
272#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
273#[non_exhaustive]
274pub enum EncryptionAlgorithm {
275 /// ChaCha20-Poly1305 (recommended — constant-time, software-friendly)
276 #[default]
277 #[serde(rename = "chacha20-poly1305")]
278 Chacha20Poly1305,
279 /// AES-256-GCM (hardware-accelerated on modern CPUs)
280 #[serde(rename = "aes-256-gcm")]
281 Aes256Gcm,
282}
283
284impl fmt::Display for EncryptionAlgorithm {
285 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286 match self {
287 Self::Chacha20Poly1305 => f.write_str("chacha20-poly1305"),
288 Self::Aes256Gcm => f.write_str("aes-256-gcm"),
289 }
290 }
291}
292
293/// Where the encryption key is sourced from.
294#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
295#[serde(rename_all = "lowercase")]
296#[non_exhaustive]
297pub enum KeySource {
298 /// Read key from an environment variable
299 #[default]
300 Env,
301}
302
303/// AEAD encryption for OAuth state parameter and PKCE code challenges.
304#[derive(Debug, Clone, Deserialize, Serialize)]
305#[serde(default, deny_unknown_fields)]
306pub struct StateEncryptionConfig {
307 /// Enable state encryption
308 pub enabled: bool,
309 /// AEAD algorithm to use
310 pub algorithm: EncryptionAlgorithm,
311 /// Where to source the encryption key
312 pub key_source: KeySource,
313 /// Environment variable holding the 32-byte hex-encoded key
314 pub key_env: Option<String>,
315}
316
317impl Default for StateEncryptionConfig {
318 fn default() -> Self {
319 Self {
320 enabled: false,
321 algorithm: EncryptionAlgorithm::default(),
322 key_source: KeySource::Env,
323 key_env: Some("STATE_ENCRYPTION_KEY".to_string()),
324 }
325 }
326}
327
328/// PKCE code challenge method.
329#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
330#[non_exhaustive]
331pub enum CodeChallengeMethod {
332 /// SHA-256 (required in production)
333 #[default]
334 #[serde(rename = "S256")]
335 S256,
336 /// Plain (spec-allowed but insecure — warns at runtime)
337 #[serde(rename = "plain")]
338 Plain,
339}
340
341/// PKCE (Proof Key for Code Exchange) configuration.
342/// Requires `state_encryption` to be enabled for secure state storage.
343#[derive(Debug, Clone, Deserialize, Serialize)]
344#[serde(default, deny_unknown_fields)]
345pub struct PkceConfig {
346 /// Enable PKCE for OAuth Authorization Code flows
347 pub enabled: bool,
348 /// Code challenge method (`S256` recommended)
349 pub code_challenge_method: CodeChallengeMethod,
350 /// How long the PKCE state is valid before the auth flow expires (seconds)
351 pub state_ttl_secs: u64,
352 /// Redis URL for distributed PKCE state storage across multiple replicas.
353 ///
354 /// Required for multi-replica deployments (Kubernetes, ECS, fly.io with
355 /// multiple instances). Without Redis, `/auth/start` and `/auth/callback`
356 /// must hit the same replica.
357 ///
358 /// Requires the `redis-pkce` Cargo feature to be compiled in.
359 /// Example: `"redis://localhost:6379"` or `"${REDIS_URL}"`.
360 #[serde(skip_serializing_if = "Option::is_none")]
361 pub redis_url: Option<String>,
362}
363
364impl Default for PkceConfig {
365 fn default() -> Self {
366 Self {
367 enabled: false,
368 code_challenge_method: CodeChallengeMethod::S256,
369 state_ttl_secs: 600,
370 redis_url: None,
371 }
372 }
373}
374
375/// API key authentication configuration.
376///
377/// ```toml
378/// [security.api_keys]
379/// enabled = true
380/// header = "X-API-Key"
381/// hash_algorithm = "sha256"
382/// storage = "env"
383///
384/// [[security.api_keys.static]]
385/// key_hash = "sha256:abc123..."
386/// scopes = ["read:*"]
387/// name = "ci-readonly"
388/// ```
389#[derive(Debug, Clone, Deserialize, Serialize)]
390#[serde(default, deny_unknown_fields)]
391pub struct ApiKeySecurityConfig {
392 /// Enable API key authentication
393 pub enabled: bool,
394 /// HTTP header name to read the API key from
395 pub header: String,
396 /// Hash algorithm for key verification (`sha256`)
397 pub hash_algorithm: String,
398 /// Storage backend: `"env"` for static keys, `"postgres"` for DB-backed
399 pub storage: String,
400 /// Static API key entries (only for `storage = "env"`)
401 #[serde(default, rename = "static")]
402 pub static_keys: Vec<StaticApiKeyEntry>,
403}
404
405impl Default for ApiKeySecurityConfig {
406 fn default() -> Self {
407 Self {
408 enabled: false,
409 header: "X-API-Key".to_string(),
410 hash_algorithm: "sha256".to_string(),
411 storage: "env".to_string(),
412 static_keys: vec![],
413 }
414 }
415}
416
417/// A single static API key entry.
418#[derive(Debug, Clone, Deserialize, Serialize)]
419#[serde(deny_unknown_fields)]
420pub struct StaticApiKeyEntry {
421 /// Hex-encoded hash, optionally prefixed with algorithm (e.g. `sha256:abc...`)
422 pub key_hash: String,
423 /// Scopes granted by this key
424 #[serde(default)]
425 pub scopes: Vec<String>,
426 /// Human-readable name for audit logging
427 pub name: String,
428}
429
430/// Trusted document mode.
431#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
432#[serde(rename_all = "lowercase")]
433#[non_exhaustive]
434pub enum TrustedDocumentMode {
435 /// Only documentId requests allowed; raw query strings rejected
436 Strict,
437 /// documentId requests use the manifest; raw queries fall through
438 #[default]
439 Permissive,
440}
441
442/// Trusted documents / query allowlist configuration.
443///
444/// ```toml
445/// [security.trusted_documents]
446/// enabled = true
447/// mode = "strict"
448/// manifest_path = "./trusted-documents.json"
449/// reload_interval_secs = 0
450/// ```
451#[derive(Debug, Clone, Deserialize, Serialize)]
452#[serde(default, deny_unknown_fields)]
453pub struct TrustedDocumentsConfig {
454 /// Enable trusted documents
455 pub enabled: bool,
456 /// Enforcement mode: "strict" or "permissive"
457 pub mode: TrustedDocumentMode,
458 /// Path to the trusted documents manifest JSON file
459 #[serde(skip_serializing_if = "Option::is_none")]
460 pub manifest_path: Option<String>,
461 /// URL to fetch the trusted documents manifest from at startup
462 #[serde(skip_serializing_if = "Option::is_none")]
463 pub manifest_url: Option<String>,
464 /// Poll interval in seconds for hot-reloading the manifest (0 = no reload)
465 #[serde(default)]
466 pub reload_interval_secs: u64,
467}
468
469impl Default for TrustedDocumentsConfig {
470 fn default() -> Self {
471 Self {
472 enabled: false,
473 mode: TrustedDocumentMode::Permissive,
474 manifest_path: None,
475 manifest_url: None,
476 reload_interval_secs: 0,
477 }
478 }
479}
480
481/// Token revocation configuration.
482///
483/// ```toml
484/// [security.token_revocation]
485/// enabled = true
486/// backend = "redis"
487/// require_jti = true
488/// fail_open = false
489/// ```
490#[derive(Debug, Clone, Deserialize, Serialize)]
491#[serde(default, deny_unknown_fields)]
492pub struct TokenRevocationSecurityConfig {
493 /// Enable token revocation
494 pub enabled: bool,
495 /// Backend: `"redis"`, `"postgres"`, or `"memory"`
496 pub backend: String,
497 /// Reject JWTs without a `jti` claim when revocation is enabled
498 #[serde(default = "default_true")]
499 pub require_jti: bool,
500 /// If revocation store is unreachable: `false` = reject (fail-closed), `true` = allow
501 /// (fail-open)
502 #[serde(default)]
503 pub fail_open: bool,
504 /// Redis URL for distributed revocation (optional — inherited from `[fraiseql.redis]` if
505 /// absent)
506 #[serde(skip_serializing_if = "Option::is_none")]
507 pub redis_url: Option<String>,
508 /// How long (seconds) a `revoke-all` epoch is retained.
509 ///
510 /// `revoke-all` records a per-user epoch rather than deleting individual tokens, so the
511 /// entry must outlive every token that could have been issued before the revocation.
512 /// Set this **above your maximum access-token lifetime**; once it expires a pre-revocation
513 /// token would resume working (until its own `exp`). Default: 86400 (24h). The server reads
514 /// this value from the compiled schema.
515 pub revoke_all_ttl_secs: u64,
516}
517
518impl Default for TokenRevocationSecurityConfig {
519 fn default() -> Self {
520 Self {
521 enabled: false,
522 backend: "memory".to_string(),
523 require_jti: true,
524 fail_open: false,
525 redis_url: None,
526 revoke_all_ttl_secs: 86_400,
527 }
528 }
529}
530
531/// OIDC configuration for `[auth]`.
532///
533/// Two independent groups may be configured, together or separately:
534///
535/// - **JWT validation** — `issuer` (required for this group), `audience` (optional). The server
536/// consumes these to validate incoming bearer tokens. Accepted and functional.
537/// - **PKCE OAuth client** (server-side login) — `discovery_url`, `client_id`, `client_secret_env`,
538/// `server_redirect_uri`, configured all four together. **Not yet functional on the compiled path
539/// (tracked in #621):** the compiled schema carries no `auth`/`auth_endpoints` for the server to
540/// consume, so a complete client group is *rejected at compile time* rather than silently
541/// accepted.
542///
543/// At least one group must be present; an empty `[auth]` is a load error. The client
544/// secret itself must never appear here — `client_secret_env` names the environment
545/// variable that holds it.
546///
547/// ```toml
548/// [auth]
549/// issuer = "https://accounts.google.com"
550/// audience = "my-api"
551/// ```
552#[derive(Debug, Clone, Default, Deserialize, Serialize)]
553#[serde(default, deny_unknown_fields)]
554pub struct OidcClientConfig {
555 /// OIDC issuer URL for JWT validation (e.g. `"https://accounts.google.com"`).
556 #[serde(skip_serializing_if = "Option::is_none")]
557 pub issuer: Option<String>,
558 /// Expected `aud` claim for JWT validation (optional; only meaningful with `issuer`).
559 #[serde(skip_serializing_if = "Option::is_none")]
560 pub audience: Option<String>,
561 /// PKCE: OIDC provider discovery URL. **Not yet functional (#621).**
562 #[serde(skip_serializing_if = "Option::is_none")]
563 pub discovery_url: Option<String>,
564 /// PKCE: OAuth2 `client_id` registered with the provider. **Not yet functional (#621).**
565 #[serde(skip_serializing_if = "Option::is_none")]
566 pub client_id: Option<String>,
567 /// PKCE: name of the environment variable that holds the client secret.
568 /// The secret itself must never appear in TOML or the compiled schema.
569 /// **Not yet functional (#621).**
570 #[serde(skip_serializing_if = "Option::is_none")]
571 pub client_secret_env: Option<String>,
572 /// PKCE: the full URL of this server's `/auth/callback` endpoint.
573 /// **Not yet functional (#621).**
574 #[serde(skip_serializing_if = "Option::is_none")]
575 pub server_redirect_uri: Option<String>,
576}
577
578impl OidcClientConfig {
579 /// Validate the `[auth]` group structure (#612 item 9).
580 ///
581 /// The JWT group (`issuer` + optional `audience`) is accepted and functional. The
582 /// PKCE client group is all-four-or-none, and a *complete* client group is rejected
583 /// — it is not yet functional on the compiled path (#621), so it is refused rather
584 /// than silently accepted. At least one group must be present.
585 ///
586 /// # Errors
587 ///
588 /// Returns an error naming the specific problem: an incomplete client group (with the
589 /// missing fields), a complete-but-unsupported client group, `audience` without
590 /// `issuer`, or an empty `[auth]` block.
591 pub fn validate(&self) -> anyhow::Result<()> {
592 let client_fields = [
593 ("discovery_url", self.discovery_url.is_some()),
594 ("client_id", self.client_id.is_some()),
595 ("client_secret_env", self.client_secret_env.is_some()),
596 ("server_redirect_uri", self.server_redirect_uri.is_some()),
597 ];
598 let client_set = client_fields.iter().filter(|(_, set)| *set).count();
599 let has_client_group = client_set > 0;
600 let has_jwt_group = self.issuer.is_some();
601
602 if has_client_group && client_set < client_fields.len() {
603 let missing: Vec<&str> =
604 client_fields.iter().filter(|(_, set)| !*set).map(|(name, _)| *name).collect();
605 anyhow::bail!(
606 "[auth] PKCE OAuth-client config is incomplete: discovery_url, client_id, \
607 client_secret_env, and server_redirect_uri must be set together (missing: {}).",
608 missing.join(", ")
609 );
610 }
611
612 if has_client_group {
613 // client_set == 4 here (all-or-none enforced above).
614 anyhow::bail!(
615 "[auth] PKCE OAuth-client config (discovery_url, client_id, client_secret_env, \
616 server_redirect_uri) is recognized but not yet functional on the compiled path: \
617 the compiled schema carries no auth/auth_endpoints for the server to consume. \
618 Remove these fields and use [auth] issuer/audience for JWT validation, or track \
619 the wiring in #621."
620 );
621 }
622
623 if self.audience.is_some() && !has_jwt_group {
624 anyhow::bail!(
625 "[auth] audience is set but issuer is not. JWT validation requires issuer — \
626 add issuer, or remove audience."
627 );
628 }
629
630 if !has_jwt_group {
631 anyhow::bail!(
632 "[auth] is empty. Configure JWT validation with issuer (audience optional). \
633 An empty [auth] block does nothing."
634 );
635 }
636
637 Ok(())
638 }
639}
640
641fn default_true() -> bool {
642 true
643}