Module: Karst::Access::PopulationApprovals
- Defined in:
- lib/karst/access/population_approvals.rb
Overview
The local, machine-scoped record of which discovered candidate populations a developer has explicitly allowed Karst to execute automatically (see Karst::Access::ApprovedPopulations for how an approval is turned back into something runnable, and Karst::Access::PopulationDiscovery for what can be discovered at all).
Deliberately data, never code: an entry is a model name and a scope
name, and nothing else. Karst never writes a lambda, a snippet, or any
other executable Ruby into this file, and never evaluates its contents
-- an entry is only ever compared, as a string, against what current
source-based discovery independently confirms. That is what keeps this
file from degrading into an arbitrary method allowlist: adding
{"model": "User", "scope": "destroy_all"} by hand approves nothing,
because discovery will not confirm it.
Stored under the host application's tmp/ (tmp/karst/) on purpose:
Rails already treats tmp/ as machine-local, disposable, and
git-ignored, which is exactly the intended lifetime of a local
development approval. Deleting the file resets every approval; nothing
else in Karst is affected. Karst never edits the host application's
initializer or any other committed file.
Every read fails closed. A file that is unreadable, is not JSON, is not the expected document shape, carries an unknown schema version, or holds a single unusable entry approves nothing at all and reports an error for the panel to show -- rather than partially trusting a document Karst cannot fully account for. rubocop:disable Metrics/ModuleLength
Constant Summary collapse
- SCHEMA_VERSION =
1- RELATIVE_PATH =
File.join("tmp", "karst", "approved_populations.json")
- MODEL_NAME =
Both names are matched against exactly what PopulationDiscovery can produce -- a real constant path, and a scope name Ripper read from a literal symbol/string in
scope :name, -> { ... }. Anything else is rejected before it can even be compared to a discovered candidate. /\A[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*\z/- SCOPE_NAME =
/\A[a-z_][A-Za-z0-9_]*\z/- MAX_ENTRIES =
A bound on how much of this file Karst will consider at all, so a corrupted or maliciously grown document cannot turn every principal source resolution into an unbounded amount of source parsing.
500- Entry =
Value.define(:model_name, :method_name) do def matches?(model_name, method_name) self.model_name == model_name.to_s && self.method_name == method_name.to_s end def display_label "#{model_name}.#{method_name}" end end
- Record =
entriesis always usable (possibly empty) and always sorted;erroris a human-readable reason the stored document was rejected or could not be written, or nil. Value.define(:entries, :error) do def approved?(model_name, method_name) entries.any? { |entry| entry.matches?(model_name, method_name) } end end
Class Method Summary collapse
-
.display_path ⇒ Object
The path as a developer should see it: relative to the application root, since that is where they will go looking for (or delete) it.
- .load ⇒ Object
- .path ⇒ Object
-
.replace(entries) ⇒ Object
Replaces the whole approval set with
entries, atomically: callers always submit the complete list they intend to keep, so unapproving is simply approving a smaller set, and a partially written file can never be observed.
Class Method Details
.display_path ⇒ Object
The path as a developer should see it: relative to the application root, since that is where they will go looking for (or delete) it.
80 81 82 |
# File 'lib/karst/access/population_approvals.rb', line 80 def display_path RELATIVE_PATH end |
.load ⇒ Object
84 85 86 87 88 89 90 91 92 93 |
# File 'lib/karst/access/population_approvals.rb', line 84 def load document = JSON.parse(File.read(path)) parse(document) rescue Errno::ENOENT empty rescue JSON::ParserError failed("could not be read as JSON") rescue StandardError => e failed("could not be read (#{e.class})") end |
.path ⇒ Object
74 75 76 |
# File 'lib/karst/access/population_approvals.rb', line 74 def path File.join(root, RELATIVE_PATH) end |
.replace(entries) ⇒ Object
Replaces the whole approval set with entries, atomically: callers
always submit the complete list they intend to keep, so unapproving
is simply approving a smaller set, and a partially written file can
never be observed.
99 100 101 102 103 104 105 |
# File 'lib/karst/access/population_approvals.rb', line 99 def replace(entries) normalized = normalize(entries) write(normalized) Record.new(entries: normalized, error: nil) rescue StandardError => e Record.new(entries: normalized || [].freeze, error: "approvals could not be saved (#{e.class})") end |