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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 86690f16283c3330223e64f48f16b14746470206e24f173e7ba0202cd19509fb
4
- data.tar.gz: 2be48976ef5008c1cfeeb141f6c1a452be65f547334dd84b5f5b4dfebd319ec0
3
+ metadata.gz: 46f6f36759cf946f2fd26b0b11b0ec64b02b7b84e1213fcf239a08aa314d0168
4
+ data.tar.gz: 295310e1f10f391cfdc53900f7eda2ee6b96db34ced1e93fbd73e50978b6fa07
5
5
  SHA512:
6
- metadata.gz: be69a5d8669de20178d1d7c20f51b395105e94ba82c6b30dfbbce3c415299b413973fcfb79a6a52e3404d282d5873e6ea1b91aae96aae1e95b8b84646843ac7b
7
- data.tar.gz: d3a40b9ef1b73168adcb2659eb423760ea1f835390101005ec5ab19503d47ac19257bfb69fd3f0d1e7b27e2f59317faecc7c32a406a1deedf07f795d88c95184
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")
@@ -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.expand_path("../language/**/*.bluebook", __dir__)).select do |path|
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
@@ -1,5 +1,5 @@
1
1
  module Hecks
2
2
  # Read directly by the gemspec, so this file must never require the
3
3
  # rest of the gem (its dependencies aren't resolved yet at that point).
4
- VERSION = "3.0.2".freeze
4
+ VERSION = "3.0.3".freeze
5
5
  end
@@ -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");
@@ -1 +1 @@
1
- 3.0.2
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 a caller holding the
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) = active_session_email(domain_ir, cookies, secret, client).await {
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 needs access, not admin: a Member may list, a stranger may not.
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
- assert_eq!(listing, if caller == "stranger@example.com" { 401 } else { 200 }, "{caller} listing");
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?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hecks
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.2
4
+ version: 3.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Young