-
-
Notifications
You must be signed in to change notification settings - Fork 916
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add avo MFA reset admin action & view of audit entries (#3426)
* Add avo MFA reset admin action & view of audit entries * Add a base action with more robust audit change tracking * Add test that covers the reset MFA action * Make gravatar smaller on show view * Sort fields in record diff * Make the audited changes the focus of the audit show component * Delete unused method * Add a test for base action error handling To goose code coverage metrics
- Loading branch information
Showing
22 changed files
with
500 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
class BaseAction < Avo::BaseAction | ||
field :comment, as: :textarea, required: true, | ||
help: "A comment explaining why this action was taken.<br>Will be saved in the audit log.<br>Must be more than 10 characters." | ||
|
||
class ActionHandler | ||
include ActiveSupport::Callbacks | ||
define_callbacks :handle, terminator: lambda { |target, result_lambda| | ||
result_lambda.call | ||
target.errored? | ||
} | ||
|
||
def initialize( # rubocop:disable Metrics/ParameterLists | ||
fields:, current_user:, arguments:, resource:, action:, models: nil | ||
) | ||
@models = models | ||
@fields = fields | ||
@current_user = current_user | ||
@arguments = arguments | ||
@resource = resource | ||
|
||
@action = action | ||
end | ||
|
||
attr_reader :models, :fields, :current_user, :arguments, :resource | ||
|
||
delegate :error, :avo, :keep_modal_open, :redirect_to, :inform, | ||
to: :@action | ||
|
||
set_callback :handle, :before do | ||
error "Must supply a sufficiently detailed comment" unless fields[:comment].presence&.then { _1.length >= 10 } | ||
end | ||
|
||
set_callback :handle, :around, lambda { |_, block| | ||
begin | ||
block.call | ||
rescue StandardError => e | ||
error e.message.truncate(300) | ||
end | ||
} | ||
|
||
def do_handle | ||
run_callbacks :handle do | ||
handle | ||
end | ||
keep_modal_open if errored? | ||
end | ||
|
||
def errored? | ||
@action.response[:messages].any? { _1[:type] == :error } | ||
end | ||
|
||
def in_audited_transaction(&) | ||
User.transaction do | ||
changed_records = {} | ||
ActiveSupport::Notifications.subscribed(proc do |_name, _started, _finished, _unique_id, data| | ||
data[:connection].transaction_manager.current_transaction.records.uniq(&:__id__).each do |record| | ||
(changed_records[record] ||= {}).merge!(record.changes_to_save) do |_key, (old, _), (_, new)| | ||
[old, new] | ||
end | ||
end | ||
end, "sql.active_record", &) | ||
|
||
audited_changed_records = changed_records.to_h do |record, changes| | ||
[ | ||
record.to_global_id.uri, | ||
{ changes:, unchanged: record.attributes.except(*changes.keys) } | ||
] | ||
end | ||
|
||
audit = Audit.create!( | ||
admin_github_user: current_user, | ||
auditable: @current_model, | ||
action: @action.name, | ||
comment: fields[:comment], | ||
audited_changes: { | ||
records: audited_changed_records, | ||
fields: fields.except(:comment), | ||
arguments: arguments, | ||
models: models.map { _1.to_global_id.uri } | ||
} | ||
) | ||
redirect_to avo.resources_audit_path(audit) | ||
end | ||
end | ||
|
||
def handle | ||
models.each do |model| | ||
@current_model = model | ||
in_audited_transaction do | ||
handle_model(model) | ||
end | ||
end | ||
@current_model = nil | ||
end | ||
end | ||
|
||
def handle(**args) | ||
"#{self.class}::ActionHandler" | ||
.constantize | ||
.new(**args, arguments:, action: self) | ||
.do_handle | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
class ResetUser2fa < BaseAction | ||
self.name = "Reset User 2FA" | ||
self.visible = lambda { | ||
current_user.team_member?("rubygems-org") && view == :show | ||
} | ||
|
||
self.message = lambda { | ||
"Are you sure you would like to disable MFA and reset the password for #{record.handle} #{record.email}?" | ||
} | ||
|
||
self.confirm_button_label = "Reset MFA" | ||
|
||
class ActionHandler < ActionHandler | ||
def handle_model(user) | ||
user.disable_mfa! | ||
user.password = SecureRandom.hex(20).encode("UTF-8") | ||
user.save! | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
class AuditedChangesField < Avo::Fields::BaseField | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
class JsonViewerField < Avo::Fields::BaseField | ||
def initialize(name, **args, &) | ||
super(name, **args, &) | ||
@theme = args[:theme].present? ? args[:theme].to_s : "default" | ||
@height = args[:height].present? ? args[:height].to_s : "auto" | ||
@tab_size = args[:tab_size].presence || 2 | ||
@indent_with_tabs = args[:indent_with_tabs].presence || false | ||
@line_wrapping = args[:line_wrapping].presence || true | ||
end | ||
|
||
attr_reader :height, :theme, :tab_size, :indent_with_tabs, :line_wrapping | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
class AuditResource < Avo::BaseResource | ||
self.title = :id | ||
self.includes = %i[ | ||
admin_github_user | ||
auditable | ||
] | ||
|
||
field :action, as: :text | ||
|
||
sidebar do | ||
field :admin_github_user, as: :belongs_to | ||
field :created_at, as: :date_time | ||
field :comment, as: :text | ||
|
||
field :auditable, as: :belongs_to, | ||
polymorphic_as: :auditable, | ||
types: [::User], | ||
name: "Edited Record" | ||
|
||
heading "Action Details" | ||
|
||
field :audited_changes_arguments, as: :json_viewer, only_on: :show do |model| | ||
model.audited_changes["arguments"] | ||
end | ||
field :audited_changes_fields, as: :json_viewer, only_on: :show do |model| | ||
model.audited_changes["fields"] | ||
end | ||
field :audited_changes_models, as: :text, as_html: true, only_on: :show do | ||
model.audited_changes["models"] | ||
end | ||
|
||
field :id, as: :id | ||
end | ||
|
||
field :audited_changes, as: :audited_changes | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
app/components/avo/audited_changes_record_diff/show_component.html.erb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<%= render Avo::PanelComponent.new(title: title_link, classes: %w[w-full]) do |c| %> | ||
<% c.body do %> | ||
<% next unless authorized? %> | ||
|
||
<div class="divide-y divide-dashed"> | ||
<% each_field do |type, component| %> | ||
<%= tag.div class: change_type_row_classes(type) do %> | ||
<span class="px-1 w-4"><%= change_type_icon type %></span> | ||
<%= render component %> | ||
<% end %> | ||
<% end %> | ||
</div> | ||
<% end %> | ||
<% end %> |
75 changes: 75 additions & 0 deletions
75
app/components/avo/audited_changes_record_diff/show_component.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
# frozen_string_literal: true | ||
|
||
class Avo::AuditedChangesRecordDiff::ShowComponent < ViewComponent::Base | ||
def initialize(gid:, changes:, unchanged:, user:) | ||
super | ||
@gid = gid | ||
@changes = changes | ||
@unchanged = unchanged | ||
@user = user | ||
|
||
model = GlobalID::Locator.locate(gid) | ||
@resource = Avo::App.get_resource_by_name(model.class.name).hydrate(model:, user:) | ||
|
||
@old_resource = resource.dup.hydrate(model: resource.model.class.new(**unchanged, **changes.transform_values(&:first))) | ||
@new_resource = resource.dup.hydrate(model: resource.model.class.new(**unchanged, **changes.transform_values(&:last))) | ||
end | ||
|
||
attr_reader :gid, :changes, :unchanged, :user, :resource, :old_resource, :new_resource | ||
|
||
def sorted_fields | ||
@resource.fields | ||
.reject { _1.is_a?(Avo::Fields::HasBaseField) } | ||
.sort_by.with_index { |f, i| [changes.key?(f.id.to_s) ? -1 : 1, i] } | ||
end | ||
|
||
def each_field | ||
sorted_fields.each do |field| | ||
unless field.visible? | ||
if changes.key?(field.id.to_s) | ||
# dummy field to avoid ever printing out the contents... we just want the label | ||
yield :changed, Avo::Fields::BooleanField::ShowComponent.new(field: field) | ||
end | ||
next | ||
end | ||
|
||
if changes.key?(field.id.to_s) | ||
yield :new, field.component_for_view(:show).new(field: field.hydrate(model: new_resource.model), resource: new_resource) | ||
yield :old, field.component_for_view(:show).new(field: field.hydrate(model: old_resource.model), resource: old_resource) | ||
else | ||
yield :unchanged, field.component_for_view(:show).new(field: field.hydrate(model: new_resource.model), resource: new_resource) | ||
end | ||
end | ||
end | ||
|
||
def authorized? | ||
Pundit.policy!(user, resource.model).avo_show? | ||
end | ||
|
||
def title_link | ||
link_to(resource.model_title, resource.record_path) | ||
end | ||
|
||
def change_type_icon(type) | ||
case type | ||
when :changed | ||
helpers.svg("arrows-right-left", class: %w[h-4]) | ||
when :new | ||
helpers.svg("forward", class: %w[h-4]) | ||
when :old | ||
helpers.svg("backward", class: %w[h-4]) | ||
end | ||
end | ||
|
||
def change_type_row_classes(type) | ||
case type | ||
when :changed | ||
%w[bg-orange-400] | ||
when :new | ||
%w[bg-green-500] | ||
when :old | ||
%w[bg-red-400] | ||
else [] | ||
end + %w[flex flex-row items-baseline] | ||
end | ||
end |
10 changes: 10 additions & 0 deletions
10
app/components/avo/fields/audited_changes_field/show_component.html.erb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<%= field_wrapper **field_wrapper_args, full_width: true do %> | ||
<% records&.each do |gid, changes, unchanged| %> | ||
<%= render Avo::AuditedChangesRecordDiff::ShowComponent.new( | ||
gid:, | ||
changes:, | ||
unchanged:, | ||
user: resource.user, | ||
) %> | ||
<% end %> | ||
<% end %> |
11 changes: 11 additions & 0 deletions
11
app/components/avo/fields/audited_changes_field/show_component.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# frozen_string_literal: true | ||
|
||
class Avo::Fields::AuditedChangesField::ShowComponent < Avo::Fields::ShowComponent | ||
def records | ||
field.value["records"]&.map do |gid, body| | ||
changes, unchanged = body.values_at("changes", "unchanged") | ||
|
||
[gid, changes, unchanged] | ||
end | ||
end | ||
end |
8 changes: 8 additions & 0 deletions
8
app/components/avo/fields/gravatar_field/show_component.html.erb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<%= field_wrapper **field_wrapper_args do %> | ||
<%= render Avo::Fields::Common::GravatarViewerComponent.new( | ||
md5: @field.md5, | ||
default: @field.default, | ||
size: 144, | ||
) | ||
%> | ||
<% end %> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# frozen_string_literal: true | ||
|
||
class Avo::Fields::GravatarField::ShowComponent < Avo::Fields::ShowComponent | ||
end |
19 changes: 19 additions & 0 deletions
19
app/components/avo/fields/json_viewer_field/show_component.html.erb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<%= field_wrapper **field_wrapper_args, full_width: true do %> | ||
<div data-controller="code-field" style="--height: <%= @field.height %>"> | ||
<%= text_area_tag @field.id, pretty_json, | ||
class: helpers.input_classes('w-full'), | ||
placeholder: @field.placeholder, | ||
disabled: true, | ||
data: { | ||
'code-field-target': 'element', | ||
view: view, | ||
language: :javascript, | ||
theme: @field.theme, | ||
'tab-size': @field.tab_size, | ||
'read-only': true, | ||
'indent-with-tabs': @field.indent_with_tabs, | ||
'line-wrapping': @field.line_wrapping, | ||
} | ||
%> | ||
</div> | ||
<% end %> |
7 changes: 7 additions & 0 deletions
7
app/components/avo/fields/json_viewer_field/show_component.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# frozen_string_literal: true | ||
|
||
class Avo::Fields::JsonViewerField::ShowComponent < Avo::Fields::ShowComponent | ||
def pretty_json | ||
JSON.pretty_generate(@field.value) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# This controller has been generated to enable Rails' resource routes. | ||
# More information on https://docs.avohq.io/2.0/controllers.html | ||
class Avo::AuditsController < Avo::ResourcesController | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
class AuditPolicy < ApplicationPolicy | ||
class Scope < Scope | ||
# NOTE: Be explicit about which records you allow access to! | ||
def resolve | ||
if rubygems_org_admin? | ||
scope.all | ||
else | ||
scope.where(admin_github_user: current_user) | ||
end | ||
end | ||
end | ||
|
||
def avo_index? | ||
true | ||
end | ||
|
||
def avo_show? | ||
true | ||
end | ||
end |
Oops, something went wrong.