hecks 3.0.2 → 3.0.3
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/embryonaut_bluebook.rb +10 -0
- data/lib/hecks/grammar/evolve.rb +22 -1
- data/lib/hecks/version.rb +1 -1
- data/rust/build/src/resolve.rs +25 -0
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/web.rs +38 -5
- data/rust/project_rust_pipeline.rb +4 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 46f6f36759cf946f2fd26b0b11b0ec64b02b7b84e1213fcf239a08aa314d0168
|
|
4
|
+
data.tar.gz: 295310e1f10f391cfdc53900f7eda2ee6b96db34ced1e93fbd73e50978b6fa07
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9d7a89486e004a7e786177ee9debfb4fcdc1e331611da9293a29cc2e16e0b4f2b388d718d3062e7b236e27b2655f16282e6e1b1065bb151fcdd8bb67c839a34f
|
|
7
|
+
data.tar.gz: 9713d8af8eb4eebdf7885e0ec9821e304f0d6be393e22968c32b971e83d920e6f4ecfa52ce3ee46a19336de8a4e50d5c70d20c443784042aeac5607046ceb9ce
|
|
@@ -7,6 +7,10 @@ module Hecks
|
|
|
7
7
|
autoload :Vendor, File.join(__dir__, "embryonaut_bluebook/vendor")
|
|
8
8
|
autoload :VendorCli, File.join(__dir__, "embryonaut_bluebook/vendor_cli")
|
|
9
9
|
|
|
10
|
+
# The shape `hecks vendor` accepts for a package directory name. The name is joined into a
|
|
11
|
+
# path that `load!` evaluates as Ruby, so `../x` or an absolute path must never reach it.
|
|
12
|
+
PACKAGE_NAME = /\A[a-z][a-z0-9_]*\z/
|
|
13
|
+
|
|
10
14
|
# Loads a vendored embryonaut bluebook package's `.bluebook` files, once
|
|
11
15
|
# per registry.
|
|
12
16
|
#
|
|
@@ -23,6 +27,12 @@ module Hecks
|
|
|
23
27
|
"uses_embryonaut_bluebook(#{name.inspect}) needs a registry with a root to vendor from"
|
|
24
28
|
end
|
|
25
29
|
|
|
30
|
+
unless name.to_s.match?(PACKAGE_NAME)
|
|
31
|
+
raise Runtime::WiringError,
|
|
32
|
+
"uses_embryonaut_bluebook(#{name.inspect}) is not a package name — it must match " \
|
|
33
|
+
"[a-z][a-z0-9_]* (the name `hecks vendor` accepts)"
|
|
34
|
+
end
|
|
35
|
+
|
|
26
36
|
return if registry.bluebook(Naming.pascal(name.to_s))
|
|
27
37
|
|
|
28
38
|
dir = File.join(registry.root, "vendor", "embryonaut_bluebooks", name.to_s, "bluebook")
|
data/lib/hecks/grammar/evolve.rb
CHANGED
|
@@ -53,9 +53,30 @@ module Hecks
|
|
|
53
53
|
raise
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
# Where the language's own bluebooks live: this checkout's, unless a block is running under
|
|
57
|
+
# `with_language_dir`.
|
|
58
|
+
#
|
|
59
|
+
# @return [String] the directory holding the syntax tables
|
|
60
|
+
def language_dir = @language_dir || File.expand_path("../language", __dir__)
|
|
61
|
+
|
|
62
|
+
# Runs the block with every table read and edit aimed at `dir`, so a caller can try real
|
|
63
|
+
# edits on a copy. The language's own files are what other processes load, so an edit that
|
|
64
|
+
# lands on them, even one put back afterwards, can be loaded half-made.
|
|
65
|
+
#
|
|
66
|
+
# @param dir [String] a directory laid out like `lib/hecks/language`
|
|
67
|
+
# @yield the work to run against `dir`
|
|
68
|
+
# @return [Object] what the block returns
|
|
69
|
+
def with_language_dir(dir)
|
|
70
|
+
previous = @language_dir
|
|
71
|
+
@language_dir = dir
|
|
72
|
+
yield
|
|
73
|
+
ensure
|
|
74
|
+
@language_dir = previous
|
|
75
|
+
end
|
|
76
|
+
|
|
56
77
|
# Every syntax-table file declaring a KeywordSeed or ArgumentSeed value object.
|
|
57
78
|
def syntax_paths
|
|
58
|
-
Dir.glob(File.
|
|
79
|
+
Dir.glob(File.join(language_dir, "**/*.bluebook")).select do |path|
|
|
59
80
|
source = read_source(path)
|
|
60
81
|
source.include?('value_object "KeywordSeed"') || source.include?('value_object "ArgumentSeed"')
|
|
61
82
|
end
|
data/lib/hecks/version.rb
CHANGED
data/rust/build/src/resolve.rs
CHANGED
|
@@ -94,7 +94,20 @@ fn resolve_json(parser_bin: &Path, chapter_name: &str, hecksagon_path: &Path, ke
|
|
|
94
94
|
Ok(names.iter().filter_map(Json::as_str).map(str::to_string).collect())
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/// `/\A[a-z][a-z0-9_]*\z/` — the shape `hecks vendor` accepts for a package directory name.
|
|
98
|
+
/// The name is joined into a path and, through its chapter, into a directory the build deletes,
|
|
99
|
+
/// so `../x` or an absolute path must never reach either.
|
|
100
|
+
pub fn valid_package_name(name: &str) -> bool {
|
|
101
|
+
let mut chars = name.chars();
|
|
102
|
+
matches!(chars.next(), Some(c) if c.is_ascii_lowercase()) && chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
|
|
103
|
+
}
|
|
104
|
+
|
|
97
105
|
pub fn vendored_bluebook_files(domain: &Path, pkg_name: &str) -> Result<Vec<PathBuf>, String> {
|
|
106
|
+
if !valid_package_name(pkg_name) {
|
|
107
|
+
return Err(format!(
|
|
108
|
+
"uses_embryonaut_bluebook {pkg_name:?} is not a package name — it must match [a-z][a-z0-9_]* (the name `hecks vendor` accepts)"
|
|
109
|
+
));
|
|
110
|
+
}
|
|
98
111
|
let dir = domain.join("vendor").join("embryonaut_bluebooks").join(pkg_name).join("bluebook");
|
|
99
112
|
if !dir.is_dir() {
|
|
100
113
|
return Err(format!(
|
|
@@ -109,6 +122,18 @@ pub fn vendored_bluebook_files(domain: &Path, pkg_name: &str) -> Result<Vec<Path
|
|
|
109
122
|
mod tests {
|
|
110
123
|
use super::*;
|
|
111
124
|
|
|
125
|
+
#[test]
|
|
126
|
+
fn a_package_name_that_is_a_path_is_refused_before_any_directory_is_touched() {
|
|
127
|
+
for name in ["../x", "a/b", "/etc", "", "X", "1st", "a-b", "a b", ".."] {
|
|
128
|
+
assert!(!valid_package_name(name), "{name:?}");
|
|
129
|
+
let refusal = vendored_bluebook_files(Path::new("/nonexistent"), name).unwrap_err();
|
|
130
|
+
assert!(refusal.contains("is not a package name"), "{refusal}");
|
|
131
|
+
}
|
|
132
|
+
for name in ["payments", "console_settings", "a1"] {
|
|
133
|
+
assert!(valid_package_name(name), "{name:?}");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
112
137
|
#[test]
|
|
113
138
|
fn pascal_cases_snake_case_stems() {
|
|
114
139
|
assert_eq!(pascal("governance"), "Governance");
|
data/rust/host/HECKS_RELEASE
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.0.
|
|
1
|
+
3.0.3
|
data/rust/host/src/web.rs
CHANGED
|
@@ -373,10 +373,11 @@ async fn accounts_me_route(domain_ir: &Value, cookies: &HashMap<String, String>,
|
|
|
373
373
|
}
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
-
// GET /members: admitted people as JSON, for
|
|
377
|
-
// account cookie but not the Governance session /admin/members needs.
|
|
376
|
+
// GET /members: admitted people as JSON, for an Admin or Owner holding the
|
|
377
|
+
// account cookie but not the Governance session /admin/members needs. The list
|
|
378
|
+
// is every person's name, email and role, so a plain member does not get it.
|
|
378
379
|
async fn members_route(domain_ir: &Value, cookies: &HashMap<String, String>, secret: &str, client: &Mutex<Client>) -> Value {
|
|
379
|
-
if let Err(response) =
|
|
380
|
+
if let Err(response) = newsletter_send::require_admin(domain_ir, cookies, secret, client).await {
|
|
380
381
|
return response;
|
|
381
382
|
}
|
|
382
383
|
match auth::all_people(client, domain_ir).await {
|
|
@@ -2302,6 +2303,32 @@ mod tests {
|
|
|
2302
2303
|
}
|
|
2303
2304
|
}
|
|
2304
2305
|
|
|
2306
|
+
#[tokio::test]
|
|
2307
|
+
async fn members_route_refuses_a_plain_member_with_a_403_and_no_people() {
|
|
2308
|
+
let secret = "s3cret";
|
|
2309
|
+
let (client, domain_ir) = scratch_members_db("hecks_host_web_test_members_route_403").await;
|
|
2310
|
+
client
|
|
2311
|
+
.lock()
|
|
2312
|
+
.await
|
|
2313
|
+
.execute(
|
|
2314
|
+
"INSERT INTO acme_member_head_snapshot_1 (id, ordinal, state) VALUES ($1, 0, $2::jsonb)",
|
|
2315
|
+
&[
|
|
2316
|
+
&"max@example.com",
|
|
2317
|
+
&json!({"name": {"value": "Max"}, "email": {"value": "max@example.com"},
|
|
2318
|
+
"role": {"value": "Member"}, "identity_id": {"value": "id-2"}}),
|
|
2319
|
+
],
|
|
2320
|
+
)
|
|
2321
|
+
.await
|
|
2322
|
+
.unwrap();
|
|
2323
|
+
|
|
2324
|
+
let response = members_route(&domain_ir, &session_cookies(secret, "max@example.com"), secret, &client).await;
|
|
2325
|
+
assert_eq!(response["statusCode"], 403, "{response:?}");
|
|
2326
|
+
let body = response["body"].as_str().unwrap();
|
|
2327
|
+
for leaked in ["zed@example.com", "amy@example.com", "Zed"] {
|
|
2328
|
+
assert!(!body.contains(leaked), "{leaked:?} leaked to a plain member: {body}");
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2305
2332
|
fn session_cookies(secret: &str, email: &str) -> HashMap<String, String> {
|
|
2306
2333
|
let mut cookies = HashMap::new();
|
|
2307
2334
|
cookies.insert(auth::account_cookie_name(), auth::account_token(secret, email, 60));
|
|
@@ -2808,9 +2835,15 @@ mod tests {
|
|
|
2808
2835
|
} else {
|
|
2809
2836
|
assert_eq!((add, registrations, disable, enable), (403, 403, 403, 403), "{caller} is refused everywhere");
|
|
2810
2837
|
}
|
|
2811
|
-
// Listing
|
|
2838
|
+
// Listing is an admin gate too: it carries every person's email and role, so an
|
|
2839
|
+
// Admin or Owner lists, a Member is refused, and a stranger has no session at all.
|
|
2812
2840
|
let listing = status_of(members_route(&domain_ir, &cookies, secret, &client).await).await;
|
|
2813
|
-
|
|
2841
|
+
let expected = match caller {
|
|
2842
|
+
"stranger@example.com" => 401,
|
|
2843
|
+
_ if admits => 200,
|
|
2844
|
+
_ => 403,
|
|
2845
|
+
};
|
|
2846
|
+
assert_eq!(listing, expected, "{caller} listing");
|
|
2814
2847
|
}
|
|
2815
2848
|
|
|
2816
2849
|
// Anonymous: 401 on every route, before any role is looked at.
|
|
@@ -70,6 +70,10 @@ module RustProjectPipeline
|
|
|
70
70
|
# vendor/embryonaut_bluebooks/<name>/bluebook/, sorted. A vendored
|
|
71
71
|
# package has no .hecksagon of its own either, just its .bluebook(s).
|
|
72
72
|
chapters += uses_embryonaut_bluebook_names.map do |pkg_name|
|
|
73
|
+
unless pkg_name.to_s.match?(Hecks::EmbryonautBluebook::PACKAGE_NAME)
|
|
74
|
+
abort "hecks project_rust (Rust path): uses_embryonaut_bluebook #{pkg_name.inspect} is not a package " \
|
|
75
|
+
"name — it must match [a-z][a-z0-9_]* (the name `hecks vendor` accepts)"
|
|
76
|
+
end
|
|
73
77
|
pkg_dir = File.join(domain, "vendor", "embryonaut_bluebooks", pkg_name.to_s, "bluebook")
|
|
74
78
|
pkg_files = Dir.glob(File.join(pkg_dir, "*.bluebook")).sort
|
|
75
79
|
if pkg_files.empty?
|