Module: Rubino::Security::DenyPersister

Defined in:
lib/rubino/security/deny_persister.rb

Overview

Persists an explicit “deny always” verdict to the ‘permissions` map so it survives a process restart and auto-denies future sibling commands through ApprovalPolicy#decide, which evaluates a permissions:deny rule FIRST (before any allow path — see approval_policy.rb step 2).

The DENY counterpart to AllowlistPersister: same Config::Writer (dot-notation -> YAML) + live-config sync, but it writes into ‘permissions` instead of `security.command_allowlist`, and the value is the verdict “deny” keyed by a PatternMatcher-format pattern (“<tool> <glob>”) rather than a bare prefix.

The pattern is derived from the SAME PrefixDeriver rule the allow side uses, so “deny always” is scoped consistently with “always allow”:

:prefix  -> "<tool> <head>*"     (e.g. "shell git*"  — denies every sibling)
:command -> "<tool> <command>"   (e.g. "shell rm -rf /tmp/x" — exact)
:pattern -> "<tool> <command>"   (a dangerous-pattern description is not a
                                  command glob, so deny the exact command)

Append-unique: an already-present “<pattern>”: “deny” entry is a no-op.

SCOPING NOTE: identical to AllowlistPersister — Config::Writer writes the process-global config.yml. Fine for a single-process / single-home setup; a shared-server deployment would need per-user config scoping.

Constant Summary collapse

KEY =
"permissions"
DENY =
"deny"

Class Method Summary collapse

Class Method Details

.current_permissions(config) ⇒ Object



64
65
66
# File 'lib/rubino/security/deny_persister.rb', line 64

def current_permissions(config)
  ((config || Rubino.configuration).dig("permissions") || {}).dup
end

.default_config_pathObject



68
69
70
# File 'lib/rubino/security/deny_persister.rb', line 68

def default_config_path
  Config::Loader.new.config_path
end

.pattern_for(tool:, rule:, command:) ⇒ Object

The PatternMatcher-format key a (tool, rule, command) “deny always” persists as. Mirrors the allow side’s scoping: a derivable :prefix denies the whole prefix class (“<tool> <head>*”); everything else denies the exact command (“<tool> <command>”). Nil when there is nothing to key on.



55
56
57
58
59
60
61
62
# File 'lib/rubino/security/deny_persister.rb', line 55

def pattern_for(tool:, rule:, command:)
  cmd = command.to_s.strip
  if rule&.kind == :prefix && !rule.value.to_s.strip.empty?
    "#{tool} #{rule.value.strip}*"
  elsif !cmd.empty?
    "#{tool} #{cmd}"
  end
end

.persist(pattern, config: nil, config_path: nil) ⇒ Object

Persists a permissions:deny rule for ‘pattern` (unique). Returns the resulting permissions hash. A blank pattern is a no-op.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/rubino/security/deny_persister.rb', line 35

def persist(pattern, config: nil, config_path: nil)
  key = pattern.to_s.strip
  return current_permissions(config) if key.empty?

  config ||= Rubino.configuration
  existing = current_permissions(config)
  return existing if existing[key] == DENY

  updated = existing.merge(key => DENY)
  Config::Writer.new(config_path: config_path || default_config_path).set(KEY, updated)
  # Keep the live config in sync so an ApprovalPolicy built this process
  # sees the new deny rule immediately (the writer only touches disk).
  config.set(KEY, updated)
  updated
end