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,196 @@
|
|
|
1
|
+
//! The single-use record of token identifiers.
|
|
2
|
+
//!
|
|
3
|
+
//! A token may be redeemed once. The store remembers each `jti` until its time window has
|
|
4
|
+
//! closed (`exp` plus the skew), so a replay inside that window is refused and
|
|
5
|
+
//! nothing is kept longer than it must be.
|
|
6
|
+
//!
|
|
7
|
+
//! ## Per process, on purpose
|
|
8
|
+
//!
|
|
9
|
+
//! The record lives in this process's memory. A restart forgets it, which leaves a replay window of
|
|
10
|
+
//! at most the token's remaining life plus the skew (90 seconds with the default settings). More
|
|
11
|
+
//! than one running instance would each have their own record, so a token could be redeemed once
|
|
12
|
+
//! per instance. This store is therefore only right for a site served by a single long-running
|
|
13
|
+
//! process; a deployment with several instances or short-lived invocations needs a shared record.
|
|
14
|
+
//!
|
|
15
|
+
//! ## Bounded, and closed when full
|
|
16
|
+
//!
|
|
17
|
+
//! At the cap the store refuses new claims rather than grow, so a flood of valid-looking tokens
|
|
18
|
+
//! cannot exhaust memory. A refusal is reported like a replay: nobody gets in on a full store.
|
|
19
|
+
|
|
20
|
+
use std::collections::HashMap;
|
|
21
|
+
use std::sync::Mutex;
|
|
22
|
+
|
|
23
|
+
/// The default cap on remembered identifiers.
|
|
24
|
+
pub const DEFAULT_MAX_ENTRIES: usize = 100_000;
|
|
25
|
+
|
|
26
|
+
/// An in-memory, bounded, atomic set of spent token identifiers.
|
|
27
|
+
pub struct JtiStore {
|
|
28
|
+
max_entries: usize,
|
|
29
|
+
seen: Mutex<HashMap<String, i64>>,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl JtiStore {
|
|
33
|
+
/// Builds an empty store with the default cap.
|
|
34
|
+
pub fn new() -> Self {
|
|
35
|
+
Self::with_capacity(DEFAULT_MAX_ENTRIES)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Builds an empty store that remembers at most `max_entries` identifiers.
|
|
39
|
+
pub fn with_capacity(max_entries: usize) -> Self {
|
|
40
|
+
Self {
|
|
41
|
+
max_entries,
|
|
42
|
+
seen: Mutex::new(HashMap::new()),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Records `jti` as spent until `expires_at` (Unix seconds) unless it is already remembered.
|
|
47
|
+
///
|
|
48
|
+
/// Returns `true` the first time, and `false` while the identifier is remembered or the store
|
|
49
|
+
/// is full. Checking and recording are one step under one lock, so of any number of concurrent
|
|
50
|
+
/// claims of the same identifier exactly one wins.
|
|
51
|
+
pub fn claim(&self, jti: &str, expires_at: i64, now: i64) -> bool {
|
|
52
|
+
let mut seen = self.lock();
|
|
53
|
+
seen.retain(|_, expiry| *expiry > now);
|
|
54
|
+
if seen.contains_key(jti) || seen.len() >= self.max_entries {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
seen.insert(jti.to_string(), expires_at);
|
|
58
|
+
true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, i64>> {
|
|
62
|
+
// A panic in another thread cannot leave the map half-updated, and refusing everything
|
|
63
|
+
// forever would be a lock-out, so a poisoned lock is simply taken over.
|
|
64
|
+
self.seen
|
|
65
|
+
.lock()
|
|
66
|
+
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Counts the identifiers still remembered at `now`; expired ones are not counted.
|
|
70
|
+
pub fn len(&self, now: i64) -> usize {
|
|
71
|
+
self.lock().values().filter(|expiry| **expiry > now).count()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Tells whether nothing is remembered at `now`.
|
|
75
|
+
pub fn is_empty(&self, now: i64) -> bool {
|
|
76
|
+
self.len(now) == 0
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
impl Default for JtiStore {
|
|
81
|
+
fn default() -> Self {
|
|
82
|
+
Self::new()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[cfg(test)]
|
|
87
|
+
mod tests {
|
|
88
|
+
use super::*;
|
|
89
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
90
|
+
use std::sync::{Arc, Barrier};
|
|
91
|
+
|
|
92
|
+
const NOW: i64 = 1_800_000_000;
|
|
93
|
+
|
|
94
|
+
#[test]
|
|
95
|
+
fn an_identifier_is_claimed_once() {
|
|
96
|
+
let store = JtiStore::new();
|
|
97
|
+
assert!(store.claim("a", NOW + 90, NOW));
|
|
98
|
+
assert!(!store.claim("a", NOW + 90, NOW));
|
|
99
|
+
assert!(store.claim("b", NOW + 90, NOW));
|
|
100
|
+
assert_eq!(store.len(NOW), 2);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#[test]
|
|
104
|
+
fn an_entry_is_remembered_until_its_expiry_and_forgotten_at_it() {
|
|
105
|
+
let store = JtiStore::new();
|
|
106
|
+
assert!(store.claim("a", NOW + 90, NOW));
|
|
107
|
+
assert!(!store.claim("a", NOW + 90, NOW + 89));
|
|
108
|
+
assert_eq!(store.len(NOW + 89), 1);
|
|
109
|
+
assert!(
|
|
110
|
+
store.claim("a", NOW + 200, NOW + 90),
|
|
111
|
+
"forgotten exactly at expires_at"
|
|
112
|
+
);
|
|
113
|
+
assert_eq!(store.len(NOW + 90), 1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[test]
|
|
117
|
+
fn expired_entries_are_not_counted_and_free_room() {
|
|
118
|
+
let store = JtiStore::with_capacity(2);
|
|
119
|
+
assert!(store.claim("a", NOW + 10, NOW));
|
|
120
|
+
assert!(store.claim("b", NOW + 10, NOW));
|
|
121
|
+
assert!(
|
|
122
|
+
!store.claim("c", NOW + 100, NOW + 5),
|
|
123
|
+
"full while both are live"
|
|
124
|
+
);
|
|
125
|
+
assert!(
|
|
126
|
+
store.claim("c", NOW + 100, NOW + 10),
|
|
127
|
+
"expired entries make room"
|
|
128
|
+
);
|
|
129
|
+
assert_eq!(store.len(NOW + 10), 1);
|
|
130
|
+
assert!(store.is_empty(NOW + 100));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[test]
|
|
134
|
+
fn the_store_is_bounded_and_fails_closed_at_the_cap() {
|
|
135
|
+
let store = JtiStore::with_capacity(3);
|
|
136
|
+
for id in ["a", "b", "c"] {
|
|
137
|
+
assert!(store.claim(id, NOW + 90, NOW));
|
|
138
|
+
}
|
|
139
|
+
assert!(
|
|
140
|
+
!store.claim("d", NOW + 90, NOW),
|
|
141
|
+
"a new identifier is refused, not stored"
|
|
142
|
+
);
|
|
143
|
+
assert_eq!(store.len(NOW), 3);
|
|
144
|
+
assert!(
|
|
145
|
+
!store.claim("a", NOW + 90, NOW),
|
|
146
|
+
"and an old one is still a replay"
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[test]
|
|
151
|
+
fn a_refused_claim_records_nothing() {
|
|
152
|
+
let store = JtiStore::with_capacity(1);
|
|
153
|
+
assert!(store.claim("a", NOW + 10, NOW));
|
|
154
|
+
assert!(!store.claim("b", NOW + 10, NOW));
|
|
155
|
+
assert!(
|
|
156
|
+
store.claim("b", NOW + 100, NOW + 10),
|
|
157
|
+
"b was never remembered"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#[test]
|
|
162
|
+
fn concurrent_claims_of_one_identifier_have_exactly_one_winner() {
|
|
163
|
+
const THREADS: usize = 16;
|
|
164
|
+
for round in 0..300 {
|
|
165
|
+
let store = Arc::new(JtiStore::new());
|
|
166
|
+
let gate = Arc::new(Barrier::new(THREADS));
|
|
167
|
+
let winners = Arc::new(AtomicUsize::new(0));
|
|
168
|
+
let handles: Vec<_> = (0..THREADS)
|
|
169
|
+
.map(|_| {
|
|
170
|
+
let (store, gate, winners) = (store.clone(), gate.clone(), winners.clone());
|
|
171
|
+
std::thread::spawn(move || {
|
|
172
|
+
gate.wait();
|
|
173
|
+
if store.claim("same-jti", NOW + 90, NOW) {
|
|
174
|
+
winners.fetch_add(1, Ordering::SeqCst);
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
.collect();
|
|
179
|
+
handles.into_iter().for_each(|h| h.join().unwrap());
|
|
180
|
+
assert_eq!(winners.load(Ordering::SeqCst), 1, "round {round}");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#[test]
|
|
185
|
+
fn a_poisoned_lock_does_not_turn_into_an_open_door() {
|
|
186
|
+
let store = Arc::new(JtiStore::new());
|
|
187
|
+
let clone = store.clone();
|
|
188
|
+
let _ = std::thread::spawn(move || {
|
|
189
|
+
let _guard = clone.seen.lock().unwrap();
|
|
190
|
+
panic!("poison the lock");
|
|
191
|
+
})
|
|
192
|
+
.join();
|
|
193
|
+
assert!(store.claim("a", NOW + 90, NOW));
|
|
194
|
+
assert!(!store.claim("a", NOW + 90, NOW));
|
|
195
|
+
}
|
|
196
|
+
}
|