super_admin 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 +7 -0
- data/README.md +216 -0
- data/Rakefile +30 -0
- data/app/assets/stylesheets/super_admin/application.css +15 -0
- data/app/assets/stylesheets/super_admin/tailwind.css +1 -0
- data/app/assets/stylesheets/super_admin/tailwind.source.css +25 -0
- data/app/controllers/super_admin/application_controller.rb +89 -0
- data/app/controllers/super_admin/associations_controller.rb +136 -0
- data/app/controllers/super_admin/audit_logs_controller.rb +39 -0
- data/app/controllers/super_admin/base_controller.rb +133 -0
- data/app/controllers/super_admin/dashboard_controller.rb +29 -0
- data/app/controllers/super_admin/exports_controller.rb +109 -0
- data/app/controllers/super_admin/resources_controller.rb +201 -0
- data/app/dashboards/super_admin/base_dashboard.rb +200 -0
- data/app/errors/super_admin/configuration_error.rb +6 -0
- data/app/helpers/super_admin/application_helper.rb +84 -0
- data/app/helpers/super_admin/exports_helper.rb +16 -0
- data/app/helpers/super_admin/resources_helper.rb +204 -0
- data/app/helpers/super_admin/route_helper.rb +7 -0
- data/app/javascript/super_admin/application.js +263 -0
- data/app/jobs/super_admin/application_job.rb +4 -0
- data/app/jobs/super_admin/generate_super_admin_csv_export_job.rb +100 -0
- data/app/mailers/super_admin/application_mailer.rb +6 -0
- data/app/models/super_admin/application_record.rb +5 -0
- data/app/models/super_admin/audit_log.rb +35 -0
- data/app/models/super_admin/csv_export.rb +67 -0
- data/app/services/super_admin/auditing.rb +74 -0
- data/app/services/super_admin/authorization.rb +113 -0
- data/app/services/super_admin/authorization_adapters/base_adapter.rb +100 -0
- data/app/services/super_admin/authorization_adapters/default_adapter.rb +77 -0
- data/app/services/super_admin/authorization_adapters/proc_adapter.rb +65 -0
- data/app/services/super_admin/authorization_adapters/pundit_adapter.rb +81 -0
- data/app/services/super_admin/csv_export_creator.rb +45 -0
- data/app/services/super_admin/dashboard_registry.rb +90 -0
- data/app/services/super_admin/dashboard_resolver.rb +100 -0
- data/app/services/super_admin/filter_builder.rb +185 -0
- data/app/services/super_admin/form_builder.rb +59 -0
- data/app/services/super_admin/form_fields/array_field.rb +35 -0
- data/app/services/super_admin/form_fields/association_field.rb +146 -0
- data/app/services/super_admin/form_fields/base_field.rb +53 -0
- data/app/services/super_admin/form_fields/boolean_field.rb +29 -0
- data/app/services/super_admin/form_fields/date_field.rb +15 -0
- data/app/services/super_admin/form_fields/date_time_field.rb +15 -0
- data/app/services/super_admin/form_fields/enum_field.rb +27 -0
- data/app/services/super_admin/form_fields/factory.rb +102 -0
- data/app/services/super_admin/form_fields/nested_field.rb +120 -0
- data/app/services/super_admin/form_fields/number_field.rb +29 -0
- data/app/services/super_admin/form_fields/text_area_field.rb +19 -0
- data/app/services/super_admin/model_inspector.rb +182 -0
- data/app/services/super_admin/queries/base_query.rb +45 -0
- data/app/services/super_admin/queries/filter_query.rb +188 -0
- data/app/services/super_admin/queries/resource_scope_query.rb +74 -0
- data/app/services/super_admin/queries/search_query.rb +146 -0
- data/app/services/super_admin/queries/sort_query.rb +41 -0
- data/app/services/super_admin/resource_configuration.rb +63 -0
- data/app/services/super_admin/resource_exporter.rb +78 -0
- data/app/services/super_admin/resource_query.rb +40 -0
- data/app/services/super_admin/resources/association_inspector.rb +112 -0
- data/app/services/super_admin/resources/collection_presenter.rb +63 -0
- data/app/services/super_admin/resources/context.rb +63 -0
- data/app/services/super_admin/resources/filter_params.rb +29 -0
- data/app/services/super_admin/resources/permitted_attributes.rb +104 -0
- data/app/services/super_admin/resources/value_normalizer.rb +121 -0
- data/app/services/super_admin/sensitive_attributes.rb +166 -0
- data/app/views/layouts/super_admin.html.erb +74 -0
- data/app/views/super_admin/audit_logs/index.html.erb +143 -0
- data/app/views/super_admin/dashboard/index.html.erb +79 -0
- data/app/views/super_admin/exports/index.html.erb +84 -0
- data/app/views/super_admin/exports/show.html.erb +57 -0
- data/app/views/super_admin/resources/_form.html.erb +42 -0
- data/app/views/super_admin/resources/destroy.turbo_stream.erb +17 -0
- data/app/views/super_admin/resources/edit.html.erb +37 -0
- data/app/views/super_admin/resources/index.html.erb +189 -0
- data/app/views/super_admin/resources/new.html.erb +31 -0
- data/app/views/super_admin/resources/show.html.erb +106 -0
- data/app/views/super_admin/shared/_breadcrumbs.html.erb +12 -0
- data/app/views/super_admin/shared/_custom_styles.html.erb +132 -0
- data/app/views/super_admin/shared/_flash.html.erb +55 -0
- data/app/views/super_admin/shared/_form_field.html.erb +35 -0
- data/app/views/super_admin/shared/_navigation.html.erb +92 -0
- data/app/views/super_admin/shared/_nested_fields.html.erb +59 -0
- data/app/views/super_admin/shared/_nested_record_fields.html.erb +45 -0
- data/config/importmap.rb +4 -0
- data/config/initializers/rack_attack.rb +134 -0
- data/config/initializers/super_admin.rb +117 -0
- data/config/locales/super_admin.en.yml +197 -0
- data/config/locales/super_admin.fr.yml +197 -0
- data/config/routes.rb +22 -0
- data/lib/generators/super_admin/dashboard_generator.rb +50 -0
- data/lib/generators/super_admin/install_generator.rb +58 -0
- data/lib/generators/super_admin/templates/20240101000001_create_super_admin_audit_logs.rb +24 -0
- data/lib/generators/super_admin/templates/20240101000002_create_super_admin_csv_exports.rb +33 -0
- data/lib/generators/super_admin/templates/super_admin.rb +58 -0
- data/lib/super_admin/dashboard_creator.rb +256 -0
- data/lib/super_admin/engine.rb +53 -0
- data/lib/super_admin/install_task.rb +96 -0
- data/lib/super_admin/version.rb +3 -0
- data/lib/super_admin.rb +7 -0
- data/lib/tasks/super_admin_tasks.rake +38 -0
- metadata +239 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SuperAdmin
|
|
4
|
+
# Persists a trace of meaningful operations executed through SuperAdmin.
|
|
5
|
+
class AuditLog < ApplicationRecord
|
|
6
|
+
self.table_name = "super_admin_audit_logs"
|
|
7
|
+
|
|
8
|
+
belongs_to :user,
|
|
9
|
+
class_name: SuperAdmin.configuration.user_class_constant.name,
|
|
10
|
+
optional: true
|
|
11
|
+
|
|
12
|
+
validates :resource_type, :action, :performed_at, presence: true
|
|
13
|
+
|
|
14
|
+
scope :recent, -> { order(performed_at: :desc) }
|
|
15
|
+
|
|
16
|
+
before_validation :default_performed_at
|
|
17
|
+
before_validation :default_payloads
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def default_performed_at
|
|
22
|
+
return if performed_at.present?
|
|
23
|
+
|
|
24
|
+
# When required attributes are missing, keep the field blank so validation errors surface.
|
|
25
|
+
return if resource_type.blank? || action.blank?
|
|
26
|
+
|
|
27
|
+
self.performed_at = Time.current
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def default_payloads
|
|
31
|
+
self.changes_snapshot ||= {}
|
|
32
|
+
self.context ||= {}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SuperAdmin
|
|
4
|
+
class CsvExport < ApplicationRecord
|
|
5
|
+
self.table_name = "super_admin_csv_exports"
|
|
6
|
+
|
|
7
|
+
RETENTION_PERIOD = 7.days unless const_defined?(:RETENTION_PERIOD)
|
|
8
|
+
|
|
9
|
+
belongs_to :user
|
|
10
|
+
|
|
11
|
+
has_one_attached :file
|
|
12
|
+
|
|
13
|
+
enum :status, {
|
|
14
|
+
pending: "pending",
|
|
15
|
+
processing: "processing",
|
|
16
|
+
ready: "ready",
|
|
17
|
+
failed: "failed"
|
|
18
|
+
}, suffix: true
|
|
19
|
+
|
|
20
|
+
validates :resource_name, :model_class_name, :status, :token, presence: true
|
|
21
|
+
validates :token, uniqueness: true
|
|
22
|
+
|
|
23
|
+
before_validation :generate_token, on: :create
|
|
24
|
+
|
|
25
|
+
scope :recent_first, -> { order(created_at: :desc) }
|
|
26
|
+
scope :active, -> { where("expires_at IS NULL OR expires_at > ?", Time.current) }
|
|
27
|
+
|
|
28
|
+
def ready_for_download?
|
|
29
|
+
ready_status? && file.attached?
|
|
30
|
+
rescue NoMethodError, ActiveRecord::StatementInvalid
|
|
31
|
+
# Handle cases where ActiveStorage is unavailable or not initialized
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def mark_processing!
|
|
36
|
+
update!(status: :processing, started_at: Time.current)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def mark_ready!(records_count:)
|
|
40
|
+
update!(
|
|
41
|
+
status: :ready,
|
|
42
|
+
records_count: records_count,
|
|
43
|
+
completed_at: Time.current,
|
|
44
|
+
expires_at: RETENTION_PERIOD.from_now
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def mark_failed!(error_message)
|
|
49
|
+
update!(
|
|
50
|
+
status: :failed,
|
|
51
|
+
error_message: error_message.to_s.truncate(500),
|
|
52
|
+
completed_at: Time.current
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# Generate a secure random token. The uniqueness validation and database unique index
|
|
59
|
+
# will prevent race conditions. With 24 bytes (192 bits), the collision probability
|
|
60
|
+
# is astronomically low (~1 in 10^57 for billions of records).
|
|
61
|
+
def generate_token
|
|
62
|
+
return if token.present?
|
|
63
|
+
|
|
64
|
+
self.token = SecureRandom.urlsafe_base64(24)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SuperAdmin
|
|
4
|
+
# Lightweight helper that records meaningful user actions inside SuperAdmin.
|
|
5
|
+
module Auditing
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def log!(user:, resource: nil, resource_type: nil, resource_id: nil, action:, changes: nil, context: {})
|
|
9
|
+
resource_type ||= resource&.class&.name
|
|
10
|
+
resource_id ||= resource&.try(:id)&.to_s
|
|
11
|
+
|
|
12
|
+
return if resource_type.blank? || resource_type == "SuperAdmin::AuditLog"
|
|
13
|
+
|
|
14
|
+
SuperAdmin::AuditLog.create(
|
|
15
|
+
user: compatible_user(user),
|
|
16
|
+
user_email: safe_user_email(user),
|
|
17
|
+
resource_type: resource_type,
|
|
18
|
+
resource_id: resource_id,
|
|
19
|
+
action: action.to_s,
|
|
20
|
+
changes_snapshot: prepare_changes(resource, action, changes),
|
|
21
|
+
context: context.presence || {},
|
|
22
|
+
performed_at: Time.current
|
|
23
|
+
)
|
|
24
|
+
rescue StandardError => error
|
|
25
|
+
Rails.logger.error(
|
|
26
|
+
"[SuperAdmin::Auditing] Failed to log action #{action} on #{resource.class.name}: #{error.class} - #{error.message}"
|
|
27
|
+
)
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def compatible_user(user)
|
|
32
|
+
return nil unless user
|
|
33
|
+
|
|
34
|
+
user_class = SuperAdmin.configuration.user_class_constant
|
|
35
|
+
return user if user.is_a?(user_class)
|
|
36
|
+
|
|
37
|
+
nil
|
|
38
|
+
rescue SuperAdmin::ConfigurationError
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def safe_user_email(user)
|
|
43
|
+
user.respond_to?(:email) ? user.email : nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def prepare_changes(resource, action, changes)
|
|
47
|
+
payload = if changes.present?
|
|
48
|
+
sanitize_changes(changes)
|
|
49
|
+
else
|
|
50
|
+
resource ? default_changes(resource, action) : {}
|
|
51
|
+
end
|
|
52
|
+
payload.presence || {}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def default_changes(resource, action)
|
|
56
|
+
case action.to_s
|
|
57
|
+
when "create"
|
|
58
|
+
{ "after" => resource.attributes }
|
|
59
|
+
when "update"
|
|
60
|
+
sanitize_changes(resource.previous_changes)
|
|
61
|
+
when "destroy"
|
|
62
|
+
{ "before" => resource.attributes }
|
|
63
|
+
else
|
|
64
|
+
{}
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def sanitize_changes(changes)
|
|
69
|
+
return {} unless changes
|
|
70
|
+
|
|
71
|
+
changes.except("updated_at")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SuperAdmin
|
|
4
|
+
# Authorization orchestrator. Selects the appropriate adapter and executes it.
|
|
5
|
+
class Authorization
|
|
6
|
+
class NotAuthorizedError < StandardError; end
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def call(controller)
|
|
10
|
+
adapter = build_adapter(controller)
|
|
11
|
+
return true if adapter.authorized?
|
|
12
|
+
|
|
13
|
+
handle_unauthorized(controller, adapter)
|
|
14
|
+
false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build_adapter(controller)
|
|
18
|
+
resolve_adapter(controller).new(controller)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def resolve_adapter(controller)
|
|
24
|
+
adapter_option = SuperAdmin.configuration.authorization_adapter
|
|
25
|
+
|
|
26
|
+
case adapter_option
|
|
27
|
+
when nil, :auto
|
|
28
|
+
auto_detect_adapter(controller)
|
|
29
|
+
when :default
|
|
30
|
+
config = SuperAdmin.configuration
|
|
31
|
+
config.authorize_with.present? ? AuthorizationAdapters::ProcAdapter : AuthorizationAdapters::DefaultAdapter
|
|
32
|
+
when Symbol, String
|
|
33
|
+
adapter_from_name(adapter_option)
|
|
34
|
+
when Class
|
|
35
|
+
adapter_option
|
|
36
|
+
else
|
|
37
|
+
AuthorizationAdapters::DefaultAdapter
|
|
38
|
+
end
|
|
39
|
+
rescue NameError => e
|
|
40
|
+
Rails.logger.error("[SuperAdmin] Authorization adapter resolution failed: #{e.message}")
|
|
41
|
+
AuthorizationAdapters::DefaultAdapter
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def auto_detect_adapter(controller)
|
|
45
|
+
config = SuperAdmin.configuration
|
|
46
|
+
|
|
47
|
+
return AuthorizationAdapters::ProcAdapter if config.authorize_with.present?
|
|
48
|
+
|
|
49
|
+
if defined?(::Pundit) && controller.respond_to?(:authorize, true)
|
|
50
|
+
return AuthorizationAdapters::PunditAdapter
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if defined?(::CanCan::Ability) && controller.respond_to?(:authorize!, true)
|
|
54
|
+
return AuthorizationAdapters::CancanAdapter
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
AuthorizationAdapters::DefaultAdapter
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def adapter_from_name(name)
|
|
61
|
+
key = name.to_sym
|
|
62
|
+
|
|
63
|
+
case key
|
|
64
|
+
when :pundit
|
|
65
|
+
AuthorizationAdapters::PunditAdapter
|
|
66
|
+
when :cancan, :cancancan
|
|
67
|
+
AuthorizationAdapters::CancanAdapter
|
|
68
|
+
when :proc
|
|
69
|
+
AuthorizationAdapters::ProcAdapter
|
|
70
|
+
when :default
|
|
71
|
+
AuthorizationAdapters::DefaultAdapter
|
|
72
|
+
else
|
|
73
|
+
"SuperAdmin::AuthorizationAdapters::#{key.to_s.camelize}Adapter".constantize
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def handle_unauthorized(controller, adapter)
|
|
78
|
+
handler = SuperAdmin.configuration.on_unauthorized
|
|
79
|
+
error = adapter.build_error
|
|
80
|
+
|
|
81
|
+
if handler
|
|
82
|
+
invoke_handler(controller, error, handler)
|
|
83
|
+
else
|
|
84
|
+
adapter.handle_unauthorized!(error)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
error
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def invoke_handler(controller, error, handler)
|
|
91
|
+
case handler
|
|
92
|
+
when Proc
|
|
93
|
+
args = case handler.arity
|
|
94
|
+
when 0
|
|
95
|
+
[]
|
|
96
|
+
when 1
|
|
97
|
+
[ error ]
|
|
98
|
+
else
|
|
99
|
+
[ controller, error ]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
controller.instance_exec(*args, &handler)
|
|
103
|
+
else
|
|
104
|
+
if controller.respond_to?(handler)
|
|
105
|
+
controller.public_send(handler, error)
|
|
106
|
+
else
|
|
107
|
+
raise ConfigurationError, "Unauthorized handler '#{handler}' is not defined"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SuperAdmin
|
|
4
|
+
module AuthorizationAdapters
|
|
5
|
+
# Base class for authorization adapters. Provides helpers shared by all adapters.
|
|
6
|
+
class BaseAdapter
|
|
7
|
+
attr_reader :controller, :last_exception
|
|
8
|
+
|
|
9
|
+
def initialize(controller)
|
|
10
|
+
@controller = controller
|
|
11
|
+
@last_exception = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def authorized?(resource = nil)
|
|
15
|
+
raise NotImplementedError, "Adapters must implement #authorized?"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def authorize(resource = nil)
|
|
19
|
+
@last_exception = nil
|
|
20
|
+
|
|
21
|
+
result = invoke_authorized_check(resource)
|
|
22
|
+
!!result
|
|
23
|
+
rescue SuperAdmin::Authorization::NotAuthorizedError => error
|
|
24
|
+
remember_failure(error)
|
|
25
|
+
false
|
|
26
|
+
rescue StandardError => error
|
|
27
|
+
remember_failure(error)
|
|
28
|
+
raise
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def authorized_scope(scope)
|
|
32
|
+
scope
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Default unauthorized handler simply raises the provided error. Adapters can override.
|
|
36
|
+
def handle_unauthorized!(error)
|
|
37
|
+
raise(error)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build_error
|
|
41
|
+
SuperAdmin::Authorization::NotAuthorizedError.new(default_error_message)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
protected
|
|
45
|
+
|
|
46
|
+
def remember_failure(exception = nil)
|
|
47
|
+
@last_exception = exception
|
|
48
|
+
false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def invoke_authorized_check(resource)
|
|
52
|
+
method = method(:authorized?)
|
|
53
|
+
|
|
54
|
+
if method.arity.zero?
|
|
55
|
+
authorized?
|
|
56
|
+
else
|
|
57
|
+
authorized?(resource)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def current_user
|
|
62
|
+
strategy = SuperAdmin.configuration.current_user_method || :current_user
|
|
63
|
+
|
|
64
|
+
case strategy
|
|
65
|
+
when Proc
|
|
66
|
+
controller.instance_exec(&strategy)
|
|
67
|
+
when Symbol, String
|
|
68
|
+
method_name = strategy.to_sym
|
|
69
|
+
return unless controller.respond_to?(method_name, true)
|
|
70
|
+
|
|
71
|
+
controller.__send__(method_name)
|
|
72
|
+
else
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
rescue NoMethodError
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def default_error_message
|
|
80
|
+
I18n.t("super_admin.flash.access_denied", default: "You do not have permission to access this section.")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def redirect_with_alert!(message)
|
|
84
|
+
return unless controller.respond_to?(:redirect_to)
|
|
85
|
+
|
|
86
|
+
if controller.respond_to?(:flash)
|
|
87
|
+
controller.flash[:alert] ||= message
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
target = if controller.respond_to?(:main_app) && controller.main_app.respond_to?(:root_path)
|
|
91
|
+
controller.main_app.root_path
|
|
92
|
+
else
|
|
93
|
+
"/"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
controller.redirect_to(target)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SuperAdmin
|
|
4
|
+
module AuthorizationAdapters
|
|
5
|
+
# Fallback authorization strategy relying on configured super admin predicate.
|
|
6
|
+
class DefaultAdapter < BaseAdapter
|
|
7
|
+
def authorized?(resource = nil)
|
|
8
|
+
checker = SuperAdmin.configuration.super_admin_check
|
|
9
|
+
|
|
10
|
+
if checker
|
|
11
|
+
user = current_user || resource
|
|
12
|
+
raise SuperAdmin::ConfigurationError, "Configure a #current_user method or provide a user resource" unless user
|
|
13
|
+
|
|
14
|
+
!!evaluate_custom_check(user, resource)
|
|
15
|
+
else
|
|
16
|
+
user = current_user || resource
|
|
17
|
+
|
|
18
|
+
if user && (user.respond_to?(:super_admin?) || user.respond_to?(:admin?))
|
|
19
|
+
!!evaluate_standard_predicate(user)
|
|
20
|
+
else
|
|
21
|
+
true
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
rescue SuperAdmin::ConfigurationError => error
|
|
25
|
+
raise error
|
|
26
|
+
rescue StandardError => error
|
|
27
|
+
raise SuperAdmin::ConfigurationError, "SuperAdmin authorization failed: #{error.message}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def handle_unauthorized!(error)
|
|
31
|
+
redirect_with_alert!(error.message)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def evaluate_custom_check(user, resource)
|
|
37
|
+
checker = SuperAdmin.configuration.super_admin_check
|
|
38
|
+
|
|
39
|
+
case checker
|
|
40
|
+
when Proc
|
|
41
|
+
case checker.arity
|
|
42
|
+
when 0
|
|
43
|
+
controller.instance_exec(&checker)
|
|
44
|
+
when 1
|
|
45
|
+
controller.instance_exec(user, &checker)
|
|
46
|
+
when 2
|
|
47
|
+
controller.instance_exec(controller, user, &checker)
|
|
48
|
+
else
|
|
49
|
+
controller.instance_exec(controller, user, resource, &checker)
|
|
50
|
+
end
|
|
51
|
+
when Symbol, String
|
|
52
|
+
predicate = checker.to_sym
|
|
53
|
+
|
|
54
|
+
if user.respond_to?(predicate)
|
|
55
|
+
user.public_send(predicate)
|
|
56
|
+
elsif controller.respond_to?(predicate, true)
|
|
57
|
+
controller.__send__(predicate, user)
|
|
58
|
+
else
|
|
59
|
+
raise SuperAdmin::ConfigurationError, "SuperAdmin predicate '#{predicate}' is not defined"
|
|
60
|
+
end
|
|
61
|
+
else
|
|
62
|
+
raise SuperAdmin::ConfigurationError, "Unsupported super_admin_check: #{checker.inspect}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def evaluate_standard_predicate(user)
|
|
67
|
+
if user.respond_to?(:super_admin?)
|
|
68
|
+
user.super_admin?
|
|
69
|
+
elsif user.respond_to?(:admin?)
|
|
70
|
+
user.admin?
|
|
71
|
+
else
|
|
72
|
+
raise SuperAdmin::ConfigurationError, "Define SuperAdmin.super_admin_check or add #super_admin?/#admin? predicate to the user"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SuperAdmin
|
|
4
|
+
module AuthorizationAdapters
|
|
5
|
+
# Adapter executing a custom callable configured via SuperAdmin.configure { |c| c.authorize_with = ... }.
|
|
6
|
+
class ProcAdapter < BaseAdapter
|
|
7
|
+
def authorized?(resource = nil)
|
|
8
|
+
callable = SuperAdmin.configuration.authorize_with
|
|
9
|
+
raise SuperAdmin::ConfigurationError, "authorize_with must be a callable or method name" if callable.blank?
|
|
10
|
+
|
|
11
|
+
result = case callable
|
|
12
|
+
when Proc
|
|
13
|
+
invoke_proc(callable, resource)
|
|
14
|
+
when Symbol, String
|
|
15
|
+
invoke_method(callable.to_sym)
|
|
16
|
+
else
|
|
17
|
+
raise SuperAdmin::ConfigurationError, "Unsupported authorize_with value: #{callable.inspect}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
!!result
|
|
21
|
+
rescue SuperAdmin::ConfigurationError
|
|
22
|
+
raise
|
|
23
|
+
rescue StandardError => exception
|
|
24
|
+
remember_failure(exception)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def build_error
|
|
28
|
+
message = if last_exception&.respond_to?(:message) && last_exception.message.present?
|
|
29
|
+
last_exception.message
|
|
30
|
+
else
|
|
31
|
+
default_error_message
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
SuperAdmin::Authorization::NotAuthorizedError.new(message)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def handle_unauthorized!(error)
|
|
38
|
+
redirect_with_alert!(error.message)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def invoke_proc(callable, resource)
|
|
44
|
+
positional_count = callable.parameters.count { |type, _| %i[req opt].include?(type) }
|
|
45
|
+
rest_parameter = callable.parameters.any? { |type, _| type == :rest }
|
|
46
|
+
|
|
47
|
+
base_arguments = [ controller, resource, current_user ]
|
|
48
|
+
args = base_arguments.first(positional_count)
|
|
49
|
+
args += base_arguments.drop(positional_count) if rest_parameter
|
|
50
|
+
|
|
51
|
+
controller.instance_exec(*args, &callable)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def invoke_method(method_name)
|
|
55
|
+
if controller.respond_to?(method_name, true)
|
|
56
|
+
controller.__send__(method_name)
|
|
57
|
+
elsif current_user&.respond_to?(method_name)
|
|
58
|
+
current_user.public_send(method_name)
|
|
59
|
+
else
|
|
60
|
+
raise SuperAdmin::ConfigurationError, "authorize_with method '#{method_name}' is not defined"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SuperAdmin
|
|
4
|
+
module AuthorizationAdapters
|
|
5
|
+
# Adapter delegating authorization to Pundit.
|
|
6
|
+
class PunditAdapter < BaseAdapter
|
|
7
|
+
def authorized?(resource = nil)
|
|
8
|
+
ensure_pundit_available!
|
|
9
|
+
subject = resource || :super_admin
|
|
10
|
+
controller.__send__(:authorize, subject, :access?)
|
|
11
|
+
true
|
|
12
|
+
rescue LoadError => exception
|
|
13
|
+
raise SuperAdmin::ConfigurationError, missing_policy_message(exception)
|
|
14
|
+
rescue StandardError => exception
|
|
15
|
+
if pundit_not_authorized?(exception)
|
|
16
|
+
return remember_failure(exception)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if policy_missing?(exception)
|
|
20
|
+
raise SuperAdmin::ConfigurationError, missing_policy_message(exception)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
raise
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def authorized_scope(scope)
|
|
27
|
+
ensure_pundit_available!
|
|
28
|
+
return scope unless controller.respond_to?(:policy_scope)
|
|
29
|
+
|
|
30
|
+
controller.policy_scope(scope)
|
|
31
|
+
rescue LoadError => exception
|
|
32
|
+
raise SuperAdmin::ConfigurationError, missing_policy_message(exception)
|
|
33
|
+
rescue StandardError => exception
|
|
34
|
+
if policy_missing?(exception)
|
|
35
|
+
raise SuperAdmin::ConfigurationError, missing_policy_message(exception)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
raise
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def build_error
|
|
42
|
+
error = SuperAdmin::Authorization::NotAuthorizedError.new(default_error_message)
|
|
43
|
+
attach_cause(error)
|
|
44
|
+
error
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def handle_unauthorized!(error)
|
|
48
|
+
redirect_with_alert!(error.message)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def ensure_pundit_available!
|
|
54
|
+
return if defined?(::Pundit)
|
|
55
|
+
return if controller.respond_to?(:authorize)
|
|
56
|
+
|
|
57
|
+
raise SuperAdmin::ConfigurationError, "Pundit adapter selected but Pundit is not loaded"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def pundit_not_authorized?(exception)
|
|
61
|
+
defined?(::Pundit::NotAuthorizedError) && exception.is_a?(::Pundit::NotAuthorizedError)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def attach_cause(error)
|
|
65
|
+
return unless last_exception
|
|
66
|
+
|
|
67
|
+
cause = last_exception
|
|
68
|
+
error.define_singleton_method(:cause) { cause }
|
|
69
|
+
error.set_backtrace(cause.backtrace) if cause.backtrace
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def missing_policy_message(exception)
|
|
73
|
+
"SuperAdminPolicy with #access? must exist when using the Pundit adapter. #{exception.message}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def policy_missing?(exception)
|
|
77
|
+
defined?(::Pundit::NotDefinedError) && exception.is_a?(::Pundit::NotDefinedError)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SuperAdmin
|
|
4
|
+
# Encapsulates CSV export creation logic and enqueues it in Solid Queue.
|
|
5
|
+
class CsvExportCreator
|
|
6
|
+
DEFAULT_RETENTION = 7.days
|
|
7
|
+
|
|
8
|
+
def self.call(**kwargs)
|
|
9
|
+
new(**kwargs).call
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(user:, model_class:, resource_name: nil, resource: nil, scope: nil, search: nil, sort: nil, direction: nil, filters: {}, attributes: [])
|
|
13
|
+
@user = user
|
|
14
|
+
@model_class = model_class
|
|
15
|
+
@resource_name = (resource || resource_name || model_class.model_name.plural).to_s
|
|
16
|
+
@scope = scope || model_class.all
|
|
17
|
+
@search = search
|
|
18
|
+
@sort = sort
|
|
19
|
+
@direction = direction
|
|
20
|
+
@filters = filters || {}
|
|
21
|
+
@attributes = Array(attributes).map(&:to_s)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call
|
|
25
|
+
export = @user.csv_exports.create!(
|
|
26
|
+
model_class_name: @model_class.name,
|
|
27
|
+
resource_name: @resource_name,
|
|
28
|
+
search: @search,
|
|
29
|
+
sort: @sort,
|
|
30
|
+
direction: @direction,
|
|
31
|
+
filters: @filters,
|
|
32
|
+
selected_attributes: @attributes,
|
|
33
|
+
expires_at: DEFAULT_RETENTION.from_now
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
SuperAdmin::GenerateSuperAdminCsvExportJob.perform_later(
|
|
37
|
+
export_id: export.id,
|
|
38
|
+
model_class_name: @model_class.name,
|
|
39
|
+
attributes: @attributes
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
export
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|