Class: Clickwrap::Generators::HardeningGenerator

Inherits:
Rails::Generators::Base
  • Object
show all
Includes:
ActiveRecord::Generators::Migration
Defined in:
lib/generators/clickwrap/hardening_generator.rb

Overview

rails generate clickwrap:hardening --database — the opt-in database tier.

Clickwrap's models refuse ordinary update and destroy calls. This generator adds a narrower database control for paths that bypass model callbacks: direct SQL, delete, delete_all, update_column, and update_all.

What that is worth is bounded, and the bound is the point: it rejects unsupported mutation paths within the documented database threat model. It does not make rows impossible to change, and it does nothing at all against anyone holding database superuser rights, direct file access, or the ability to drop the triggers — which, in most Rails applications, is the same credential that runs migrations. Real assurance against that comes from separately verified mechanisms: chained history, event digests published outside the primary database, and provider timestamps.

It is opt-in because it is a production decision with real consequences for anyone who clears tables with DELETE.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.next_migration_number(dir) ⇒ Object



35
36
37
# File 'lib/generators/clickwrap/hardening_generator.rb', line 35

def self.next_migration_number(dir)
  ActiveRecord::Generators::Base.next_migration_number(dir)
end

.render_event_write_sets(write_sets = Clickwrap::Event::DATABASE_HARDENING_WRITE_SETS) ⇒ Object

A generated migration must remain runnable after the application later upgrades Clickwrap, so it snapshots the write sets at generation time instead of consulting the then-current gem while db:migrate runs. The Event model is the sole source of truth; this formatter only turns that frozen contract into readable, self-contained migration code.



44
45
46
47
48
49
# File 'lib/generators/clickwrap/hardening_generator.rb', line 44

def self.render_event_write_sets(write_sets = Clickwrap::Event::DATABASE_HARDENING_WRITE_SETS)
  write_sets.map do |name, columns|
    wrapped = columns.each_slice(5).map { |slice| "      #{slice.join(" ")}" }.join("\n")
    %(    #{name.inspect} => %w[\n#{wrapped}\n    ])
  end.join(",\n")
end

Instance Method Details

#create_migration_fileObject



69
70
71
72
# File 'lib/generators/clickwrap/hardening_generator.rb', line 69

def create_migration_file
  migration_template "clickwrap_hardening.rb.erb",
                     File.join(db_migrate_path, "clickwrap_database_hardening.rb")
end

#display_post_install_messageObject



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/generators/clickwrap/hardening_generator.rb', line 83

def display_post_install_message
  say "\n☑️  The database hardening migration has been created.", :green
  say "\nBefore you run it:"
  say "  1. Read it. Its comments say exactly which transitions it accepts and rejects."
  say "  2. Check how your test suite clears tables. Transactional tests are fine —"
  say "     a rollback is not a DELETE. But fixtures, and any cleaner using the"
  say "     deletion strategy, run `DELETE FROM …`, and these protections reject"
  say "     blanket deletion of finalized events and their evidence children."
  say "  3. Run 'rails db:migrate'."
  say "\nThe migration is reversible: `rails db:rollback` removes the triggers and"
  say "functions it created and leaves your data alone."
  say "\nWhat this tier claims, in full: it rejects unsupported mutation paths within"
  say "the documented database threat model. Nothing more. A local digest is still a"
  say "local digest, and your server's clock is still your server's clock.\n"
end

#explain_adapter_supportObject



74
75
76
77
78
79
80
81
# File 'lib/generators/clickwrap/hardening_generator.rb', line 74

def explain_adapter_support
  case adapter_family
  when :postgresql then explain_postgresql
  when :sqlite then explain_sqlite
  when :mysql then explain_mysql
  else explain_unknown_adapter
  end
end

#require_explicit_opt_in!Object

Raises:

  • (Thor::Error)


51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/generators/clickwrap/hardening_generator.rb', line 51

def require_explicit_opt_in!
  return if options[:database]

  raise Thor::Error, <<~MSG
    ❌ Nothing was generated, on purpose.

    Database hardening changes what your database will accept, in every
    environment the migration runs in, so it is never applied as a side
    effect of installing the gem. Ask for it explicitly:

      rails generate clickwrap:hardening --database

    Read what it does and does not do first — the generated migration says
    so at the top, and the honest summary is: it rejects unsupported
    mutation paths, and it stops nobody with superuser rights.
  MSG
end