bulwark_config/config.rs
1//! The config module provides the internal representation of Bulwark's configuration.
2
3use crate::ResolutionError;
4use bytes::Bytes;
5use itertools::Itertools;
6use regex::Regex;
7use serde::Serialize;
8use std::collections::{HashMap, HashSet};
9use std::fmt::{Display, Formatter};
10use std::path::PathBuf;
11use url::Url;
12use validator::Validate;
13
14lazy_static! {
15 static ref RE_VALID_REFERENCE: Regex = Regex::new(r"^[a-z_]([a-z0-9_])*$").unwrap();
16}
17
18/// The root of a Bulwark configuration.
19///
20/// Wraps all child configuration structures and provides the internal representation of Bulwark's configuration.
21#[derive(Debug, Clone)]
22pub struct Config {
23 /// Configuration for the services being launched.
24 pub service: Service,
25 /// Configuration for the services being launched.
26 pub runtime: Runtime,
27 /// Configuration for state managed by Bulwark plugins.
28 pub state: State,
29 /// Configuration for the decision thresholds.
30 pub thresholds: Thresholds,
31 /// Configuration for metrics collection.
32 pub metrics: Metrics,
33 /// A list of configurations for individual secrets.
34 pub secrets: Vec<Secret>,
35 /// A list of configurations for individual plugins.
36 pub plugins: Vec<Plugin>,
37 /// A list of plugin groups that allows a plugin set to be loaded with a single reference.
38 pub presets: Vec<Preset>,
39 /// A list of routes that maps from resource paths to plugins or presets.
40 pub resources: Vec<Resource>,
41 // TODO: It might make sense to convert the vectors to maps since both routes and references should be unique.
42}
43
44impl Config {
45 /// Looks up the [`Secret`] corresponding to the `reference` string.
46 ///
47 /// # Arguments
48 ///
49 /// * `reference` - A string that corresponds to a [`Secret::reference`] value.
50 pub fn secret<'a>(&self, reference: &str) -> Option<&Secret>
51 where
52 Secret: 'a,
53 {
54 self.secrets
55 .iter()
56 .find(|&secret| secret.reference == reference)
57 }
58
59 /// Looks up the [`Plugin`] corresponding to the `reference` string.
60 ///
61 /// # Arguments
62 ///
63 /// * `reference` - A string that corresponds to a [`Plugin::reference`] value.
64 pub fn plugin<'a>(&self, reference: &str) -> Option<&Plugin>
65 where
66 Plugin: 'a,
67 {
68 self.plugins
69 .iter()
70 .find(|&plugin| plugin.reference == reference)
71 }
72
73 /// Looks up the [`Preset`] corresponding to the `reference` string.
74 ///
75 /// # Arguments
76 ///
77 /// * `reference` - A string that corresponds to a [`Preset::reference`] value.
78 pub fn preset<'a>(&self, reference: &str) -> Option<&Preset>
79 where
80 Preset: 'a,
81 {
82 self.presets
83 .iter()
84 .find(|&preset| preset.reference == reference)
85 }
86}
87
88/// Configuration for the services being launched.
89#[derive(Debug, Clone)]
90pub struct Service {
91 /// The port for the primary service.
92 pub port: u16,
93 /// The port for the admin service and health checks.
94 pub admin_port: u16,
95 /// True if the admin service is enabled, false otherwise.
96 pub admin_enabled: bool,
97 /// The number of trusted proxy hops expected to be exterior to Bulwark.
98 ///
99 /// This number does not include Bulwark or the proxy hosting it in the proxy hop count. Zero implies that
100 /// there are no other proxies exterior to Bulwark. This is used to ensure the `Forwarded` and `X-Forwarded-For`
101 /// headers are not spoofed. If this is set incorrectly, the client IP reported to plugins will be incorrect.
102 pub proxy_hops: u8,
103 // TODO: it may be useful to introduce an "auto" setting for `proxy_hops` since it's possible to auto-discover
104}
105
106/// The default [`Service::port`] value.
107pub const DEFAULT_PORT: u16 = 8089;
108/// The default [`Service::admin_port`] value.
109pub const DEFAULT_ADMIN_PORT: u16 = 8090;
110
111impl Default for Service {
112 /// Default service config
113 fn default() -> Self {
114 Self {
115 port: DEFAULT_PORT,
116 admin_port: DEFAULT_ADMIN_PORT,
117 admin_enabled: true,
118 proxy_hops: 0,
119 }
120 }
121}
122
123/// Configuration for the runtime environment.
124#[derive(Debug, Clone)]
125pub struct Runtime {
126 /// The maximum number of concurrent incoming requests that the runtime will process before blocking.
127 pub max_concurrent_requests: usize,
128 /// The maximum number of concurrent plugin tasks that the runtime will launch.
129 pub max_plugin_tasks: usize,
130}
131
132/// The default [`Runtime::max_concurrent_requests`] value.
133pub const DEFAULT_MAX_CONCURRENT_REQUESTS: usize = 8;
134
135/// The default [`Runtime::max_plugin_tasks`] value.
136pub const DEFAULT_MAX_PLUGIN_TASKS: usize = 16;
137
138impl Default for Runtime {
139 /// Default runtime config
140 fn default() -> Self {
141 Self {
142 max_concurrent_requests: DEFAULT_MAX_CONCURRENT_REQUESTS,
143 max_plugin_tasks: DEFAULT_MAX_PLUGIN_TASKS,
144 }
145 }
146}
147
148/// Configuration for state managed by Bulwark plugins.
149#[derive(Debug, Clone)]
150pub struct State {
151 /// The URI for the external Redis state store.
152 pub redis_uri: Option<String>,
153 /// The size of the Redis connection pool.
154 pub redis_pool_size: usize,
155}
156
157impl Default for State {
158 /// Default runtime config
159 fn default() -> Self {
160 Self {
161 redis_uri: None,
162 redis_pool_size: num_cpus::get_physical() * 4,
163 }
164 }
165}
166
167/// Configuration for the decision thresholds.
168///
169/// No threshold is necessary for the default `allowed` outcome because it is defined by the range between the
170/// `suspicious` threshold and the `trusted` threshold. The thresholds must have values in descending order, with
171/// `restrict` > `suspicious` > `trusted`. None of the threshold values may be equal.
172#[derive(Debug, Clone, Copy)]
173pub struct Thresholds {
174 /// True if the primary service should take no action in response to restrict decisions.
175 pub observe_only: bool,
176 /// Any decision value above the `restrict` threshold will cause the corresponding request to be blocked.
177 pub restrict: f64,
178 /// Any decision value above the `suspicious` threshold will cause the corresponding request to be flagged as
179 /// suspicious but it will still be allowed.
180 pub suspicious: f64,
181 /// Any decision value below the `trust` threshold will cause the corresponding request to be flagged as trusted.
182 /// This primarily affects plugins which use feedback loops.
183 pub trust: f64,
184}
185
186/// The default [`Thresholds::observe_only`] value.
187pub const DEFAULT_OBSERVE_ONLY: bool = false;
188/// The default [`Thresholds::restrict`] value.
189pub const DEFAULT_RESTRICT_THRESHOLD: f64 = 0.8;
190/// The default [`Thresholds::suspicious`] value.
191pub const DEFAULT_SUSPICIOUS_THRESHOLD: f64 = 0.6;
192/// The default [`Thresholds::trust`] value.
193pub const DEFAULT_TRUST_THRESHOLD: f64 = 0.2;
194
195impl Default for Thresholds {
196 /// Default decision thresholds.
197 fn default() -> Self {
198 Self {
199 observe_only: DEFAULT_OBSERVE_ONLY,
200 restrict: DEFAULT_RESTRICT_THRESHOLD,
201 suspicious: DEFAULT_SUSPICIOUS_THRESHOLD,
202 trust: DEFAULT_TRUST_THRESHOLD,
203 }
204 }
205}
206
207/// Configuration for metrics collection.
208#[derive(Debug, Clone)]
209pub struct Metrics {
210 pub statsd_host: Option<String>,
211 pub statsd_port: Option<u16>,
212 pub statsd_queue_size: usize,
213 pub statsd_buffer_size: usize,
214 pub statsd_prefix: String,
215}
216
217/// The default [`Metrics::statsd_port`] value.
218pub const DEFAULT_STATSD_PORT: Option<u16> = Some(8125);
219
220/// The default [`Metrics::statsd_queue_size`] value.
221pub const DEFAULT_STATSD_QUEUE_SIZE: usize = 5000;
222
223/// The default [`Metrics::statsd_buffer_size`] value.
224pub const DEFAULT_STATSD_BUFFER_SIZE: usize = 1024;
225
226impl Default for Metrics {
227 /// Default metrics config
228 fn default() -> Self {
229 Self {
230 statsd_host: None,
231 statsd_port: DEFAULT_STATSD_PORT,
232 statsd_queue_size: DEFAULT_STATSD_QUEUE_SIZE,
233 statsd_buffer_size: DEFAULT_STATSD_BUFFER_SIZE,
234 statsd_prefix: String::from(""),
235 }
236 }
237}
238
239/// Configuration for a secret that Bulwark will need to reference.
240#[derive(Debug, Validate, Clone, Default)]
241pub struct Secret {
242 /// The secret reference key. Should be limited to ASCII lowercase a-z plus underscores. Maximum 96 characters.
243 #[validate(length(min = 1, max = 96), regex(path = "RE_VALID_REFERENCE"))]
244 pub reference: String,
245 /// The location where the secret can be loaded from.
246 pub location: SecretLocation,
247}
248
249/// The location where a secret is stored.
250#[derive(Debug, Clone)]
251pub enum SecretLocation {
252 /// The secret is an environment variable.
253 EnvVar(String),
254 /// The secret is mounted as a file.
255 File(PathBuf),
256}
257
258impl Default for SecretLocation {
259 /// Defaults to an unusable empty environment variable.
260 fn default() -> Self {
261 Self::EnvVar(String::new())
262 }
263}
264
265/// The location where the plugin WASM can be loaded from.
266#[derive(Debug, Clone)]
267pub enum PluginLocation {
268 /// The plugin is a local file.
269 Local(PathBuf),
270 /// The plugin is a remote file served over HTTPS.
271 Remote(Url),
272 /// The plugin is an binary blob.
273 Bytes(Bytes),
274}
275
276impl Default for PluginLocation {
277 /// Defaults to an unusable empty byte vector.
278 fn default() -> Self {
279 Self::Bytes(Bytes::new())
280 }
281}
282
283impl Display for PluginLocation {
284 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
285 match self {
286 PluginLocation::Local(path) => write!(f, "[local: {}]", path.display()),
287 PluginLocation::Remote(uri) => write!(f, "[remote: {}]", uri),
288 PluginLocation::Bytes(bytes) => write!(f, "[{} bytes]", bytes.len()),
289 }
290 }
291}
292
293/// The access control applied to the plugin.
294///
295/// Typically used in conjunction with privately distributed remote plugins.
296#[derive(Debug, Clone)]
297pub enum PluginAccess {
298 /// No authentication provided.
299 None,
300 /// The authentication is provided via an HTTPS Authorization header.
301 ///
302 /// The entire header value will be sent verbatim. This will generally only work for
303 /// basic authentication or bearer tokens.
304 ///
305 /// This is a secret reference. A corresponding [`Secret`] must be provided.
306 Header(String),
307}
308
309impl Default for PluginAccess {
310 /// Defaults to no authentication.
311 fn default() -> Self {
312 Self::None
313 }
314}
315
316/// Verification that plugin contents are what was expected.
317#[derive(Debug, Clone)]
318pub enum PluginVerification {
319 /// No verification.
320 None,
321 /// The plugin is hashed with a SHA-256 digest.
322 Sha256(Bytes),
323}
324
325impl Default for PluginVerification {
326 /// Defaults to no verification.
327 fn default() -> Self {
328 Self::None
329 }
330}
331
332/// The configuration for an individual plugin.
333///
334/// This structure will be wrapped by structs in the host environment.
335#[derive(Debug, Validate, Clone, Default)]
336pub struct Plugin {
337 /// The plugin reference key. Should be limited to ASCII lowercase a-z plus underscores. Maximum 96 characters.
338 #[validate(length(min = 1, max = 96), regex(path = "RE_VALID_REFERENCE"))]
339 pub reference: String,
340 /// The location where the plugin WASM can be loaded from.
341 pub location: PluginLocation,
342 /// The access requirements for the plugin. If the plugin requires authentication, this can be provided here.
343 pub access: PluginAccess,
344 /// Verification that plugin contents match what was expected.
345 pub verification: PluginVerification,
346 /// A weight to multiply this plugin's decision values by.
347 ///
348 /// A 1.0 value has no effect on the decision. See [`bulwark_decision::Decision::weight`].
349 #[validate(range(min = 0.0))]
350 pub weight: f64,
351 // TODO: this might be better represented as a valuable::Mappable / valuable::Value
352 /// JSON-serializable configuration passed into the plugin environment.
353 ///
354 /// The host environment will not do anything with this value beyond serialization.
355 pub config: serde_json::map::Map<String, serde_json::Value>,
356 /// The permissions granted to this plugin.
357 ///
358 /// Any attempt to perform an operation within the plugin sandbox that requires a permission to be set will fail.
359 pub permissions: Permissions,
360}
361
362/// The default [`Plugin::weight`] value.
363pub const DEFAULT_PLUGIN_WEIGHT: f64 = 1.0;
364
365/// The permissions granted to an associated plugin.
366#[derive(Debug, Clone, Default)]
367pub struct Permissions {
368 /// A list of environment variables a plugin may acquire values for.
369 ///
370 /// This permission may be used to grant a plugin fine-grained access to a specific secret.
371 pub env: Vec<String>,
372 /// A list of domains that a plugin may make HTTP requests to.
373 ///
374 /// The permission value must case-insensitively match the entire host component of the request URI.
375 pub http: Vec<String>,
376 /// A list of key prefixes that a plugin may get or set within the external state store.
377 ///
378 /// This permission also affects rate limits and circuit breakers since they also use the external state store.
379 pub state: Vec<String>,
380}
381
382/// A mapping between a reference identifier and a list of plugins that form a preset plugin group.
383#[derive(Debug, Validate, Clone)]
384pub struct Preset {
385 /// The preset reference key. Should be limited to ASCII lowercase a-z plus underscores. Maximum 96 characters.
386 #[validate(length(min = 1, max = 96), regex(path = "RE_VALID_REFERENCE"))]
387 pub reference: String,
388 /// The list of references to plugins and other presets contained within this preset.
389 #[validate(length(min = 1))]
390 pub plugins: Vec<Reference>,
391}
392
393impl Preset {
394 /// Resolves all references within a `Preset`, producing a flattened list of the corresponding [`Plugin`]s.
395 ///
396 /// # Arguments
397 ///
398 /// * `config` - A [`Config`] reference to perform lookups againsts.
399 /// `Preset`s do not maintain their own references to their parent [`Config`] so this must be passed in.
400 ///
401 /// See [`Config::plugin`] and [`Config::preset`].
402 pub fn resolve_plugins<'a>(
403 &'a self,
404 config: &'a Config,
405 ) -> Result<Vec<&Plugin>, ResolutionError> {
406 let mut resolved_presets = HashSet::new();
407 self.resolve_plugins_recursive(config, &mut resolved_presets)
408 }
409
410 /// Resolves all references, checking for cycles.
411 fn resolve_plugins_recursive<'a>(
412 &'a self,
413 config: &'a Config,
414 resolved_presets: &mut HashSet<String>,
415 ) -> Result<Vec<&Plugin>, ResolutionError> {
416 let mut plugins: HashMap<String, &Plugin> = HashMap::with_capacity(self.plugins.len());
417 for reference in &self.plugins {
418 match reference {
419 Reference::Plugin(ref_name) => {
420 if let Some(plugin) = config.plugin(ref_name.as_str()) {
421 plugins.insert(plugin.reference.to_string(), plugin);
422 }
423 }
424 Reference::Preset(ref_name) => {
425 if resolved_presets.contains(ref_name) {
426 return Err(ResolutionError::CircularPreset(ref_name.to_string()));
427 } else {
428 resolved_presets.insert(ref_name.to_string());
429 }
430 if let Some(preset) = config.preset(ref_name.as_str()) {
431 let inner_plugins =
432 preset.resolve_plugins_recursive(config, resolved_presets)?;
433 for inner_plugin in inner_plugins {
434 plugins.insert(inner_plugin.reference.to_string(), inner_plugin);
435 }
436 }
437 }
438 Reference::Missing(ref_name) => {
439 return Err(ResolutionError::Missing(ref_name.to_string()));
440 }
441 }
442 }
443 Ok(plugins.values().cloned().collect())
444 }
445}
446
447/// A mapping between a route pattern and the plugins that should be run for matching requests.
448#[derive(Debug, Clone)]
449pub struct Resource {
450 /// The route pattern used to match requests with.
451 ///
452 /// Uses `matchit` router patterns.
453 pub routes: Vec<String>,
454 /// The plugin references for this route.
455 pub plugins: Vec<Reference>,
456 /// The maximum amount of time a plugin may take for each execution phase.
457 pub timeout: Option<u64>,
458}
459
460impl Resource {
461 /// Expands routes to make them more user-friendly.
462 ///
463 /// # Arguments
464 ///
465 /// * `routes` - The route patterns to expand.
466 /// * `exact` - Whether route expansion should ignore trailing slashes or not.
467 /// * `prefix` - Whether route expansion should add a catch-all '/{*suffix}' pattern to each route.
468 pub(crate) fn expand_routes(routes: &[String], exact: bool, prefix: bool) -> Vec<String> {
469 let mut new_routes = routes.to_vec();
470 if !exact {
471 for route in routes.iter() {
472 let new_route = if route.ends_with('/') && route.len() > 1 {
473 route[..route.len() - 1].to_string()
474 } else if !route.contains("{*") && !route.ends_with('/') {
475 route.clone() + "/"
476 } else {
477 continue;
478 };
479 if !new_routes.contains(&new_route) {
480 new_routes.push(new_route);
481 }
482 }
483 }
484 if prefix {
485 for route in new_routes.clone().iter() {
486 if !route.contains("{*") {
487 let new_route = PathBuf::from(route)
488 .join("{*suffix}")
489 .to_string_lossy()
490 .to_string();
491 if !new_routes.contains(&new_route) {
492 new_routes.push(new_route);
493 }
494 }
495 }
496 }
497 new_routes.sort_by_key(|route| -(route.len() as i64));
498 new_routes
499 }
500
501 /// Resolves all references within a `Resource`, producing a flattened list of the corresponding [`Plugin`]s.
502 ///
503 /// # Arguments
504 ///
505 /// * `config` - A [`Config`] reference to perform lookups againsts.
506 /// `Resource`s do not maintain their own references to their parent [`Config`] so this must be passed in.
507 ///
508 /// See [`Config::plugin`] and [`Config::preset`].
509 pub fn resolve_plugins<'a>(
510 &'a self,
511 config: &'a Config,
512 ) -> Result<Vec<&Plugin>, ResolutionError> {
513 let mut plugins: Vec<&Plugin> = Vec::with_capacity(self.plugins.len());
514 for reference in &self.plugins {
515 match reference {
516 Reference::Plugin(ref_name) => {
517 if let Some(plugin) = config.plugin(ref_name.as_str()) {
518 plugins.push(plugin);
519 }
520 }
521 Reference::Preset(ref_name) => {
522 if let Some(preset) = config.preset(ref_name.as_str()) {
523 let mut inner_plugins = preset.resolve_plugins(config)?;
524 plugins.append(&mut inner_plugins);
525 }
526 }
527 Reference::Missing(ref_name) => {
528 return Err(ResolutionError::Missing(ref_name.to_string()));
529 }
530 }
531 }
532 Ok(plugins
533 .iter()
534 .sorted_by(|a, b| Ord::cmp(&a.reference, &b.reference))
535 .copied()
536 .collect())
537 }
538}
539
540/// Wraps reference strings and differentiates what the reference points to.
541#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
542pub enum Reference {
543 /// A reference to a [`Plugin`].
544 Plugin(String),
545 /// A reference to a [`Preset`].
546 Preset(String),
547 /// A reference that could not be resolved.
548 Missing(String),
549}