1use aes_gcm::{Aes256Gcm, KeyInit};
3use jwt_simple::reexports::anyhow::{self, ensure};
4
5use core::fmt;
7
8use sec::Secret;
10
11use std::fmt::Debug;
13
14use serde::{Deserialize, Deserializer};
16
17use regex::Regex;
19
20use url::Url;
22
23#[derive(Clone, Debug, Deserialize)]
25pub struct V2PluginConfiguration {
26 #[serde(with = "serde_regex")]
28 pub exclude_hosts: Vec<Regex>,
29 #[serde(with = "serde_regex")]
31 pub exclude_paths: Vec<Regex>,
32 #[serde(with = "serde_regex")]
34 pub exclude_urls: Vec<Regex>,
35
36 pub access_token_header_name: Option<String>,
40 pub access_token_header_prefix: Option<String>,
43
44 pub id_token_header_name: Option<String>,
47 pub id_token_header_prefix: Option<String>,
50
51 pub cookie_name: String,
54 pub logout_path: String,
56 pub filter_plugin_cookies: bool,
59 pub cookie_duration_in_s: u64,
61 pub token_validation: bool,
63 #[serde(deserialize_with = "deserialize_aes_key")]
65 pub aes_key: Secret<Aes256Gcm>,
66
67 pub reload_interval_in_h: u64,
70 pub ticking_interval_in_ms: u64,
72 pub open_id_configs: Vec<OpenIdConfig>,
74}
75
76impl V2PluginConfiguration {
77 pub fn parse(config_bytes: &[u8]) -> anyhow::Result<(Self, bool)> {
78 if let Ok(config) = serde_yaml::from_slice::<Self>(config_bytes) {
80 config.validate()?;
81 return Ok((config, false));
82 }
83
84 match serde_yaml::from_slice::<V1PluginConfiguration>(config_bytes) {
86 Ok(legacy_config) => {
87 let config = legacy_config.to_new_format()?;
88 config.validate()?;
89 Ok((config, true))
90 }
91 Err(e) => {
92 anyhow::bail!(
93 "Failed to parse configuration in both new and legacy formats. Last error: {}",
94 e
95 )
96 }
97 }
98 }
99
100 fn validate(&self) -> anyhow::Result<()> {
103 ensure!(self.reload_interval_in_h > 0, "`reload_interval` is 0");
104 ensure!(self.ticking_interval_in_ms > 0, "`ticking_interval` is 0");
105 ensure!(
106 self.cookie_name.len() <= 32,
107 "`cookie_name` is too long, max 32"
108 );
109
110 let cookies_name_regex = Regex::new(r"^[\w\d-]+$").unwrap();
111 ensure!(cookies_name_regex.is_match(&self.cookie_name), "`cookie_name` is empty or not valid meaning that it contains invalid characters like ;, =, :, /, space");
112
113 ensure!(!self.logout_path.is_empty(), "`logout_path` is empty");
114 ensure!(
115 self.logout_path.starts_with('/'),
116 "`logout_path` does not start with a `/`"
117 );
118 ensure!(self.cookie_duration_in_s > 0, "`cookie_duration_in_s` is 0");
119
120 for provider in &self.open_id_configs {
121 ensure!(!provider.authority.is_empty(), "`authority` is empty");
122 ensure!(!provider.client_id.is_empty(), "`client_id` is empty");
123 ensure!(!provider.scope.is_empty(), "`scope` is empty");
124 ensure!(
125 !provider.client_secret.reveal().is_empty(),
126 "`client_secret` is empty"
127 );
128 ensure!(!provider.audience.is_empty(), "audience is empty");
129 }
130
131 Ok(())
132 }
133}
134
135#[derive(Clone, Debug, Deserialize)]
137pub struct OpenIdConfig {
138 pub name: String,
141 pub image: Url,
143
144 pub config_endpoint: Url,
147 pub upstream_cluster: String,
149 pub authority: String,
151 pub redirect_uri: Url,
153 pub client_id: String,
155 pub scope: String,
157 pub claims: serde_json::Map<String, serde_json::Value>,
159
160 pub client_secret: Secret<String>,
163 pub audience: String,
165}
166
167fn default_logout_path() -> Option<String> {
169 Some("/logout".to_string())
170}
171
172#[derive(Clone, Debug, Deserialize)]
175struct V1PluginConfiguration {
176 pub config_endpoint: Url,
179 pub reload_interval_in_h: u64,
181
182 #[serde(with = "serde_regex")]
184 pub exclude_hosts: Vec<Regex>,
185 #[serde(with = "serde_regex")]
187 pub exclude_paths: Vec<Regex>,
188 #[serde(with = "serde_regex")]
190 pub exclude_urls: Vec<Regex>,
191
192 pub access_token_header_name: Option<String>,
195 pub access_token_header_prefix: Option<String>,
197 pub id_token_header_name: Option<String>,
199 pub id_token_header_prefix: Option<String>,
201
202 pub cookie_name: String,
205 #[serde(default = "default_logout_path")]
207 pub logout_path: Option<String>,
208 pub filter_plugin_cookies: bool,
210 pub cookie_duration: u64,
212 pub token_validation: bool,
214 #[serde(deserialize_with = "deserialize_aes_key")]
216 pub aes_key: Secret<Aes256Gcm>,
217
218 pub authority: String,
221 pub redirect_uri: Url,
223 pub client_id: String,
225 pub scope: String,
227 pub claims: String,
229 pub client_secret: Secret<String>,
231 pub audience: String,
233}
234
235impl V1PluginConfiguration {
236 #[allow(clippy::wrong_self_convention)]
238 fn to_new_format(self) -> anyhow::Result<V2PluginConfiguration> {
239 let claims: serde_json::Map<String, serde_json::Value> = serde_json::from_str(&self.claims)
241 .map_err(|e| anyhow::anyhow!("Failed to parse claims JSON in legacy config: {}", e))?;
242
243 let default_image = Url::parse(
245 "https://developers.elementor.com/docs/assets/img/elementor-placeholder-image.png",
246 )
247 .expect("Default placeholder URL should be valid");
248
249 let open_id_config = OpenIdConfig {
251 name: "legacy-provider".to_string(),
252 image: default_image,
253 config_endpoint: self.config_endpoint,
254 upstream_cluster: "oidc".to_string(),
255 authority: self.authority,
256 redirect_uri: self.redirect_uri,
257 client_id: self.client_id,
258 scope: self.scope,
259 claims,
260 client_secret: self.client_secret,
261 audience: self.audience,
262 };
263
264 Ok(V2PluginConfiguration {
266 open_id_configs: vec![open_id_config],
267 reload_interval_in_h: self.reload_interval_in_h,
268 ticking_interval_in_ms: 500, exclude_hosts: self.exclude_hosts,
270 exclude_paths: self.exclude_paths,
271 exclude_urls: self.exclude_urls,
272 access_token_header_name: self.access_token_header_name,
273 access_token_header_prefix: self.access_token_header_prefix,
274 id_token_header_name: self.id_token_header_name,
275 id_token_header_prefix: self.id_token_header_prefix,
276 cookie_name: self.cookie_name,
277 logout_path: self.logout_path.unwrap_or_else(|| "/logout".to_string()),
278 filter_plugin_cookies: self.filter_plugin_cookies,
279 cookie_duration_in_s: self.cookie_duration,
280 token_validation: self.token_validation,
281 aes_key: self.aes_key,
282 })
283 }
284}
285
286fn deserialize_aes_key<'de, D>(deserializer: D) -> Result<Secret<Aes256Gcm>, D::Error>
288where
289 D: Deserializer<'de>,
290{
291 use base64::{engine::general_purpose::STANDARD as base64engine, Engine as _};
292 use serde::de::{Error, Visitor};
293
294 struct AesKeyVisitor;
295
296 impl<'de> Visitor<'de> for AesKeyVisitor {
297 type Value = Secret<Aes256Gcm>;
298
299 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
300 formatter.write_str("a base64 string encoding a 32 byte AES key")
301 }
302
303 fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
304 where
305 E: Error,
306 {
307 let aes_key = base64engine.decode(s).map_err(Error::custom)?;
308 let cipher = Aes256Gcm::new_from_slice(&aes_key).map_err(|e| {
309 Error::custom(format!("{e}, got {} bytes, expected 32", aes_key.len()))
310 })?;
311
312 Ok(Secret::new(cipher))
313 }
314 }
315
316 deserializer.deserialize_str(AesKeyVisitor)
319}