active_record-undo 0.1.5 → 0.2.0
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/DOCUMENTATION.md +96 -22
- data/README.md +350 -197
- data/app/controllers/active_record/undo/application_controller.rb +91 -0
- data/app/controllers/active_record/undo/logs_controller.rb +87 -0
- data/app/controllers/active_record/undo/restores_controller.rb +11 -0
- data/config/routes.rb +11 -0
- data/lib/active_record/undo/configuration.rb +8 -1
- data/lib/active_record/undo/engine.rb +14 -1
- data/lib/active_record/undo/model_extension/attribution_helper.rb +8 -0
- data/lib/active_record/undo/model_extension/tenant_verification.rb +1 -1
- data/lib/active_record/undo/model_extension.rb +15 -2
- data/lib/active_record/undo/safe_redirect.rb +60 -0
- data/lib/active_record/undo/undo_log.rb +60 -2
- data/lib/active_record/undo/version.rb +1 -1
- data/lib/active_record/undo/view_helpers.rb +101 -0
- data/lib/active_record/undo.rb +13 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1fc1fe96cf406b3cdb4034d8c0a20b2920b3ea685eaa67a88c59b850bf2971d8
|
|
4
|
+
data.tar.gz: cc1830bd09b2eeec06f8d27992a2b18255d6db0277751fd9c7614581f8d9d10b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d558abdcbb507970ff723a3453f35a8a37581c4aad79e605112affbddd23be71bc224a6a1f400fee76ffd7b3655eb408752c800216ab70229e02e62b883a5e8
|
|
7
|
+
data.tar.gz: d0be5d9b492aa4a6cf3bc2a46ff5aa2c83da139f069eb377c5799932e158e82ffc0e3122a47e9aca0de9eb2d9c2d4c9b58033dda90047f78f8a2f7eeb7ef209d
|
data/DOCUMENTATION.md
CHANGED
|
@@ -39,23 +39,29 @@ graph TD
|
|
|
39
39
|
|
|
40
40
|
### Component Summary
|
|
41
41
|
|
|
42
|
-
| Component | File Path
|
|
43
|
-
| :----------------------------- |
|
|
44
|
-
| **Main Hook & Context** | `lib/active_record/undo.rb`
|
|
45
|
-
| **Model Extension** | `lib/active_record/undo/model_extension.rb`
|
|
46
|
-
| **Model Attribution Helper** | `lib/active_record/undo/model_extension/attribution_helper.rb`
|
|
47
|
-
| **Tenant Verification** | `lib/active_record/undo/model_extension/tenant_verification.rb
|
|
48
|
-
| **Cascade Engine** | `lib/active_record/undo/cascade_handler.rb`
|
|
49
|
-
| **Cascade Association Finder** | `lib/active_record/undo/cascade_handler/association_finder.rb`
|
|
50
|
-
| **Cascade Record Updater** | `lib/active_record/undo/cascade_handler/record_updater.rb`
|
|
51
|
-
| **Audit Log Parent** | `lib/active_record/undo/undo_log.rb`
|
|
52
|
-
| **Audit Log Child** | `lib/active_record/undo/undo_log_item.rb`
|
|
53
|
-
| **Engine Link** | `lib/active_record/undo/engine.rb`
|
|
54
|
-
| **Configuration** | `lib/active_record/undo/configuration.rb`
|
|
55
|
-
| **Purger Service** | `lib/active_record/undo/purger.rb`
|
|
56
|
-
| **Purger Reflection Helper** | `lib/active_record/undo/purger/reflection_helper.rb`
|
|
57
|
-
| **Purge Job** | `lib/active_record/undo/purge_job.rb`
|
|
58
|
-
| **Rake Task** | `lib/active_record/undo/tasks/purge.rake`
|
|
42
|
+
| Component | File Path | Class / Module | Core Responsibility |
|
|
43
|
+
| :----------------------------- | :-------------------------------------------------------------- | :------------------- | :------------------------------------------------------------------------------------------------------------------------------------- |
|
|
44
|
+
| **Main Hook & Context** | `lib/active_record/undo.rb` | `ActiveRecord::Undo` | Hooks into `ActiveSupport.on_load(:active_record)`, manages ambient thread context (`whodunnit`, `current_tenant`), and defines errors |
|
|
45
|
+
| **Model Extension** | `lib/active_record/undo/model_extension.rb` | `ModelExtension` | Injects DSL (`acts_as_undoable`), scopes (`kept`, `soft_deleted`), and methods (`soft_delete!`, `undoable?`, `restore!`) |
|
|
46
|
+
| **Model Attribution Helper** | `lib/active_record/undo/model_extension/attribution_helper.rb` | `AttributionHelper` | Resolves user attribution and tenant context from parameters, configured procs, or thread contexts |
|
|
47
|
+
| **Tenant Verification** | `lib/active_record/undo/model_extension/tenant_verification.rb` | `TenantVerification` | Validates initiating context against log tenant on `#restore!`, raising `SecurityError` on mismatch |
|
|
48
|
+
| **Cascade Engine** | `lib/active_record/undo/cascade_handler.rb` | `CascadeHandler` | Inspects ActiveRecord reflections (`reflections`) and executes DFS traversal |
|
|
49
|
+
| **Cascade Association Finder** | `lib/active_record/undo/cascade_handler/association_finder.rb` | `AssociationFinder` | Resolves which records should cascade based on dependency configuration |
|
|
50
|
+
| **Cascade Record Updater** | `lib/active_record/undo/cascade_handler/record_updater.rb` | `RecordUpdater` | Updates the database timestamps directly bypassing callbacks |
|
|
51
|
+
| **Audit Log Parent** | `lib/active_record/undo/undo_log.rb` | `UndoLog` | Represents the top-level deletion event and manages atomic batch restoration, tenancy, and user attribution |
|
|
52
|
+
| **Audit Log Child** | `lib/active_record/undo/undo_log_item.rb` | `UndoLogItem` | Maps polymorphic targets (`item_type`, `item_id`) to original deleted entities |
|
|
53
|
+
| **Engine Link** | `lib/active_record/undo/engine.rb` | `Engine` | Appends `db/migrate/` directly to host app migration paths |
|
|
54
|
+
| **Configuration** | `lib/active_record/undo/configuration.rb` | `Configuration` | Houses retention_period, current_user_method, and current_tenant_method settings |
|
|
55
|
+
| **Purger Service** | `lib/active_record/undo/purger.rb` | `Purger` | Deletes expired UndoLog/Items and soft-deleted model records in batches, scoped to tenant context when present |
|
|
56
|
+
| **Purger Reflection Helper** | `lib/active_record/undo/purger/reflection_helper.rb` | `ReflectionHelper` | Determines cascade and nullify reflection behavior for purge routines |
|
|
57
|
+
| **Purge Job** | `lib/active_record/undo/purge_job.rb` | `PurgeJob` | ActiveJob background runner invoking Purger |
|
|
58
|
+
| **Rake Task** | `lib/active_record/undo/tasks/purge.rake` | Rake Task | Exposes `active_record_undo:purge_expired` command |
|
|
59
|
+
| **Mountable Engine** | `lib/active_record/undo/engine.rb` | `Engine` | Defines mountable Rails engine route namespace, view helper hooks, and migrations |
|
|
60
|
+
| **Open-Redirect Guard** | `lib/active_record/undo/safe_redirect.rb` | `SafeRedirect` | Enforces URL safety, same-host/port validation, CRLF blocking, and safe fallback paths |
|
|
61
|
+
| **View Helpers** | `lib/active_record/undo/view_helpers.rb` | `ViewHelpers` | Provides `undo_button_to` and `undo_link_to` helpers with automatic model and signed token resolution |
|
|
62
|
+
| **Engine Base Controller** | `app/controllers/active_record/undo/application_controller.rb` | `ApplicationController` | Base engine controller handling CSRF, attribution, tenancy, and multi-format error responses |
|
|
63
|
+
| **HTTP Restoration Endpoints** | `app/controllers/active_record/undo/logs_controller.rb` | `LogsController` | Handles restore actions by ID or signed token for HTML, Turbo Stream, and JSON |
|
|
64
|
+
| **Signed Restore Endpoint** | `app/controllers/active_record/undo/restores_controller.rb` | `RestoresController` | Dedicated route endpoint for signed token restorations (`POST /undo/restore/:token`) |
|
|
59
65
|
|
|
60
66
|
---
|
|
61
67
|
|
|
@@ -191,6 +197,7 @@ sequenceDiagram
|
|
|
191
197
|
7. **Tenant Matching Verification:** During `#restore!`, if `UndoLog` is associated with a tenant, the current tenant context is verified against the log's tenant before initiating restoration, raising `ActiveRecord::Undo::SecurityError` on mismatch to prevent cross-tenant data restores.
|
|
192
198
|
8. **User Attribution Transparency:** `#soft_delete!` and `#restore!` support transparent user tracking via explicit keyword arguments or ambient context resolution (`Current.user` / Thread context).
|
|
193
199
|
9. **Configured Context Enforcement:** When `current_user_method` or `current_tenant_method` is configured via `ActiveRecord::Undo.configure`, operations verify that the evaluated context is not `nil`. If any configured method evaluates to `nil`, `soft_delete!` and `restore!` immediately raise `ActiveRecord::Undo::SecurityError` to prevent unauthenticated or tenantless mutations.
|
|
200
|
+
10. **Cryptographic Restore Tokens & Single-Use Semantics:** `UndoLog#signed_token` and `ModelExtension#signed_token` generate HMAC-SHA256 signed tokens using Rails' `message_verifier(:active_record_undo)` (configured with `ActiveRecord::Undo.config.token_expires_in`). The token encapsulates the log's ID with a purpose lock (`purpose: :restore`) and timestamp expiration (`exp`). When verified via `UndoLog.find_by_signed_token`, any payload alteration, purpose mismatch, or expiration automatically invalidates the token. Furthermore, because `#restore!` destroys the `UndoLog` database record upon completion, tokens automatically provide single-use replay protection: once restored, subsequent attempts to verify the same token find no corresponding database record, resulting in a `404 Not Found`. Even with a cryptographically authentic token, tenant boundary validation (`TenantVerification`) remains strictly enforced during restoration to prevent cross-tenant attacks.
|
|
194
201
|
|
|
195
202
|
---
|
|
196
203
|
|
|
@@ -235,6 +242,10 @@ sequenceDiagram
|
|
|
235
242
|
1. Returns `false` immediately if `soft_deleted?` is `false`.
|
|
236
243
|
2. Performs a lightweight SQL `EXISTS` query (`ActiveRecord::Undo::UndoLogItem.joins(:undo_log).exists?(item_type: self.class.name, item_id: id)`).
|
|
237
244
|
3. Returns `true` if both the log item and its parent `UndoLog` exist in the database.
|
|
245
|
+
* **`undo_log` / `latest_undo_log`**
|
|
246
|
+
* *Function*: Resolves the latest `UndoLog` audit record recording the soft deletion of this model instance via `find_latest_undo_log_item`. Returns `nil` if not soft-deleted or already purged/restored.
|
|
247
|
+
* **`signed_token(expires_in: ...)` / `to_signed_token`**
|
|
248
|
+
* *Function*: Generates a cryptographic signed token using Rails' message verifier directly from the model instance for secure direct-link restorations.
|
|
238
249
|
* **`soft_delete!(whodunnit: nil, tenant: nil)`**
|
|
239
250
|
* *Function*: Starts the cascade soft-deletion sequence for the record.
|
|
240
251
|
* *Parameters*:
|
|
@@ -315,14 +326,28 @@ sequenceDiagram
|
|
|
315
326
|
|
|
316
327
|
### 6.9 `lib/active_record/undo/undo_log.rb` (Batch Restoration)
|
|
317
328
|
|
|
318
|
-
* **`restore
|
|
329
|
+
* **`restore!(whodunnit: nil)`**
|
|
319
330
|
* *Function*: Triggers database restoration of the entire tree recorded under this log.
|
|
320
331
|
* *Steps*:
|
|
321
332
|
1. Invokes `ActiveRecord::Undo.verify_configured_context!` to enforce that configured context methods do not evaluate to `nil`.
|
|
322
|
-
2.
|
|
323
|
-
3.
|
|
324
|
-
4.
|
|
325
|
-
5.
|
|
333
|
+
2. Enforces tenant verification if tenant association is present on the log.
|
|
334
|
+
3. Resolves `whodunnit` actor context.
|
|
335
|
+
4. Opens a database transaction block.
|
|
336
|
+
5. Iterates over associated `undo_log_items` in *reverse order* (`reverse_each`), guaranteeing parent records are restored before child records.
|
|
337
|
+
6. Invokes `#restore_item!` on each item.
|
|
338
|
+
7. Automatically calls `#destroy!` on completion to purge the audit records (`UndoLog` and nested `UndoLogItem` rows) from the database.
|
|
339
|
+
* **`expired?`**
|
|
340
|
+
* *Function*: Checks whether `created_at` timestamp is older than `ActiveRecord::Undo.config.retention_period`.
|
|
341
|
+
* **`signed_token(expires_in: ActiveRecord::Undo.config.token_expires_in, purpose: :restore)` / `to_signed_token`**
|
|
342
|
+
* *Function*: Generates a cryptographically signed, URL-safe token representing this `UndoLog` for direct link restorations.
|
|
343
|
+
* *Details*: Employs `self.class.token_verifier.generate(id, purpose: purpose, expires_in: expires_in)`.
|
|
344
|
+
* **`.find_by_signed_token(token, purpose: :restore)`**
|
|
345
|
+
* *Function*: Verifies the cryptographic token signature and fetches the matching `UndoLog` database record.
|
|
346
|
+
* *Details*: Returns `nil` if the token is blank, expired, tampered with, has a mismatched purpose, or if the underlying `UndoLog` record no longer exists in the database.
|
|
347
|
+
* **`.verify_signed_token(token, purpose: :restore)`**
|
|
348
|
+
* *Function*: Decodes and verifies the token payload via `token_verifier.verified(token, purpose: purpose)`. Catches `ActiveSupport::MessageVerifier::InvalidSignature` and returns `nil` on tampering or expiration.
|
|
349
|
+
* **`.token_verifier`**
|
|
350
|
+
* *Function*: Provides the `ActiveSupport::MessageVerifier` instance used for token generation and verification. In Rails environments, utilizes `Rails.application.message_verifier(:active_record_undo)`. In standalone environments, falls back to an SHA256 verifier keyed to `ActiveRecord::Undo.config.token_secret_key`.
|
|
326
351
|
* **`.for_whodunnit(user)`**
|
|
327
352
|
* *Function*: Scopes logs to those deleted by a specific user/actor. Supports both model instances and raw IDs.
|
|
328
353
|
* **`.for_tenant(tenant)`**
|
|
@@ -353,6 +378,11 @@ sequenceDiagram
|
|
|
353
378
|
* `@retention_period`: Defaults to `30.days`.
|
|
354
379
|
* `@current_user_method`: Defaults to `nil`. Callable proc to resolve the current user/actor.
|
|
355
380
|
* `@current_tenant_method`: Defaults to `nil`. Callable proc to resolve the current tenant.
|
|
381
|
+
* `@base_controller`: Defaults to `"::ApplicationController"`. Base controller for engine authentication and authorization hooks.
|
|
382
|
+
* `@default_redirect_path`: Defaults to lambda resolving `main_app.root_path` (falling back to `'/'`).
|
|
383
|
+
* `@token_expires_in`: Defaults to `24.hours`. Lifespan for signed direct-link restore tokens.
|
|
384
|
+
* `@token_secret_key`: Defaults to `nil` (uses Rails application verifier).
|
|
385
|
+
* `@error_handling`: Defaults to `:auto`. Controls whether HTML requests redirect on error or render status codes.
|
|
356
386
|
|
|
357
387
|
### 6.12 `lib/active_record/undo/purger.rb` (Hard Purging Engine)
|
|
358
388
|
|
|
@@ -383,3 +413,47 @@ sequenceDiagram
|
|
|
383
413
|
* **`active_record_undo:purge_expired`**
|
|
384
414
|
* *Function*: Exposes CLI Rake task for the purger engine.
|
|
385
415
|
* *Details*: Checks `ENV['BATCH_SIZE']` for custom batch sizes, falling back to 1000, and triggers `ActiveRecord::Undo::Purger.purge_expired!`.
|
|
416
|
+
|
|
417
|
+
### 6.16 `lib/active_record/undo/engine.rb` (Mountable Rails Engine)
|
|
418
|
+
|
|
419
|
+
* **`ActiveRecord::Undo::Engine`**
|
|
420
|
+
* *Function*: Rails engine that defines isolated namespace `ActiveRecord::Undo` and mounts routes.
|
|
421
|
+
* *Details*:
|
|
422
|
+
* Automatically hooks view helpers into ActionView via `ActiveSupport.on_load(:action_view)`.
|
|
423
|
+
* Registers `:turbo_stream` MIME type (`text/vnd.turbo-stream.html`) when not already defined.
|
|
424
|
+
* Appends gem migrations to host app's migration paths.
|
|
425
|
+
|
|
426
|
+
### 6.17 `lib/active_record/undo/safe_redirect.rb` (Open-Redirect Prevention)
|
|
427
|
+
|
|
428
|
+
* **`determine_redirect_path` (Private)**
|
|
429
|
+
* *Function*: Determines safe target path in priority: validated `params[:redirect_to]`, validated `request.referer`, or `resolve_fallback_path`.
|
|
430
|
+
* **`safe_redirect_path(path)` (Private)**
|
|
431
|
+
* *Function*: Validates URL to strictly allow relative paths or same-host/port absolute URLs, preventing open redirect vulnerabilities. Automatically rejects CRLF characters (`\r`, `\n`) and non-HTTP/HTTPS schemes (e.g. `javascript:`, `data:`).
|
|
432
|
+
|
|
433
|
+
### 6.18 `lib/active_record/undo/view_helpers.rb` (ERB View Helpers)
|
|
434
|
+
|
|
435
|
+
* **`undo_button_to(target, text = nil, signed: false, **html_options)`**
|
|
436
|
+
* *Function*: Renders a `button_to` form targeting `active_record_undo.restore_log_path` or `signed_restore_path`.
|
|
437
|
+
* **`undo_link_to(target, text = nil, signed: false, **html_options)`**
|
|
438
|
+
* *Function*: Renders an anchor tag configured with `data: { turbo_method: :post }` for Turbo and UJS compatibility.
|
|
439
|
+
* **`extract_log_target(target)` (Private)**
|
|
440
|
+
* *Function*: Safely extracts the target `UndoLog`. When passed an ActiveRecord model, verifies that the model is soft-deleted and has an associated undo log, raising `ArgumentError` if active or unpersisted to prevent erroneous routing with the model's primary key.
|
|
441
|
+
|
|
442
|
+
### 6.19 `app/controllers/active_record/undo/application_controller.rb` (Engine Base Controller)
|
|
443
|
+
|
|
444
|
+
* **`ApplicationController`**
|
|
445
|
+
* *Function*: Inherits from `ActiveRecord::Undo.base_controller_class` with CSRF protection enabled.
|
|
446
|
+
* *Details*: Handles tenant resolution, whodunnit resolution, open redirect verification, and graceful error responses (HTML, Turbo Stream, JSON).
|
|
447
|
+
|
|
448
|
+
### 6.20 `app/controllers/active_record/undo/logs_controller.rb` & `restores_controller.rb` (HTTP Endpoints)
|
|
449
|
+
|
|
450
|
+
* **`LogsController#restore`**
|
|
451
|
+
* *Function*: Finds undo log by ID or signed token, checks expiration and permissions, executes restoration, and returns format-specific response (emitting `303 See Other` on HTML redirect for Turbo Drive compatibility).
|
|
452
|
+
* **`LogsController#find_undo_log` (Private)**
|
|
453
|
+
* *Function*: Resolves the target `UndoLog` from request parameters. If `params[:token]` is present, calls `UndoLog.find_by_signed_token(params[:token])`. If `params[:id]` is present, delegates to `find_by_id_or_token`.
|
|
454
|
+
* **`LogsController#find_by_id_or_token(identifier)` (Private)**
|
|
455
|
+
* *Function*: Dual-lookup resolver. If `identifier` consists purely of numeric digits or matches a UUID format, fetches via `UndoLog.find_by(id: identifier)`. For all other string formats, safely routes to `UndoLog.find_by_signed_token(identifier)` without triggering database type exceptions.
|
|
456
|
+
* **`LogsController#execute_restore_action` (Private)**
|
|
457
|
+
* *Function*: Wraps `@undo_log.restore!(whodunnit: resolve_whodunnit_user)` inside tenant context (`with_tenant_context`) to enforce multi-tenant security during restoration.
|
|
458
|
+
* **`RestoresController#create`**
|
|
459
|
+
* *Function*: Dedicated HTTP endpoint for signed token restorations (`POST /undo/restore/:token`). Inherits from `LogsController` and executes `#restore`.
|