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,233 @@
|
|
|
1
|
+
//! The callback's query string, read strictly.
|
|
2
|
+
//!
|
|
3
|
+
//! The host's general query parser keeps the last of a repeated key. For a sign-in callback that
|
|
4
|
+
//! would let two parsers disagree about which `token` or `state` counts, so this one keeps every
|
|
5
|
+
//! value and refuses a repeat. Everything except `token` and `state` is ignored: nothing in the
|
|
6
|
+
//! query can steer where the browser goes next.
|
|
7
|
+
|
|
8
|
+
/// The longest query string read, in bytes. It only bounds the work done on hostile input; the
|
|
9
|
+
/// verifier caps the token itself.
|
|
10
|
+
pub const MAX_QUERY_BYTES: usize = 16 * 1024;
|
|
11
|
+
|
|
12
|
+
/// The two values a callback carries.
|
|
13
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
14
|
+
pub struct CallbackParams {
|
|
15
|
+
pub token: String,
|
|
16
|
+
pub state: String,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// Why the query could not be used.
|
|
20
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
21
|
+
pub enum ParamFault {
|
|
22
|
+
/// The query is longer than [`MAX_QUERY_BYTES`].
|
|
23
|
+
Oversized,
|
|
24
|
+
/// A percent escape is malformed or the decoded text is not UTF-8.
|
|
25
|
+
BadEncoding,
|
|
26
|
+
/// A required parameter was not given.
|
|
27
|
+
Missing,
|
|
28
|
+
/// A required parameter was given more than once.
|
|
29
|
+
Repeated,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Reads `token` and `state` from a raw query string.
|
|
33
|
+
///
|
|
34
|
+
/// # Errors
|
|
35
|
+
///
|
|
36
|
+
/// Returns the [`ParamFault`] that says why the pair is not usable.
|
|
37
|
+
pub fn parse_callback(raw_query: &str) -> Result<CallbackParams, ParamFault> {
|
|
38
|
+
let pairs = decode_pairs(raw_query)?;
|
|
39
|
+
Ok(CallbackParams {
|
|
40
|
+
token: single(&pairs, "token")?,
|
|
41
|
+
state: single(&pairs, "state")?,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn single(pairs: &[(String, String)], name: &str) -> Result<String, ParamFault> {
|
|
46
|
+
let mut found = pairs
|
|
47
|
+
.iter()
|
|
48
|
+
.filter(|(key, _)| key == name)
|
|
49
|
+
.map(|(_, value)| value);
|
|
50
|
+
let value = found.next().ok_or(ParamFault::Missing)?;
|
|
51
|
+
if found.next().is_some() {
|
|
52
|
+
return Err(ParamFault::Repeated);
|
|
53
|
+
}
|
|
54
|
+
Ok(value.clone())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn decode_pairs(raw_query: &str) -> Result<Vec<(String, String)>, ParamFault> {
|
|
58
|
+
if raw_query.len() > MAX_QUERY_BYTES {
|
|
59
|
+
return Err(ParamFault::Oversized);
|
|
60
|
+
}
|
|
61
|
+
raw_query
|
|
62
|
+
.split('&')
|
|
63
|
+
.filter(|pair| !pair.is_empty())
|
|
64
|
+
.map(|pair| {
|
|
65
|
+
let (name, value) = pair.split_once('=').unwrap_or((pair, ""));
|
|
66
|
+
Ok((decode(name)?, decode(value)?))
|
|
67
|
+
})
|
|
68
|
+
.collect()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Decodes percent escapes; a plus stays a plus, as it does in the host's other query reading.
|
|
72
|
+
fn decode(text: &str) -> Result<String, ParamFault> {
|
|
73
|
+
let bytes = text.as_bytes();
|
|
74
|
+
let mut out = Vec::with_capacity(bytes.len());
|
|
75
|
+
let mut i = 0;
|
|
76
|
+
while i < bytes.len() {
|
|
77
|
+
if bytes[i] == b'%' {
|
|
78
|
+
let pair = bytes.get(i + 1..i + 3).ok_or(ParamFault::BadEncoding)?;
|
|
79
|
+
out.push(hex_byte(pair).ok_or(ParamFault::BadEncoding)?);
|
|
80
|
+
i += 3;
|
|
81
|
+
} else {
|
|
82
|
+
out.push(bytes[i]);
|
|
83
|
+
i += 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
String::from_utf8(out).map_err(|_| ParamFault::BadEncoding)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fn hex_byte(pair: &[u8]) -> Option<u8> {
|
|
90
|
+
let digit = |b: u8| (b as char).to_digit(16);
|
|
91
|
+
Some((digit(pair[0])? * 16 + digit(pair[1])?) as u8)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Tells whether the query carries `name` with exactly `value` at least once.
|
|
95
|
+
///
|
|
96
|
+
/// A malformed query carries nothing.
|
|
97
|
+
pub fn has_value(raw_query: &str, name: &str, value: &str) -> bool {
|
|
98
|
+
decode_pairs(raw_query).is_ok_and(|pairs| pairs.iter().any(|(k, v)| k == name && v == value))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#[cfg(test)]
|
|
102
|
+
mod tests {
|
|
103
|
+
use super::*;
|
|
104
|
+
|
|
105
|
+
#[test]
|
|
106
|
+
fn token_and_state_are_read_and_the_rest_is_ignored() {
|
|
107
|
+
let params =
|
|
108
|
+
parse_callback("token=a.b.c&state=S1&return=https%3A%2F%2Fevil.example&next=/x")
|
|
109
|
+
.unwrap();
|
|
110
|
+
assert_eq!(
|
|
111
|
+
params,
|
|
112
|
+
CallbackParams {
|
|
113
|
+
token: "a.b.c".into(),
|
|
114
|
+
state: "S1".into()
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#[test]
|
|
120
|
+
fn order_and_empty_pairs_do_not_matter() {
|
|
121
|
+
let params = parse_callback("&state=S1&&token=T&").unwrap();
|
|
122
|
+
assert_eq!((params.token.as_str(), params.state.as_str()), ("T", "S1"));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#[test]
|
|
126
|
+
fn percent_escapes_decode_and_a_plus_stays_a_plus() {
|
|
127
|
+
let params = parse_callback("token=a%2Bb+c&state=%53%31").unwrap();
|
|
128
|
+
assert_eq!(params.token, "a+b+c");
|
|
129
|
+
assert_eq!(params.state, "S1");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#[test]
|
|
133
|
+
fn a_missing_parameter_is_a_fault_whichever_one() {
|
|
134
|
+
assert_eq!(parse_callback("state=S1"), Err(ParamFault::Missing));
|
|
135
|
+
assert_eq!(parse_callback("token=T"), Err(ParamFault::Missing));
|
|
136
|
+
assert_eq!(parse_callback(""), Err(ParamFault::Missing));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[test]
|
|
140
|
+
fn an_empty_value_is_present_and_left_to_the_verifier() {
|
|
141
|
+
let params = parse_callback("token=&state=").unwrap();
|
|
142
|
+
assert_eq!((params.token.as_str(), params.state.as_str()), ("", ""));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#[test]
|
|
146
|
+
fn a_repeated_parameter_is_refused_even_when_the_values_agree() {
|
|
147
|
+
assert_eq!(
|
|
148
|
+
parse_callback("token=T&token=T&state=S"),
|
|
149
|
+
Err(ParamFault::Repeated)
|
|
150
|
+
);
|
|
151
|
+
assert_eq!(
|
|
152
|
+
parse_callback("token=T&state=S&state=S"),
|
|
153
|
+
Err(ParamFault::Repeated)
|
|
154
|
+
);
|
|
155
|
+
assert_eq!(
|
|
156
|
+
parse_callback("token=T&state=S&token=U"),
|
|
157
|
+
Err(ParamFault::Repeated)
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#[test]
|
|
162
|
+
fn array_style_names_are_different_names() {
|
|
163
|
+
assert_eq!(
|
|
164
|
+
parse_callback("token[]=T&state=S"),
|
|
165
|
+
Err(ParamFault::Missing)
|
|
166
|
+
);
|
|
167
|
+
let params = parse_callback("token=T&state=S&token[]=U").unwrap();
|
|
168
|
+
assert_eq!(params.token, "T");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#[test]
|
|
172
|
+
fn a_percent_encoded_name_counts_as_the_name() {
|
|
173
|
+
assert_eq!(
|
|
174
|
+
parse_callback("tok%65n=T&token=U&state=S"),
|
|
175
|
+
Err(ParamFault::Repeated)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#[test]
|
|
180
|
+
fn an_oversized_query_is_refused_before_it_is_read() {
|
|
181
|
+
let big = format!("token={}&state=S", "a".repeat(MAX_QUERY_BYTES));
|
|
182
|
+
assert_eq!(parse_callback(&big), Err(ParamFault::Oversized));
|
|
183
|
+
let exact = format!(
|
|
184
|
+
"token={}&state=S",
|
|
185
|
+
"a".repeat(MAX_QUERY_BYTES - "token=&state=S".len())
|
|
186
|
+
);
|
|
187
|
+
assert_eq!(exact.len(), MAX_QUERY_BYTES);
|
|
188
|
+
assert!(parse_callback(&exact).is_ok());
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#[test]
|
|
192
|
+
fn a_bad_percent_escape_or_invalid_utf8_is_a_fault() {
|
|
193
|
+
for bad in [
|
|
194
|
+
"token=%&state=S",
|
|
195
|
+
"token=%G1&state=S",
|
|
196
|
+
"token=%4&state=S",
|
|
197
|
+
"token=%ff%fe&state=S",
|
|
198
|
+
"token=T&state=%c3",
|
|
199
|
+
] {
|
|
200
|
+
assert_eq!(parse_callback(bad), Err(ParamFault::BadEncoding), "{bad}");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#[test]
|
|
205
|
+
fn a_name_without_an_equals_sign_is_a_name_with_an_empty_value() {
|
|
206
|
+
assert_eq!(parse_callback("token&state=S").unwrap().token, "");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#[test]
|
|
210
|
+
fn has_value_finds_an_exact_pair_anywhere() {
|
|
211
|
+
assert!(has_value("via=google", "via", "google"));
|
|
212
|
+
assert!(has_value("a=1&via=google&b=2", "via", "google"));
|
|
213
|
+
assert!(has_value("via=x&via=google", "via", "google"));
|
|
214
|
+
assert!(has_value("v%69a=goo%67le", "via", "google"));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#[test]
|
|
218
|
+
fn has_value_is_exact_and_tolerates_junk() {
|
|
219
|
+
for query in [
|
|
220
|
+
"",
|
|
221
|
+
"via=",
|
|
222
|
+
"via=Google",
|
|
223
|
+
"via=google2",
|
|
224
|
+
"via=googl",
|
|
225
|
+
"xvia=google",
|
|
226
|
+
"via",
|
|
227
|
+
"via=%",
|
|
228
|
+
"a=via=google",
|
|
229
|
+
] {
|
|
230
|
+
assert!(!has_value(query, "via", "google"), "{query:?}");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
//! Turning a verified token into a signed-in member of this site, or a refusal.
|
|
2
|
+
//!
|
|
3
|
+
//! The token proves who the issuer says the person is; it never creates a member. The mapping is
|
|
4
|
+
//! the one the site's Google sign-in already uses:
|
|
5
|
+
//!
|
|
6
|
+
//! 1. the token's `(idp_iss, idp_sub)` resolves to an identity that a member is linked to;
|
|
7
|
+
//! 2. otherwise the person's email is matched to an existing, enabled member who has a role and
|
|
8
|
+
//! no identity yet, and that member is linked (the one write this flow can cause);
|
|
9
|
+
//! 3. otherwise the sign-in is refused. There is no fallback by email alone: the identity pin is
|
|
10
|
+
//! what protects against a recycled or reassigned address.
|
|
11
|
+
//!
|
|
12
|
+
//! A member without a role is refused as well. The token's role is compared with the member's
|
|
13
|
+
//! local role and a difference is logged, never written.
|
|
14
|
+
|
|
15
|
+
use std::path::Path;
|
|
16
|
+
|
|
17
|
+
use async_trait::async_trait;
|
|
18
|
+
use serde_json::Value;
|
|
19
|
+
use tokio::sync::Mutex;
|
|
20
|
+
use tokio_postgres::Client;
|
|
21
|
+
|
|
22
|
+
use super::settings::SigninSettings;
|
|
23
|
+
use super::verify::VerifiedClaims;
|
|
24
|
+
use super::{cookie, pages};
|
|
25
|
+
use crate::auth::{self, Session};
|
|
26
|
+
use crate::journal::LineageConfig;
|
|
27
|
+
use crate::lambda_client::LambdaInvoker;
|
|
28
|
+
|
|
29
|
+
/// Where the account lands after a successful sign-in.
|
|
30
|
+
pub const LANDING_PATH: &str = "/admin.html";
|
|
31
|
+
|
|
32
|
+
/// What the flow needs to know about this site's members.
|
|
33
|
+
#[async_trait]
|
|
34
|
+
pub trait MemberDirectory: Send + Sync {
|
|
35
|
+
/// Finds the identity registered for a provider's `(issuer, subject)`.
|
|
36
|
+
async fn identity_for(&self, issuer: &str, subject: &str) -> anyhow::Result<Option<String>>;
|
|
37
|
+
|
|
38
|
+
/// Finds the enabled member linked to an identity.
|
|
39
|
+
async fn session_for_identity(&self, identity_id: &str) -> anyhow::Result<Option<Session>>;
|
|
40
|
+
|
|
41
|
+
/// Links an existing, enabled member with a role and no identity, found by email.
|
|
42
|
+
async fn provision(
|
|
43
|
+
&self,
|
|
44
|
+
email: &str,
|
|
45
|
+
issuer: &str,
|
|
46
|
+
subject: &str,
|
|
47
|
+
) -> anyhow::Result<Option<Session>>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Maps a verified token to the member it may sign in, or `None` to refuse.
|
|
51
|
+
///
|
|
52
|
+
/// # Errors
|
|
53
|
+
///
|
|
54
|
+
/// Returns the member store's error; the caller treats it as this side's fault, not a refusal.
|
|
55
|
+
pub async fn map_member(
|
|
56
|
+
directory: &dyn MemberDirectory,
|
|
57
|
+
claims: &VerifiedClaims,
|
|
58
|
+
) -> anyhow::Result<Option<Session>> {
|
|
59
|
+
let (Some(issuer), Some(subject)) =
|
|
60
|
+
(claims.idp_issuer.as_deref(), claims.idp_subject.as_deref())
|
|
61
|
+
else {
|
|
62
|
+
return Ok(None);
|
|
63
|
+
};
|
|
64
|
+
let by_identity = match directory.identity_for(issuer, subject).await? {
|
|
65
|
+
Some(identity_id) => directory.session_for_identity(&identity_id).await?,
|
|
66
|
+
None => None,
|
|
67
|
+
};
|
|
68
|
+
let session = match by_identity {
|
|
69
|
+
Some(session) => Some(session),
|
|
70
|
+
None => directory.provision(&claims.email, issuer, subject).await?,
|
|
71
|
+
};
|
|
72
|
+
Ok(session.filter(has_role))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn has_role(session: &Session) -> bool {
|
|
76
|
+
session.role.as_deref().is_some_and(|role| !role.is_empty())
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// A difference between the role the issuer sent and the member's local role.
|
|
80
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
81
|
+
pub struct RoleDrift {
|
|
82
|
+
pub token_role: Option<String>,
|
|
83
|
+
pub local_role: Option<String>,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Compares the token's role with the member's local one.
|
|
87
|
+
pub fn role_drift(claims: &VerifiedClaims, session: &Session) -> Option<RoleDrift> {
|
|
88
|
+
let token_role = claims.roles.first().cloned();
|
|
89
|
+
let local_role = session.role.clone();
|
|
90
|
+
(token_role != local_role).then_some(RoleDrift {
|
|
91
|
+
token_role,
|
|
92
|
+
local_role,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Builds the signed-in response: the account cookie, the cleared sign-in cookie, and a redirect
|
|
97
|
+
/// to the landing page.
|
|
98
|
+
pub fn signed_in(settings: &SigninSettings, session: &Session) -> Value {
|
|
99
|
+
let token = auth::account_token(
|
|
100
|
+
&settings.session_secret,
|
|
101
|
+
&session.email,
|
|
102
|
+
auth::SESSION_TTL_SECS,
|
|
103
|
+
);
|
|
104
|
+
// The site address decides Secure: the redirect URI of a Google client may be absent.
|
|
105
|
+
let flags = crate::web::account_cookie_flags("", &settings.site_url);
|
|
106
|
+
let account = format!(
|
|
107
|
+
"{}={token}{flags}; Max-Age={}",
|
|
108
|
+
settings.account_cookie,
|
|
109
|
+
auth::SESSION_TTL_SECS
|
|
110
|
+
);
|
|
111
|
+
pages::redirect(LANDING_PATH, &[account, cookie::clear_header()])
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// The host's own member store, reached through the same functions the Google sign-in uses.
|
|
115
|
+
pub struct HostMembers<'a> {
|
|
116
|
+
pub client: &'a Mutex<Client>,
|
|
117
|
+
pub domain_ir: &'a Value,
|
|
118
|
+
pub wasm_path: &'a Path,
|
|
119
|
+
pub config: &'a LineageConfig,
|
|
120
|
+
pub invoker: &'a dyn LambdaInvoker,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#[async_trait]
|
|
124
|
+
impl MemberDirectory for HostMembers<'_> {
|
|
125
|
+
async fn identity_for(&self, issuer: &str, subject: &str) -> anyhow::Result<Option<String>> {
|
|
126
|
+
auth::resolve_identity_from_head(self.client, self.domain_ir, issuer, subject).await
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async fn session_for_identity(&self, identity_id: &str) -> anyhow::Result<Option<Session>> {
|
|
130
|
+
auth::session_for_member_by_identity(self.client, self.domain_ir, identity_id).await
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async fn provision(
|
|
134
|
+
&self,
|
|
135
|
+
email: &str,
|
|
136
|
+
issuer: &str,
|
|
137
|
+
subject: &str,
|
|
138
|
+
) -> anyhow::Result<Option<Session>> {
|
|
139
|
+
auth::provision(
|
|
140
|
+
self.client,
|
|
141
|
+
self.wasm_path,
|
|
142
|
+
self.config,
|
|
143
|
+
self.domain_ir,
|
|
144
|
+
email,
|
|
145
|
+
issuer,
|
|
146
|
+
subject,
|
|
147
|
+
self.invoker,
|
|
148
|
+
)
|
|
149
|
+
.await
|
|
150
|
+
}
|
|
151
|
+
}
|