hecks 3.11.0 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/script_runner.adapter +3 -0
- data/lib/hecks/adapters/driven/script_runner.rb +93 -0
- data/lib/hecks/adapters/driven.rb +1 -0
- data/lib/hecks/adapters/driving/cli_runner/resolution.rb +6 -3
- data/lib/hecks/adapters/driving/cli_runner.rb +1 -0
- data/lib/hecks/adapters/driving/newcomer.rb +50 -0
- data/lib/hecks/bluebook/assembly/contracts/read_models.rb +1 -1
- data/lib/hecks/bluebook/assembly/marks.rb +3 -15
- data/lib/hecks/bluebook/assembly/scalars.rb +39 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +6 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +6 -3
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +16 -12
- data/lib/hecks/bluebook/meta_validator.rb +28 -21
- data/lib/hecks/bluebook.rb +1 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +17 -3
- data/lib/hecks/framework/bluebook/change.behaviors +30 -0
- data/lib/hecks/framework/bluebook/change.bluebook +162 -0
- data/lib/hecks/framework/bluebook/change.hecksagon +21 -0
- data/lib/hecks/framework/change.oidc.json +34 -0
- data/lib/hecks/framework/console_settings.oidc.json +103 -0
- data/lib/hecks/framework/governance.oidc.json +28 -0
- data/lib/hecks/framework/identity.oidc.json +18 -0
- data/lib/hecks/framework/privacy.oidc.json +30 -0
- data/lib/hecks/gate/stages.yml +35 -0
- data/lib/hecks/grammar/translation.oidc.json +48 -0
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +12 -1
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +2 -1
- data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +5 -1
- data/lib/hecks/hecks/hecks.world +3 -0
- data/lib/hecks/language/bluebook/bluebook.behaviors +23 -0
- data/lib/hecks/language/bluebook/policy.behaviors +19 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +52 -0
- data/lib/hecks/projection_files/expression_tables.rb +7 -2
- data/lib/hecks/projections/deploy/box/compose.rb +6 -4
- data/lib/hecks/projections/deploy/box/edge.rb +62 -0
- data/lib/hecks/projections/deploy/box/logging.rb +99 -0
- data/lib/hecks/projections/deploy/box/roll_scripts.rb +16 -8
- data/lib/hecks/projections/deploy/box/settings/cdn.rb +40 -0
- data/lib/hecks/projections/deploy/box/settings/logs.rb +40 -0
- data/lib/hecks/projections/deploy/box/settings/patterns.rb +11 -0
- data/lib/hecks/projections/deploy/box/settings/types.rb +16 -1
- data/lib/hecks/projections/deploy/box/settings.rb +6 -2
- data/lib/hecks/projections/deploy/box/stack_yaml.rb +5 -2
- data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +16 -0
- data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +8 -4
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +3 -0
- data/lib/hecks/projections/deploy/box/templates/edge.mk.tmpl +17 -0
- data/lib/hecks/projections/deploy/box/templates/edge.yaml.tmpl +65 -0
- data/lib/hecks/projections/deploy/box/templates/log-group.yaml.tmpl +12 -0
- data/lib/hecks/projections/deploy/box/templates/log-policy.yaml.tmpl +7 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +5 -4
- data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +5 -4
- data/lib/hecks/projections/deploy/box.rb +15 -3
- data/lib/hecks/projections/deploy/fargate/cdn/lines.rb +12 -1
- data/lib/hecks/projections/site/admin_cms/templates/membership.rb +10 -7
- data/lib/hecks/projections/site/edge/behaviours.rb +10 -6
- data/lib/hecks/projections/site/edge/checks.rb +2 -0
- data/lib/hecks/projections/site/edge/function_checks.rb +104 -0
- data/lib/hecks/projections/site/edge/function_code.rb +86 -0
- data/lib/hecks/projections/site/edge/redirect_host.js.tmpl +65 -0
- data/lib/hecks/projections/site/edge.rb +35 -9
- data/lib/hecks/projections/site/site_cdn.rb +21 -7
- data/lib/hecks/projections/site/site_root/workflow.rb +5 -0
- data/lib/hecks/projections/site/table/row_builder.rb +12 -2
- data/lib/hecks/projections/site/table.rb +4 -2
- data/lib/hecks/projector/cli_audience.rb +20 -0
- data/lib/hecks/projector/cli_projector/usage.rb +15 -4
- data/lib/hecks/rust_build/host.rb +11 -4
- data/lib/hecks/rust_build/prebuilt_binary.rb +101 -0
- data/lib/hecks/site/bluebook/site.bluebook +16 -1
- data/lib/hecks/tools/ci_gate_decision.rb +35 -0
- data/lib/hecks/tools/ci_gates.rb +20 -1
- data/lib/hecks/tools/lanes/files.rb +8 -0
- data/lib/hecks/tools/lanes/prebuilt_workflow.rb +81 -0
- data/lib/hecks/tools/lanes.rb +1 -0
- data/lib/hecks/tools/oidc_manifests.rb +44 -6
- data/lib/hecks/tools/release_only.rb +111 -0
- data/lib/hecks/tools/site_routes/templates.rb +13 -9
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +15 -1
- data/prebuilt/2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal +0 -0
- data/prebuilt/58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal +0 -0
- data/prebuilt/{verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json → verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json} +1 -1
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/auth.rs +58 -0
- data/rust/host/src/extension.rs +13 -1
- data/rust/host/src/lib.rs +1 -0
- data/rust/host/src/platform_signin/attack_tests.rs +1100 -0
- data/rust/host/src/platform_signin/config.rs +512 -0
- data/rust/host/src/platform_signin/cookie.rs +237 -0
- data/rust/host/src/platform_signin/extension.rs +190 -0
- data/rust/host/src/platform_signin/fetcher.rs +322 -0
- data/rust/host/src/platform_signin/handler.rs +251 -0
- data/rust/host/src/platform_signin/handler_tests.rs +554 -0
- data/rust/host/src/platform_signin/jti.rs +196 -0
- data/rust/host/src/platform_signin/jwks.rs +542 -0
- data/rust/host/src/platform_signin/jws.rs +249 -0
- data/rust/host/src/platform_signin/known_answer_tests.rs +237 -0
- data/rust/host/src/platform_signin/mapping_tests.rs +254 -0
- data/rust/host/src/platform_signin/mod.rs +75 -0
- data/rust/host/src/platform_signin/mode_tests.rs +382 -0
- data/rust/host/src/platform_signin/pages.rs +204 -0
- data/rust/host/src/platform_signin/query.rs +233 -0
- data/rust/host/src/platform_signin/session.rs +151 -0
- data/rust/host/src/platform_signin/settings.rs +385 -0
- data/rust/host/src/platform_signin/strict_json.rs +351 -0
- data/rust/host/src/platform_signin/test_support.rs +235 -0
- data/rust/host/src/platform_signin/ttl_tests.rs +94 -0
- data/rust/host/src/platform_signin/verify.rs +604 -0
- data/rust/host/src/platform_signin/wiring_support.rs +319 -0
- data/rust/host/src/web.rs +50 -6
- data/rust/parser/src/parse/value_object.rs +22 -10
- metadata +59 -7
- data/prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal +0 -0
- data/prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal +0 -0
- /data/lib/hecks/framework/{oidc.json → compliance.oidc.json} +0 -0
- /data/lib/hecks/grammar/{oidc.json → expression.oidc.json} +0 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
//! The settings that switch platform sign-in on, read once at boot from an explicit map.
|
|
2
|
+
//!
|
|
3
|
+
//! The switch is `PLATFORM_SIGNIN`. Unset or empty means the feature does not exist: no extension
|
|
4
|
+
//! is installed and the host answers exactly as it did before, whatever other `PLATFORM_*`
|
|
5
|
+
//! variables are present, so switching off is one unset. Any other value must be `alongside` or
|
|
6
|
+
//! `default`, and then every other setting the flow needs must be present and valid, or the boot
|
|
7
|
+
//! is refused. A refusal names the variable and the kind of mistake, never the value, because a
|
|
8
|
+
//! mistyped value is often a pasted secret.
|
|
9
|
+
//!
|
|
10
|
+
//! Nothing here reads the process environment: [`SigninSettings::from_env_map`] takes a map, and
|
|
11
|
+
//! [`PROCESS_VARIABLES`] lists the names a caller copies from the process into it.
|
|
12
|
+
|
|
13
|
+
use std::collections::HashMap;
|
|
14
|
+
use std::fmt;
|
|
15
|
+
|
|
16
|
+
use super::config::{ConfigError, PlatformSigninConfig};
|
|
17
|
+
|
|
18
|
+
/// The variable that switches the feature on and chooses how.
|
|
19
|
+
pub const MODE_VARIABLE: &str = "PLATFORM_SIGNIN";
|
|
20
|
+
/// The public address of this site; the callback and the cookie flags derive from it.
|
|
21
|
+
pub const SITE_URL_VARIABLE: &str = "SITE_URL";
|
|
22
|
+
/// The secret that signs the account cookie.
|
|
23
|
+
pub const SESSION_SECRET_VARIABLE: &str = "SESSION_SECRET";
|
|
24
|
+
/// The variable that says this process is one long-lived server.
|
|
25
|
+
pub const SERVE_MODE_VARIABLE: &str = "HECKS_SERVE_MODE";
|
|
26
|
+
/// The variable that names the account cookie.
|
|
27
|
+
pub const ACCOUNT_COOKIE_VARIABLE: &str = "HECKS_SESSION_COOKIE";
|
|
28
|
+
|
|
29
|
+
/// Every variable the settings read, so a caller can copy exactly these from the process.
|
|
30
|
+
pub const PROCESS_VARIABLES: [&str; 9] = [
|
|
31
|
+
MODE_VARIABLE,
|
|
32
|
+
super::config::ISSUER_URL_VARIABLE,
|
|
33
|
+
super::config::JWKS_URL_VARIABLE,
|
|
34
|
+
super::config::SITE_ID_VARIABLE,
|
|
35
|
+
super::config::ENVIRONMENT_VARIABLE,
|
|
36
|
+
SITE_URL_VARIABLE,
|
|
37
|
+
SESSION_SECRET_VARIABLE,
|
|
38
|
+
SERVE_MODE_VARIABLE,
|
|
39
|
+
ACCOUNT_COOKIE_VARIABLE,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/// How the platform flow sits next to the site's existing Google sign-in.
|
|
43
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
44
|
+
pub enum Mode {
|
|
45
|
+
/// `GET /auth/platform` starts the platform flow; `/auth/google` is untouched.
|
|
46
|
+
Alongside,
|
|
47
|
+
/// `GET /auth/google` starts the platform flow, `?via=google` keeps the Google one.
|
|
48
|
+
Default,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Everything the flow needs, validated.
|
|
52
|
+
#[derive(Debug, Clone)]
|
|
53
|
+
pub struct SigninSettings {
|
|
54
|
+
pub mode: Mode,
|
|
55
|
+
pub config: PlatformSigninConfig,
|
|
56
|
+
/// The site's public address without a trailing slash; always https.
|
|
57
|
+
pub site_url: String,
|
|
58
|
+
pub session_secret: String,
|
|
59
|
+
pub account_cookie: String,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// A refused setting: which variable, and what kind of mistake, never the value.
|
|
63
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
64
|
+
pub enum SettingsError {
|
|
65
|
+
/// One of the issuer, key set, site or environment settings.
|
|
66
|
+
Config(ConfigError),
|
|
67
|
+
/// Any other setting.
|
|
68
|
+
Setting {
|
|
69
|
+
variable: &'static str,
|
|
70
|
+
problem: &'static str,
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
impl fmt::Display for SettingsError {
|
|
75
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
76
|
+
match self {
|
|
77
|
+
SettingsError::Config(error) => write!(f, "{error}"),
|
|
78
|
+
SettingsError::Setting { variable, problem } => write!(f, "{variable} {problem}"),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
impl std::error::Error for SettingsError {}
|
|
84
|
+
|
|
85
|
+
impl SettingsError {
|
|
86
|
+
/// Returns the name of the variable at fault.
|
|
87
|
+
pub fn variable(&self) -> &'static str {
|
|
88
|
+
match self {
|
|
89
|
+
SettingsError::Config(error) => error.variable,
|
|
90
|
+
SettingsError::Setting { variable, .. } => variable,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
impl SigninSettings {
|
|
96
|
+
/// Builds the settings, or `None` when `PLATFORM_SIGNIN` is unset or empty.
|
|
97
|
+
///
|
|
98
|
+
/// # Errors
|
|
99
|
+
///
|
|
100
|
+
/// Returns the first setting that is missing or malformed once the switch is on.
|
|
101
|
+
pub fn from_env_map(vars: &HashMap<String, String>) -> Result<Option<Self>, SettingsError> {
|
|
102
|
+
let Some(mode) = read_mode(vars)? else {
|
|
103
|
+
return Ok(None);
|
|
104
|
+
};
|
|
105
|
+
let config = PlatformSigninConfig::from_env_map(vars).map_err(SettingsError::Config)?;
|
|
106
|
+
let site_url = read_site_url(vars)?;
|
|
107
|
+
let session_secret = required(vars, SESSION_SECRET_VARIABLE)?.to_string();
|
|
108
|
+
check_serve_mode(vars)?;
|
|
109
|
+
let account_cookie = read_account_cookie(vars)?;
|
|
110
|
+
Ok(Some(Self {
|
|
111
|
+
mode,
|
|
112
|
+
config,
|
|
113
|
+
site_url,
|
|
114
|
+
session_secret,
|
|
115
|
+
account_cookie,
|
|
116
|
+
}))
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fn setting(variable: &'static str, problem: &'static str) -> SettingsError {
|
|
121
|
+
SettingsError::Setting { variable, problem }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fn optional<'a>(vars: &'a HashMap<String, String>, name: &str) -> Option<&'a str> {
|
|
125
|
+
vars.get(name)
|
|
126
|
+
.map(String::as_str)
|
|
127
|
+
.filter(|value| !value.is_empty())
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fn required<'a>(
|
|
131
|
+
vars: &'a HashMap<String, String>,
|
|
132
|
+
name: &'static str,
|
|
133
|
+
) -> Result<&'a str, SettingsError> {
|
|
134
|
+
optional(vars, name).ok_or_else(|| setting(name, "is not set"))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Unset or empty is off; exactly `alongside` or `default` is on; anything else is refused.
|
|
138
|
+
fn read_mode(vars: &HashMap<String, String>) -> Result<Option<Mode>, SettingsError> {
|
|
139
|
+
match optional(vars, MODE_VARIABLE) {
|
|
140
|
+
None => Ok(None),
|
|
141
|
+
Some("alongside") => Ok(Some(Mode::Alongside)),
|
|
142
|
+
Some("default") => Ok(Some(Mode::Default)),
|
|
143
|
+
Some(_) => Err(setting(
|
|
144
|
+
MODE_VARIABLE,
|
|
145
|
+
"must be alongside or default, or be left unset",
|
|
146
|
+
)),
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// The site address must be a bare https origin; one trailing slash is dropped.
|
|
151
|
+
fn read_site_url(vars: &HashMap<String, String>) -> Result<String, SettingsError> {
|
|
152
|
+
let url = required(vars, SITE_URL_VARIABLE)?;
|
|
153
|
+
let origin = url.strip_suffix('/').unwrap_or(url);
|
|
154
|
+
let authority = origin.strip_prefix("https://").unwrap_or("");
|
|
155
|
+
let plain = !authority.is_empty() && !authority.contains(['/', '?', '#', '@', '\\', ' ']);
|
|
156
|
+
if plain {
|
|
157
|
+
Ok(origin.to_string())
|
|
158
|
+
} else {
|
|
159
|
+
Err(setting(
|
|
160
|
+
SITE_URL_VARIABLE,
|
|
161
|
+
"must be a plain https address with no path, query or credentials",
|
|
162
|
+
))
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// The single-use record of token identifiers lives in this process, so it must be the only one.
|
|
167
|
+
fn check_serve_mode(vars: &HashMap<String, String>) -> Result<(), SettingsError> {
|
|
168
|
+
match optional(vars, SERVE_MODE_VARIABLE) {
|
|
169
|
+
Some("1") => Ok(()),
|
|
170
|
+
_ => Err(setting(
|
|
171
|
+
SERVE_MODE_VARIABLE,
|
|
172
|
+
"must be 1: platform sign-in needs the single long-lived server",
|
|
173
|
+
)),
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn read_account_cookie(vars: &HashMap<String, String>) -> Result<String, SettingsError> {
|
|
178
|
+
crate::auth::resolve_account_cookie(optional(vars, ACCOUNT_COOKIE_VARIABLE))
|
|
179
|
+
.map_err(|_| setting(ACCOUNT_COOKIE_VARIABLE, "is not a valid cookie name"))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#[cfg(test)]
|
|
183
|
+
mod tests {
|
|
184
|
+
use super::*;
|
|
185
|
+
|
|
186
|
+
fn full() -> HashMap<String, String> {
|
|
187
|
+
[
|
|
188
|
+
("PLATFORM_SIGNIN", "alongside"),
|
|
189
|
+
("PLATFORM_ISSUER_URL", "https://issuer.example"),
|
|
190
|
+
("PLATFORM_SITE_ID", "site-a"),
|
|
191
|
+
("PLATFORM_ENVIRONMENT", "production"),
|
|
192
|
+
("SITE_URL", "https://site.example"),
|
|
193
|
+
("SESSION_SECRET", "a-long-session-secret-for-tests"),
|
|
194
|
+
("HECKS_SERVE_MODE", "1"),
|
|
195
|
+
]
|
|
196
|
+
.into_iter()
|
|
197
|
+
.map(|(k, v)| (k.to_string(), v.to_string()))
|
|
198
|
+
.collect()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
fn without(name: &str) -> HashMap<String, String> {
|
|
202
|
+
let mut vars = full();
|
|
203
|
+
vars.remove(name);
|
|
204
|
+
vars
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fn with(name: &str, value: &str) -> HashMap<String, String> {
|
|
208
|
+
let mut vars = full();
|
|
209
|
+
vars.insert(name.to_string(), value.to_string());
|
|
210
|
+
vars
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fn refused(vars: &HashMap<String, String>) -> SettingsError {
|
|
214
|
+
SigninSettings::from_env_map(vars).expect_err("the settings are refused")
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#[test]
|
|
218
|
+
fn an_unset_switch_means_no_feature_even_when_other_variables_are_set() {
|
|
219
|
+
let vars = without("PLATFORM_SIGNIN");
|
|
220
|
+
assert!(SigninSettings::from_env_map(&vars).unwrap().is_none());
|
|
221
|
+
assert!(SigninSettings::from_env_map(&HashMap::new())
|
|
222
|
+
.unwrap()
|
|
223
|
+
.is_none());
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#[test]
|
|
227
|
+
fn an_empty_switch_means_no_feature() {
|
|
228
|
+
assert!(SigninSettings::from_env_map(&with("PLATFORM_SIGNIN", ""))
|
|
229
|
+
.unwrap()
|
|
230
|
+
.is_none());
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#[test]
|
|
234
|
+
fn alongside_and_default_are_the_two_modes() {
|
|
235
|
+
let alongside = SigninSettings::from_env_map(&full()).unwrap().unwrap();
|
|
236
|
+
assert_eq!(alongside.mode, Mode::Alongside);
|
|
237
|
+
let default = SigninSettings::from_env_map(&with("PLATFORM_SIGNIN", "default"))
|
|
238
|
+
.unwrap()
|
|
239
|
+
.unwrap();
|
|
240
|
+
assert_eq!(default.mode, Mode::Default);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#[test]
|
|
244
|
+
fn any_other_switch_value_refuses_naming_only_the_variable() {
|
|
245
|
+
for bad in [
|
|
246
|
+
"ALONGSIDE",
|
|
247
|
+
"on",
|
|
248
|
+
"off",
|
|
249
|
+
"true",
|
|
250
|
+
"1",
|
|
251
|
+
"Default",
|
|
252
|
+
" alongside",
|
|
253
|
+
"alongside ",
|
|
254
|
+
"both",
|
|
255
|
+
] {
|
|
256
|
+
let error = refused(&with("PLATFORM_SIGNIN", bad));
|
|
257
|
+
assert_eq!(error.variable(), "PLATFORM_SIGNIN", "{bad:?}");
|
|
258
|
+
assert_eq!(
|
|
259
|
+
error.to_string(),
|
|
260
|
+
"PLATFORM_SIGNIN must be alongside or default, or be left unset",
|
|
261
|
+
"{bad:?}"
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
#[test]
|
|
267
|
+
fn each_required_setting_missing_refuses_naming_it() {
|
|
268
|
+
for name in [
|
|
269
|
+
"PLATFORM_ISSUER_URL",
|
|
270
|
+
"PLATFORM_SITE_ID",
|
|
271
|
+
"PLATFORM_ENVIRONMENT",
|
|
272
|
+
"SITE_URL",
|
|
273
|
+
"SESSION_SECRET",
|
|
274
|
+
"HECKS_SERVE_MODE",
|
|
275
|
+
] {
|
|
276
|
+
let error = refused(&without(name));
|
|
277
|
+
assert_eq!(error.variable(), name);
|
|
278
|
+
assert!(error.to_string().starts_with(name), "{error}");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
#[test]
|
|
283
|
+
fn the_key_set_address_stays_optional() {
|
|
284
|
+
let settings = SigninSettings::from_env_map(&full()).unwrap().unwrap();
|
|
285
|
+
assert_eq!(
|
|
286
|
+
settings.config.jwks_url,
|
|
287
|
+
"https://issuer.example/.well-known/jwks.json"
|
|
288
|
+
);
|
|
289
|
+
let own = with("PLATFORM_JWKS_URL", "https://issuer.example/keys.json");
|
|
290
|
+
assert_eq!(
|
|
291
|
+
SigninSettings::from_env_map(&own)
|
|
292
|
+
.unwrap()
|
|
293
|
+
.unwrap()
|
|
294
|
+
.config
|
|
295
|
+
.jwks_url,
|
|
296
|
+
"https://issuer.example/keys.json"
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
#[test]
|
|
301
|
+
fn a_bad_pinned_value_refuses_naming_the_variable_and_never_the_value() {
|
|
302
|
+
let sentinel = "http://leaky-sentinel.example/path?secret=hunter2";
|
|
303
|
+
for name in [
|
|
304
|
+
"PLATFORM_ISSUER_URL",
|
|
305
|
+
"PLATFORM_JWKS_URL",
|
|
306
|
+
"PLATFORM_SITE_ID",
|
|
307
|
+
"PLATFORM_ENVIRONMENT",
|
|
308
|
+
"SITE_URL",
|
|
309
|
+
] {
|
|
310
|
+
let error = refused(&with(name, sentinel));
|
|
311
|
+
assert_eq!(error.variable(), name);
|
|
312
|
+
let text = error.to_string();
|
|
313
|
+
assert!(
|
|
314
|
+
!text.contains("sentinel") && !text.contains("hunter2"),
|
|
315
|
+
"{name}: {text}"
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
#[test]
|
|
321
|
+
fn the_site_address_must_be_a_plain_https_origin() {
|
|
322
|
+
for bad in [
|
|
323
|
+
"http://site.example",
|
|
324
|
+
"site.example",
|
|
325
|
+
"https://site.example/app",
|
|
326
|
+
"https://site.example?x=1",
|
|
327
|
+
"https://user@site.example",
|
|
328
|
+
] {
|
|
329
|
+
assert_eq!(
|
|
330
|
+
refused(&with("SITE_URL", bad)).variable(),
|
|
331
|
+
"SITE_URL",
|
|
332
|
+
"{bad}"
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
let trimmed = SigninSettings::from_env_map(&with("SITE_URL", "https://site.example/"))
|
|
336
|
+
.unwrap()
|
|
337
|
+
.unwrap();
|
|
338
|
+
assert_eq!(trimmed.site_url, "https://site.example");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
#[test]
|
|
342
|
+
fn a_session_secret_is_required_and_never_printed() {
|
|
343
|
+
assert_eq!(
|
|
344
|
+
refused(&with("SESSION_SECRET", "")).variable(),
|
|
345
|
+
"SESSION_SECRET"
|
|
346
|
+
);
|
|
347
|
+
let error = refused(&without("SESSION_SECRET"));
|
|
348
|
+
assert!(!error.to_string().contains("a-long-session-secret"));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
#[test]
|
|
352
|
+
fn the_per_process_jti_record_needs_the_single_server_mode() {
|
|
353
|
+
for bad in ["0", "", "true", "yes"] {
|
|
354
|
+
assert_eq!(
|
|
355
|
+
refused(&with("HECKS_SERVE_MODE", bad)).variable(),
|
|
356
|
+
"HECKS_SERVE_MODE",
|
|
357
|
+
"{bad:?}"
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
#[test]
|
|
363
|
+
fn the_account_cookie_name_defaults_and_an_invalid_one_refuses() {
|
|
364
|
+
let default = SigninSettings::from_env_map(&full()).unwrap().unwrap();
|
|
365
|
+
assert_eq!(default.account_cookie, crate::auth::DEFAULT_ACCOUNT_COOKIE);
|
|
366
|
+
let named = SigninSettings::from_env_map(&with("HECKS_SESSION_COOKIE", "site_session"))
|
|
367
|
+
.unwrap()
|
|
368
|
+
.unwrap();
|
|
369
|
+
assert_eq!(named.account_cookie, "site_session");
|
|
370
|
+
assert_eq!(
|
|
371
|
+
refused(&with("HECKS_SESSION_COOKIE", "bad name;")).variable(),
|
|
372
|
+
"HECKS_SESSION_COOKIE"
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
#[test]
|
|
377
|
+
fn the_process_variable_list_names_exactly_what_is_read() {
|
|
378
|
+
for name in PROCESS_VARIABLES {
|
|
379
|
+
let vars = without(name);
|
|
380
|
+
let _ = SigninSettings::from_env_map(&vars);
|
|
381
|
+
}
|
|
382
|
+
assert!(PROCESS_VARIABLES.contains(&"PLATFORM_SIGNIN"));
|
|
383
|
+
assert!(PROCESS_VARIABLES.contains(&"SESSION_SECRET"));
|
|
384
|
+
}
|
|
385
|
+
}
|