hecks 3.11.0 → 3.12.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/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.rb +1 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +16 -2
- data/lib/hecks/gate/stages.yml +32 -0
- 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/site/site_root/workflow.rb +5 -0
- 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/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/release_only.rb +111 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +15 -1
- data/prebuilt/9ee54e2f0d4d6d3fe046827779b221e02de665fba911a048ae5dd8b8fbfb7d90.marshal +0 -0
- data/prebuilt/d74a00e996222c1d8b676d5959092effc46b4dc59e0555f3bf9554ac58e3094a.marshal +0 -0
- data/prebuilt/{verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json → verdicts-df19f35868d3284a70f55e42dcef87ef4e65e6a678e5135f72e1f6cff4c1357b.json} +1 -1
- data/rust/host/HECKS_RELEASE +1 -1
- 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 +42 -4
- data/prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal +0 -0
- data/prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
//! The site's own sign-in cookie, `__Host-signin`, which binds a callback to the browser that
|
|
2
|
+
//! started the flow.
|
|
3
|
+
//!
|
|
4
|
+
//! The value is `<nonce>.<state>`, both base64url, written exactly as the issuer's reference
|
|
5
|
+
//! implementation writes it so the two sides agree. The `__Host-` prefix makes a browser insist on
|
|
6
|
+
//! `Secure`, `Path=/` and no `Domain`, so a sibling subdomain cannot plant a lookalike. `Secure` is
|
|
7
|
+
//! written unconditionally rather than inferred from the request, because the host sits behind
|
|
8
|
+
//! proxies that speak plain http to it. `SameSite=Lax`, not `Strict`: the return from the issuer is
|
|
9
|
+
//! a cross-site top-level navigation, and a strict cookie would not be sent on it.
|
|
10
|
+
//!
|
|
11
|
+
//! Reading is strict in the other direction: a cookie that appears twice is refused, because which
|
|
12
|
+
//! of two values counts is unknowable.
|
|
13
|
+
|
|
14
|
+
use serde_json::Value;
|
|
15
|
+
|
|
16
|
+
use super::verify::SigninCookie;
|
|
17
|
+
|
|
18
|
+
/// The attributes every write of the cookie carries, the same ones that set it and delete it.
|
|
19
|
+
const ATTRIBUTES: &str = "Path=/; Secure; HttpOnly; SameSite=Lax";
|
|
20
|
+
/// The cookie's name.
|
|
21
|
+
pub const NAME: &str = "__Host-signin";
|
|
22
|
+
/// How long the browser keeps the cookie, in seconds.
|
|
23
|
+
pub const MAX_AGE_SECONDS: u32 = 600;
|
|
24
|
+
/// The shortest nonce or state accepted.
|
|
25
|
+
pub const MIN_VALUE_LENGTH: usize = 16;
|
|
26
|
+
/// The longest nonce or state accepted.
|
|
27
|
+
pub const MAX_VALUE_LENGTH: usize = 128;
|
|
28
|
+
|
|
29
|
+
/// Why the cookie could not be used.
|
|
30
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
31
|
+
pub enum CookieFault {
|
|
32
|
+
/// The browser sent no such cookie.
|
|
33
|
+
Absent,
|
|
34
|
+
/// The browser sent it more than once.
|
|
35
|
+
Repeated,
|
|
36
|
+
/// The value is not `<nonce>.<state>` of acceptable shape.
|
|
37
|
+
Malformed,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Returns the `Set-Cookie` value that stores the nonce and state in the browser.
|
|
41
|
+
pub fn set_header(cookie: &SigninCookie) -> String {
|
|
42
|
+
format!(
|
|
43
|
+
"{NAME}={}.{}; Max-Age={MAX_AGE_SECONDS}; {ATTRIBUTES}",
|
|
44
|
+
cookie.nonce, cookie.state
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Returns the `Set-Cookie` value that deletes the cookie, with the attributes that set it.
|
|
49
|
+
pub fn clear_header() -> String {
|
|
50
|
+
format!("{NAME}=; Max-Age=0; {ATTRIBUTES}")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Collects every `name=value` pair the request carried, from the event's `cookies` array and its
|
|
54
|
+
/// `cookie` header.
|
|
55
|
+
pub fn request_pairs(event: &Value) -> Vec<String> {
|
|
56
|
+
let listed = event
|
|
57
|
+
.get("cookies")
|
|
58
|
+
.and_then(Value::as_array)
|
|
59
|
+
.into_iter()
|
|
60
|
+
.flatten()
|
|
61
|
+
.filter_map(Value::as_str);
|
|
62
|
+
let header = event
|
|
63
|
+
.get("headers")
|
|
64
|
+
.and_then(|h| h.get("cookie"))
|
|
65
|
+
.and_then(Value::as_str)
|
|
66
|
+
.into_iter()
|
|
67
|
+
.flat_map(|h| h.split(';'));
|
|
68
|
+
listed
|
|
69
|
+
.chain(header)
|
|
70
|
+
.map(|pair| pair.trim().to_string())
|
|
71
|
+
.filter(|pair| !pair.is_empty())
|
|
72
|
+
.collect()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Finds the sign-in cookie among the request's pairs.
|
|
76
|
+
///
|
|
77
|
+
/// # Errors
|
|
78
|
+
///
|
|
79
|
+
/// Returns the [`CookieFault`] that says why there is no usable cookie.
|
|
80
|
+
pub fn read(pairs: &[String]) -> Result<SigninCookie, CookieFault> {
|
|
81
|
+
let mut values = pairs
|
|
82
|
+
.iter()
|
|
83
|
+
.filter_map(|pair| pair.split_once('='))
|
|
84
|
+
.filter(|(name, _)| name.trim() == NAME)
|
|
85
|
+
.map(|(_, value)| value.trim());
|
|
86
|
+
let value = values.next().ok_or(CookieFault::Absent)?;
|
|
87
|
+
if values.next().is_some() {
|
|
88
|
+
return Err(CookieFault::Repeated);
|
|
89
|
+
}
|
|
90
|
+
parse_value(value).ok_or(CookieFault::Malformed)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fn parse_value(value: &str) -> Option<SigninCookie> {
|
|
94
|
+
let (nonce, state) = value.split_once('.')?;
|
|
95
|
+
let acceptable = |part: &str| {
|
|
96
|
+
(MIN_VALUE_LENGTH..=MAX_VALUE_LENGTH).contains(&part.len()) && part.bytes().all(is_url_safe)
|
|
97
|
+
};
|
|
98
|
+
(acceptable(nonce) && acceptable(state)).then(|| SigninCookie {
|
|
99
|
+
nonce: nonce.into(),
|
|
100
|
+
state: state.into(),
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fn is_url_safe(byte: u8) -> bool {
|
|
105
|
+
byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[cfg(test)]
|
|
109
|
+
mod tests {
|
|
110
|
+
use super::*;
|
|
111
|
+
use serde_json::json;
|
|
112
|
+
|
|
113
|
+
const NONCE: &str = "Nn0nceNn0nceNn0nceAA";
|
|
114
|
+
const STATE: &str = "StateStateState0123456789";
|
|
115
|
+
|
|
116
|
+
fn cookie() -> SigninCookie {
|
|
117
|
+
SigninCookie {
|
|
118
|
+
nonce: NONCE.into(),
|
|
119
|
+
state: STATE.into(),
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fn pairs(items: &[&str]) -> Vec<String> {
|
|
124
|
+
items.iter().map(|s| s.to_string()).collect()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#[test]
|
|
128
|
+
fn the_set_header_matches_the_issuers_reference_exactly() {
|
|
129
|
+
assert_eq!(
|
|
130
|
+
set_header(&cookie()),
|
|
131
|
+
format!("__Host-signin={NONCE}.{STATE}; Max-Age=600; Path=/; Secure; HttpOnly; SameSite=Lax")
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#[test]
|
|
136
|
+
fn the_clear_header_keeps_every_attribute_and_zeroes_the_age() {
|
|
137
|
+
assert_eq!(
|
|
138
|
+
clear_header(),
|
|
139
|
+
"__Host-signin=; Max-Age=0; Path=/; Secure; HttpOnly; SameSite=Lax"
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#[test]
|
|
144
|
+
fn neither_header_carries_a_domain_attribute() {
|
|
145
|
+
assert!(!set_header(&cookie())
|
|
146
|
+
.to_ascii_lowercase()
|
|
147
|
+
.contains("domain"));
|
|
148
|
+
assert!(!clear_header().to_ascii_lowercase().contains("domain"));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#[test]
|
|
152
|
+
fn a_pair_reads_back_into_its_nonce_and_state() {
|
|
153
|
+
let read = read(&pairs(&[&format!("__Host-signin={NONCE}.{STATE}")])).unwrap();
|
|
154
|
+
assert_eq!(read, cookie());
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[test]
|
|
158
|
+
fn other_cookies_are_ignored_and_a_lookalike_name_is_not_the_cookie() {
|
|
159
|
+
let found = read(&pairs(&[
|
|
160
|
+
"a=b",
|
|
161
|
+
&format!("__Host-signin={NONCE}.{STATE}"),
|
|
162
|
+
"c=d",
|
|
163
|
+
]))
|
|
164
|
+
.unwrap();
|
|
165
|
+
assert_eq!(found, cookie());
|
|
166
|
+
assert_eq!(
|
|
167
|
+
read(&pairs(&[
|
|
168
|
+
&format!("signin={NONCE}.{STATE}"),
|
|
169
|
+
&format!("x__Host-signin={NONCE}.{STATE}")
|
|
170
|
+
])),
|
|
171
|
+
Err(CookieFault::Absent)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[test]
|
|
176
|
+
fn no_cookie_is_absent() {
|
|
177
|
+
assert_eq!(read(&[]), Err(CookieFault::Absent));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#[test]
|
|
181
|
+
fn a_repeated_cookie_is_refused_even_when_the_values_agree() {
|
|
182
|
+
let one = format!("__Host-signin={NONCE}.{STATE}");
|
|
183
|
+
assert_eq!(read(&pairs(&[&one, &one])), Err(CookieFault::Repeated));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#[test]
|
|
187
|
+
fn a_value_of_the_wrong_shape_is_malformed() {
|
|
188
|
+
for bad in [
|
|
189
|
+
"",
|
|
190
|
+
NONCE,
|
|
191
|
+
&format!("{NONCE}."),
|
|
192
|
+
&format!(".{STATE}"),
|
|
193
|
+
&format!("{NONCE}.{STATE}.extra"),
|
|
194
|
+
&format!("short.{STATE}"),
|
|
195
|
+
&format!("{NONCE}.short"),
|
|
196
|
+
&format!("{NONCE}.{}", "s".repeat(129)),
|
|
197
|
+
&format!("{NONCE}.{STATE}+"),
|
|
198
|
+
&format!("{NONCE}.{STATE}="),
|
|
199
|
+
&format!("{NONCE}.sta te0123456789012"),
|
|
200
|
+
] {
|
|
201
|
+
assert_eq!(
|
|
202
|
+
read(&pairs(&[&format!("__Host-signin={bad}")])),
|
|
203
|
+
Err(CookieFault::Malformed),
|
|
204
|
+
"{bad:?}"
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#[test]
|
|
210
|
+
fn the_longest_acceptable_values_pass() {
|
|
211
|
+
let long = "a".repeat(128);
|
|
212
|
+
let found = read(&pairs(&[&format!("__Host-signin={long}.{long}")])).unwrap();
|
|
213
|
+
assert_eq!(found.nonce.len(), 128);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#[test]
|
|
217
|
+
fn pairs_come_from_the_cookie_header_and_from_the_cookies_array() {
|
|
218
|
+
let event = json!({"headers": {"cookie": "a=b; __Host-signin=x.y ; c=d"}, "cookies": ["e=f", "g=h"]});
|
|
219
|
+
let all = request_pairs(&event);
|
|
220
|
+
for expected in ["a=b", "__Host-signin=x.y", "c=d", "e=f", "g=h"] {
|
|
221
|
+
assert!(all.iter().any(|p| p == expected), "{expected} in {all:?}");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#[test]
|
|
226
|
+
fn a_cookie_named_in_both_places_counts_twice() {
|
|
227
|
+
let one = format!("__Host-signin={NONCE}.{STATE}");
|
|
228
|
+
let event = json!({"headers": {"cookie": one}, "cookies": [one]});
|
|
229
|
+
assert_eq!(read(&request_pairs(&event)), Err(CookieFault::Repeated));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#[test]
|
|
233
|
+
fn an_event_without_cookies_yields_no_pairs() {
|
|
234
|
+
assert!(request_pairs(&json!({})).is_empty());
|
|
235
|
+
assert!(request_pairs(&json!({"headers": {"cookie": 5}, "cookies": "x"})).is_empty());
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
//! The host extension that puts the sign-in flow on the site's routes.
|
|
2
|
+
//!
|
|
3
|
+
//! It is deliberately small and generically named so it can move to the platform later: it knows
|
|
4
|
+
//! the routes, the mode and nothing about any one site. With `PLATFORM_SIGNIN` unset it is never
|
|
5
|
+
//! installed, so the host answers exactly as before.
|
|
6
|
+
//!
|
|
7
|
+
//! ## One installed instance per process
|
|
8
|
+
//!
|
|
9
|
+
//! The single-use record of token identifiers lives in the verifier this extension owns, and the
|
|
10
|
+
//! host installs one extension for the life of the process, so the record is process-wide. It does
|
|
11
|
+
//! not survive a restart (the replay window that opens is at most the token lifetime plus the
|
|
12
|
+
//! skew, 90 seconds) and it is not shared between instances, which is why the settings refuse any
|
|
13
|
+
//! process that is not the single long-lived server.
|
|
14
|
+
|
|
15
|
+
use std::collections::HashMap;
|
|
16
|
+
use std::sync::Arc;
|
|
17
|
+
|
|
18
|
+
use async_trait::async_trait;
|
|
19
|
+
use serde_json::Value;
|
|
20
|
+
|
|
21
|
+
use super::fetcher::HttpJwksFetcher;
|
|
22
|
+
use super::handler::{CallbackRequest, SigninHandler, StdoutLog};
|
|
23
|
+
use super::jti::JtiStore;
|
|
24
|
+
use super::jwks::JwksCache;
|
|
25
|
+
use super::pages::{self, Failure};
|
|
26
|
+
use super::session::{HostMembers, MemberDirectory};
|
|
27
|
+
use super::settings::{Mode, SettingsError, SigninSettings};
|
|
28
|
+
use super::verify::Verifier;
|
|
29
|
+
use super::{cookie, query};
|
|
30
|
+
use crate::extension::{Ctx, HostExtension, RateRule};
|
|
31
|
+
|
|
32
|
+
/// Starts the flow in either mode.
|
|
33
|
+
pub const START_PATH: &str = "/auth/platform";
|
|
34
|
+
/// Finishes the flow in either mode.
|
|
35
|
+
pub const CALLBACK_PATH: &str = "/auth/platform/callback";
|
|
36
|
+
/// The site's Google sign-in path, which `default` mode takes over.
|
|
37
|
+
pub const GOOGLE_PATH: &str = "/auth/google";
|
|
38
|
+
|
|
39
|
+
/// Which half of the flow a request is for.
|
|
40
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
41
|
+
pub enum Route {
|
|
42
|
+
Start,
|
|
43
|
+
Callback,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// The platform sign-in extension.
|
|
47
|
+
pub struct PlatformSignin {
|
|
48
|
+
handler: SigninHandler,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
impl PlatformSignin {
|
|
52
|
+
/// Wraps a handler that is already built.
|
|
53
|
+
pub fn new(handler: SigninHandler) -> Self {
|
|
54
|
+
Self { handler }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Builds the extension from an explicit map, or `None` when `PLATFORM_SIGNIN` is unset.
|
|
58
|
+
///
|
|
59
|
+
/// # Errors
|
|
60
|
+
///
|
|
61
|
+
/// Returns the setting that is missing or malformed once the switch is on.
|
|
62
|
+
pub fn from_env_map(vars: &HashMap<String, String>) -> Result<Option<Self>, SettingsError> {
|
|
63
|
+
let Some(settings) = SigninSettings::from_env_map(vars)? else {
|
|
64
|
+
return Ok(None);
|
|
65
|
+
};
|
|
66
|
+
let keys = JwksCache::new(HttpJwksFetcher::new(&settings.config.jwks_url));
|
|
67
|
+
let verifier = Verifier::new(settings.config.clone(), keys, JtiStore::new());
|
|
68
|
+
Ok(Some(Self::new(SigninHandler::new(
|
|
69
|
+
settings,
|
|
70
|
+
Arc::new(verifier),
|
|
71
|
+
Arc::new(StdoutLog),
|
|
72
|
+
))))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Decides which half of the flow, if either, a request is for.
|
|
76
|
+
pub fn classify(&self, event: &Value) -> Option<Route> {
|
|
77
|
+
let method = event
|
|
78
|
+
.pointer("/requestContext/http/method")
|
|
79
|
+
.and_then(Value::as_str)?;
|
|
80
|
+
let path = event.get("rawPath").and_then(Value::as_str)?;
|
|
81
|
+
if method != "GET" {
|
|
82
|
+
return None;
|
|
83
|
+
}
|
|
84
|
+
match path {
|
|
85
|
+
START_PATH => Some(Route::Start),
|
|
86
|
+
CALLBACK_PATH => Some(Route::Callback),
|
|
87
|
+
GOOGLE_PATH if self.takes_over_google(raw_query(event)) => Some(Route::Start),
|
|
88
|
+
_ => None,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fn takes_over_google(&self, raw_query: &str) -> bool {
|
|
93
|
+
self.handler.settings().mode == Mode::Default
|
|
94
|
+
&& !query::has_value(raw_query, "via", "google")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Finishes a sign-in for a callback request against a member store.
|
|
98
|
+
pub async fn finish(&self, request: &CallbackRequest, members: &dyn MemberDirectory) -> Value {
|
|
99
|
+
self.handler.callback(request, members).await
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Answers a request that belongs to the flow, or `None` for everything else.
|
|
103
|
+
pub async fn route_event(&self, event: &Value, members: &dyn MemberDirectory) -> Option<Value> {
|
|
104
|
+
match self.classify(event)? {
|
|
105
|
+
Route::Start => Some(self.handler.start()),
|
|
106
|
+
Route::Callback => Some(self.finish(&callback_request(event), members).await),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fn raw_query(event: &Value) -> &str {
|
|
112
|
+
event
|
|
113
|
+
.get("rawQueryString")
|
|
114
|
+
.and_then(Value::as_str)
|
|
115
|
+
.unwrap_or("")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fn callback_request(event: &Value) -> CallbackRequest {
|
|
119
|
+
CallbackRequest {
|
|
120
|
+
raw_query: raw_query(event).to_string(),
|
|
121
|
+
cookie_pairs: cookie::request_pairs(event),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#[async_trait]
|
|
126
|
+
impl HostExtension for PlatformSignin {
|
|
127
|
+
async fn guest_route(&self, ctx: &Ctx<'_>) -> Option<Value> {
|
|
128
|
+
let event = ctx.event?;
|
|
129
|
+
match self.classify(event)? {
|
|
130
|
+
Route::Start => Some(self.handler.start()),
|
|
131
|
+
Route::Callback => Some(self.finish_for_host(ctx, event).await),
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fn rate_rules(&self) -> Vec<RateRule> {
|
|
136
|
+
vec![
|
|
137
|
+
RateRule {
|
|
138
|
+
name: "platform_signin_start",
|
|
139
|
+
method: "GET",
|
|
140
|
+
path: START_PATH,
|
|
141
|
+
limit_env: "HECKS_RATE_LIMIT_PLATFORM_SIGNIN_START",
|
|
142
|
+
default_limit: 30,
|
|
143
|
+
},
|
|
144
|
+
RateRule {
|
|
145
|
+
name: "platform_signin_callback",
|
|
146
|
+
method: "GET",
|
|
147
|
+
path: CALLBACK_PATH,
|
|
148
|
+
limit_env: "HECKS_RATE_LIMIT_PLATFORM_SIGNIN_CALLBACK",
|
|
149
|
+
default_limit: 30,
|
|
150
|
+
},
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
impl PlatformSignin {
|
|
156
|
+
/// Finishes a callback against the host's own member store.
|
|
157
|
+
async fn finish_for_host(&self, ctx: &Ctx<'_>, event: &Value) -> Value {
|
|
158
|
+
let Some(domain_ir) = ctx.domain_ir else {
|
|
159
|
+
return pages::refusal(Failure::Unavailable);
|
|
160
|
+
};
|
|
161
|
+
let members = HostMembers {
|
|
162
|
+
client: ctx.client,
|
|
163
|
+
domain_ir,
|
|
164
|
+
wasm_path: ctx.wasm_path,
|
|
165
|
+
config: ctx.config,
|
|
166
|
+
invoker: ctx.invoker,
|
|
167
|
+
};
|
|
168
|
+
self.finish(&callback_request(event), &members).await
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/// Refuses the boot because the sign-in settings were switched on and are wrong.
|
|
173
|
+
pub struct RefusedSettings(pub SettingsError);
|
|
174
|
+
|
|
175
|
+
#[async_trait]
|
|
176
|
+
impl HostExtension for RefusedSettings {
|
|
177
|
+
fn boot_check(&self, _domain: &str) -> Result<(), String> {
|
|
178
|
+
Err(format!("platform sign-in: {}", self.0))
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/// The extensions that platform sign-in contributes for these settings: none when it is off, the
|
|
183
|
+
/// flow when it is on, and a boot refusal when the settings are wrong.
|
|
184
|
+
pub fn extensions_for(vars: &HashMap<String, String>) -> Vec<Arc<dyn HostExtension>> {
|
|
185
|
+
match PlatformSignin::from_env_map(vars) {
|
|
186
|
+
Ok(None) => Vec::new(),
|
|
187
|
+
Ok(Some(extension)) => vec![Arc::new(extension)],
|
|
188
|
+
Err(error) => vec![Arc::new(RefusedSettings(error))],
|
|
189
|
+
}
|
|
190
|
+
}
|