rails_base 0.51.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/MIT-LICENSE +20 -0
- data/README.md +32 -0
- data/Rakefile +32 -0
- data/app/assets/config/rails_base/manifest.js +3 -0
- data/app/assets/images/rails_base/favicon.ico +0 -0
- data/app/assets/javascripts/rails_base/admin.js +2 -0
- data/app/assets/javascripts/rails_base/application.js +22 -0
- data/app/assets/javascripts/rails_base/cable.js +13 -0
- data/app/assets/javascripts/rails_base/mfa_auth.coffee +3 -0
- data/app/assets/javascripts/rails_base/secondary_authentication.coffee +3 -0
- data/app/assets/javascripts/rails_base/sessions.js +152 -0
- data/app/assets/javascripts/rails_base/user_settings.coffee +3 -0
- data/app/assets/stylesheets/rails_base/admin.css +4 -0
- data/app/assets/stylesheets/rails_base/application.scss +15 -0
- data/app/assets/stylesheets/rails_base/mfa_auth.scss +3 -0
- data/app/assets/stylesheets/rails_base/scaffolds.scss +84 -0
- data/app/assets/stylesheets/rails_base/secondary_authentication.scss +3 -0
- data/app/assets/stylesheets/rails_base/user_settings.scss +3 -0
- data/app/controllers/rails_base/admin_controller.rb +315 -0
- data/app/controllers/rails_base/application_controller.rb +153 -0
- data/app/controllers/rails_base/errors_controller.rb +29 -0
- data/app/controllers/rails_base/mfa_auth_controller.rb +50 -0
- data/app/controllers/rails_base/secondary_authentication_controller.rb +224 -0
- data/app/controllers/rails_base/switch_user_controller.rb +29 -0
- data/app/controllers/rails_base/user_settings_controller.rb +81 -0
- data/app/controllers/rails_base/users/passwords_controller.rb +19 -0
- data/app/controllers/rails_base/users/registrations_controller.rb +80 -0
- data/app/controllers/rails_base/users/sessions_controller.rb +108 -0
- data/app/helpers/rails_base/admin_helper.rb +107 -0
- data/app/helpers/rails_base/appearance_helper.rb +58 -0
- data/app/helpers/rails_base/application_helper.rb +26 -0
- data/app/helpers/rails_base/capture_reference_helper.rb +57 -0
- data/app/helpers/rails_base/mfa_auth_helper.rb +2 -0
- data/app/helpers/rails_base/secondary_authentication_helper.rb +2 -0
- data/app/helpers/rails_base/user_field_validators.rb +108 -0
- data/app/helpers/rails_base/user_settings_helper.rb +22 -0
- data/app/jobs/rails_base/application_job.rb +10 -0
- data/app/jobs/twilio_job.rb +9 -0
- data/app/mailers/rails_base/application_mailer.rb +9 -0
- data/app/mailers/rails_base/email_verification_mailer.rb +22 -0
- data/app/mailers/rails_base/event_mailer.rb +16 -0
- data/app/models/admin_action.rb +119 -0
- data/app/models/rails_base/application_record.rb +22 -0
- data/app/models/rails_base/user_constants.rb +28 -0
- data/app/models/secret.rb +37 -0
- data/app/models/short_lived_data.rb +132 -0
- data/app/models/user.rb +143 -0
- data/app/services/rails_base/admin_risky_mfa_send.rb +80 -0
- data/app/services/rails_base/admin_update_attribute.rb +100 -0
- data/app/services/rails_base/authentication/authenticate_user.rb +28 -0
- data/app/services/rails_base/authentication/constants.rb +60 -0
- data/app/services/rails_base/authentication/decision_twofa_type.rb +76 -0
- data/app/services/rails_base/authentication/destroy_user.rb +45 -0
- data/app/services/rails_base/authentication/mfa_set_encrypt_token.rb +32 -0
- data/app/services/rails_base/authentication/mfa_validator.rb +88 -0
- data/app/services/rails_base/authentication/modify_password.rb +67 -0
- data/app/services/rails_base/authentication/send_forgot_password.rb +26 -0
- data/app/services/rails_base/authentication/send_login_mfa_to_user.rb +77 -0
- data/app/services/rails_base/authentication/send_verification_email.rb +103 -0
- data/app/services/rails_base/authentication/session_token_verifier.rb +31 -0
- data/app/services/rails_base/authentication/single_sign_on_create.rb +44 -0
- data/app/services/rails_base/authentication/single_sign_on_send.rb +101 -0
- data/app/services/rails_base/authentication/single_sign_on_verify.rb +42 -0
- data/app/services/rails_base/authentication/sso_verify_email.rb +43 -0
- data/app/services/rails_base/authentication/update_phone_send_verification.rb +46 -0
- data/app/services/rails_base/authentication/verify_forgot_password.rb +46 -0
- data/app/services/rails_base/email_change.rb +20 -0
- data/app/services/rails_base/encryption.rb +87 -0
- data/app/services/rails_base/name_change.rb +71 -0
- data/app/services/rails_base/service_base.rb +65 -0
- data/app/services/rails_base/service_logging.rb +23 -0
- data/app/views/layouts/rails_base/application.html.erb +185 -0
- data/app/views/layouts/rails_base/mailer.html.erb +13 -0
- data/app/views/layouts/rails_base/mailer.text.erb +1 -0
- data/app/views/new.html.erb +4 -0
- data/app/views/rails_base/admin/history.html.erb +26 -0
- data/app/views/rails_base/admin/index.html.erb +149 -0
- data/app/views/rails_base/admin/show_config.html.erb +18 -0
- data/app/views/rails_base/devise/confirmations/new.html.erb +16 -0
- data/app/views/rails_base/devise/mailer/confirmation_instructions.html.erb +5 -0
- data/app/views/rails_base/devise/mailer/email_changed.html.erb +7 -0
- data/app/views/rails_base/devise/mailer/password_change.html.erb +3 -0
- data/app/views/rails_base/devise/mailer/reset_password_instructions.html.erb +8 -0
- data/app/views/rails_base/devise/mailer/unlock_instructions.html.erb +7 -0
- data/app/views/rails_base/devise/passwords/edit.html.erb +25 -0
- data/app/views/rails_base/devise/passwords/new.html.erb +27 -0
- data/app/views/rails_base/devise/registrations/edit.html.erb +43 -0
- data/app/views/rails_base/devise/registrations/new.html.erb +123 -0
- data/app/views/rails_base/devise/sessions/new.html.erb +4 -0
- data/app/views/rails_base/devise/shared/_error_messages.html.erb +15 -0
- data/app/views/rails_base/devise/shared/_links.html.erb +25 -0
- data/app/views/rails_base/devise/unlocks/new.html.erb +16 -0
- data/app/views/rails_base/email_verification_mailer/email_verification.html.erb +25 -0
- data/app/views/rails_base/email_verification_mailer/event.html.erb +20 -0
- data/app/views/rails_base/email_verification_mailer/forgot_password.html.erb +22 -0
- data/app/views/rails_base/errors/internal_error.html.erb +1 -0
- data/app/views/rails_base/errors/not_found.html.erb +1 -0
- data/app/views/rails_base/errors/unacceptable.html.erb +1 -0
- data/app/views/rails_base/event_mailer/event.html.erb +10 -0
- data/app/views/rails_base/mfa_auth/mfa_code.html.erb +10 -0
- data/app/views/rails_base/secondary_authentication/after_email_login_session_new.html.erb +3 -0
- data/app/views/rails_base/secondary_authentication/forgot_password.html.erb +9 -0
- data/app/views/rails_base/secondary_authentication/remove_me.html.erb +1 -0
- data/app/views/rails_base/secondary_authentication/static.html.erb +5 -0
- data/app/views/rails_base/shared/_admin_actions_modal.html.erb +65 -0
- data/app/views/rails_base/shared/_admin_config_class.html.erb +52 -0
- data/app/views/rails_base/shared/_admin_history.html.erb +86 -0
- data/app/views/rails_base/shared/_admin_modify_email.html.erb +78 -0
- data/app/views/rails_base/shared/_admin_modify_name.html.erb +107 -0
- data/app/views/rails_base/shared/_admin_modify_phone.html.erb +87 -0
- data/app/views/rails_base/shared/_admin_modify_text.html.erb +35 -0
- data/app/views/rails_base/shared/_admin_risky_change.html.erb +57 -0
- data/app/views/rails_base/shared/_admin_risky_mfa.html.erb +74 -0
- data/app/views/rails_base/shared/_admin_selector_dropdown.html.erb +70 -0
- data/app/views/rails_base/shared/_admin_toggle_button.html.erb +72 -0
- data/app/views/rails_base/shared/_admin_warning_alert.html.erb +7 -0
- data/app/views/rails_base/shared/_appearance_mode_selector.html.erb +183 -0
- data/app/views/rails_base/shared/_custom_form_validation_javascript.html.erb +129 -0
- data/app/views/rails_base/shared/_enable_mfa_auth_modal.html.erb +105 -0
- data/app/views/rails_base/shared/_error_pages.html.erb +123 -0
- data/app/views/rails_base/shared/_logged_in_header.html.erb +123 -0
- data/app/views/rails_base/shared/_logged_out_header.html.erb +14 -0
- data/app/views/rails_base/shared/_mfa_input_layout.html.erb +5 -0
- data/app/views/rails_base/shared/_mfa_input_layout_default.html.erb +97 -0
- data/app/views/rails_base/shared/_mfa_input_layout_fallback.html.erb +55 -0
- data/app/views/rails_base/shared/_modify_mfa_auth_modal.html.erb +20 -0
- data/app/views/rails_base/shared/_password_confirm_javascript.html.erb +71 -0
- data/app/views/rails_base/shared/_reset_password_form.html.erb +111 -0
- data/app/views/rails_base/shared/_session_create_form.html.erb +32 -0
- data/app/views/rails_base/shared/_session_timeout_modal.html.erb +76 -0
- data/app/views/rails_base/switch_user/_widget.html.erb +5 -0
- data/app/views/rails_base/user_settings/_confirm_destroy_user.html.erb +42 -0
- data/app/views/rails_base/user_settings/_destroy_user.html.erb +106 -0
- data/app/views/rails_base/user_settings/_modify_name.html.erb +71 -0
- data/app/views/rails_base/user_settings/_modify_password.html.erb +101 -0
- data/app/views/rails_base/user_settings/_modify_password_update_password.html.erb +2 -0
- data/app/views/rails_base/user_settings/index.html.erb +54 -0
- data/config/initializers/01_rails_config.rb +19 -0
- data/config/initializers/admin_action_helper.rb +88 -0
- data/config/initializers/browser.rb +4 -0
- data/config/initializers/default_logged_in_headers.rb +23 -0
- data/config/initializers/devise.rb +314 -0
- data/config/initializers/encryption.rb +2 -0
- data/config/initializers/switch_user.rb +58 -0
- data/config/initializers/switch_user_helper.rb +29 -0
- data/config/locales/devise.en.yml +65 -0
- data/config/locales/en.yml +58 -0
- data/config/routes.rb +114 -0
- data/db/migrate/20210212175453_devise_create_rails_base_users.rb +56 -0
- data/db/migrate/20210212190537_create_rails_base_short_lived_data.rb +19 -0
- data/db/migrate/20210212192645_create_rails_base_secrets.rb +11 -0
- data/db/migrate/20210406015744_create_rails_base_admin_actions.rb +17 -0
- data/db/seeds.rb +23 -0
- data/lib/link_decision_helper.rb +71 -0
- data/lib/rails_base.rb +50 -0
- data/lib/rails_base/admin/action_cache.rb +99 -0
- data/lib/rails_base/admin/action_helper.rb +134 -0
- data/lib/rails_base/admin/default_index_tile.rb +176 -0
- data/lib/rails_base/admin/index_tile.rb +186 -0
- data/lib/rails_base/config.rb +52 -0
- data/lib/rails_base/configuration/active_job.rb +38 -0
- data/lib/rails_base/configuration/admin.rb +231 -0
- data/lib/rails_base/configuration/app.rb +52 -0
- data/lib/rails_base/configuration/appearance.rb +131 -0
- data/lib/rails_base/configuration/authentication.rb +37 -0
- data/lib/rails_base/configuration/base.rb +209 -0
- data/lib/rails_base/configuration/display/background_color.rb +25 -0
- data/lib/rails_base/configuration/display/btn_danger.rb +25 -0
- data/lib/rails_base/configuration/display/btn_dark.rb +25 -0
- data/lib/rails_base/configuration/display/btn_info.rb +25 -0
- data/lib/rails_base/configuration/display/btn_light.rb +25 -0
- data/lib/rails_base/configuration/display/btn_primary.rb +25 -0
- data/lib/rails_base/configuration/display/btn_secondary.rb +25 -0
- data/lib/rails_base/configuration/display/btn_success.rb +25 -0
- data/lib/rails_base/configuration/display/btn_warning.rb +25 -0
- data/lib/rails_base/configuration/display/footer.rb +54 -0
- data/lib/rails_base/configuration/display/navbar.rb +25 -0
- data/lib/rails_base/configuration/display/table_body.rb +25 -0
- data/lib/rails_base/configuration/display/table_header.rb +25 -0
- data/lib/rails_base/configuration/display/text.rb +26 -0
- data/lib/rails_base/configuration/exceptions_app.rb +25 -0
- data/lib/rails_base/configuration/login_behavior.rb +17 -0
- data/lib/rails_base/configuration/mailer.rb +116 -0
- data/lib/rails_base/configuration/mfa.rb +84 -0
- data/lib/rails_base/configuration/owner.rb +17 -0
- data/lib/rails_base/configuration/redis.rb +29 -0
- data/lib/rails_base/configuration/user.rb +43 -0
- data/lib/rails_base/engine.rb +51 -0
- data/lib/rails_base/version.rb +10 -0
- data/lib/tasks/rails_base_tasks.rake +4 -0
- data/lib/twilio_helper.rb +26 -0
- data/lib/velocity_limiter.rb +91 -0
- metadata +619 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
module RailsBase
|
2
|
+
module UserConstants
|
3
|
+
ADMIN_ENUMS = [
|
4
|
+
ADMIN_ROLE_NONE = :none,
|
5
|
+
ADMIN_ROLE_VIEW_ONLY = :view_only,
|
6
|
+
ADMIN_ROLE_SUPER = :super,
|
7
|
+
ADMIN_ROLE_OWNER = :owner,
|
8
|
+
]
|
9
|
+
|
10
|
+
SOFT_DESTROY_PARAMS = {
|
11
|
+
mfa_enabled: false,
|
12
|
+
email_validated: false,
|
13
|
+
last_mfa_login: nil,
|
14
|
+
encrypted_password: '',
|
15
|
+
phone_number: nil,
|
16
|
+
}
|
17
|
+
|
18
|
+
SAFE_AUTOMAGIC_UPGRADE_COLS = {
|
19
|
+
active: ->(user) { RailsBase.config.admin.active_tile_users?(user) } ,
|
20
|
+
admin: ->(user) { RailsBase.config.admin.admin_type_tile_users?(user) } ,
|
21
|
+
email: ->(user) { RailsBase.config.admin.email_tile_users?(user) } ,
|
22
|
+
email_validated: ->(user) { RailsBase.config.admin.email_validate_tile_users?(user) } ,
|
23
|
+
mfa_enabled: ->(user) { RailsBase.config.admin.mfa_tile_users?(user) } ,
|
24
|
+
phone_number: ->(user) { RailsBase.config.admin.phone_tile_users?(user) } ,
|
25
|
+
last_known_timezone: ->(user) { RailsBase.config.admin.modify_timezone_tile_users?(user) }
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# == Schema Information
|
2
|
+
#
|
3
|
+
# Table name: secrets
|
4
|
+
#
|
5
|
+
# id :bigint not null, primary key
|
6
|
+
# version :integer
|
7
|
+
# secret :text(65535)
|
8
|
+
# name :string(255)
|
9
|
+
# created_at :datetime not null
|
10
|
+
# updated_at :datetime not null
|
11
|
+
#
|
12
|
+
|
13
|
+
class Secret < RailsBase::ApplicationRecord
|
14
|
+
class << self
|
15
|
+
def update(name:, secret:)
|
16
|
+
next_version = get_secrets(name: name).select(:version).last&.version || 0
|
17
|
+
next_version += 1 # always increase the version
|
18
|
+
|
19
|
+
instance = new(version: next_version, name: name, secret: secret)
|
20
|
+
instance.save!
|
21
|
+
|
22
|
+
instance
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_current_secret(name:)
|
26
|
+
get_secrets(name: name).last
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_secret_range(name:, range: [-2..-1])
|
30
|
+
where(name: name)[*range]
|
31
|
+
end
|
32
|
+
|
33
|
+
def get_secrets(name:)
|
34
|
+
where(name: name)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# == Schema Information
|
2
|
+
#
|
3
|
+
# Table name: short_lived_data
|
4
|
+
#
|
5
|
+
# id :bigint not null, primary key
|
6
|
+
# user_id :integer not null
|
7
|
+
# data :string(255) not null
|
8
|
+
# reason :string(255)
|
9
|
+
# death_time :datetime not null
|
10
|
+
# extra :string(255)
|
11
|
+
# created_at :datetime not null
|
12
|
+
# updated_at :datetime not null
|
13
|
+
# exclusive_use_count :integer default(0)
|
14
|
+
# exclusive_use_count_max :integer
|
15
|
+
#
|
16
|
+
|
17
|
+
class ShortLivedData < RailsBase::ApplicationRecord
|
18
|
+
self.table_name = 'short_lived_data'
|
19
|
+
|
20
|
+
DEFAULT_TIME_TO_LIVE = 1.hour.freeze
|
21
|
+
LENGTH_OF_HEX = 64.freeze
|
22
|
+
MAX_ATTEMPTS = 10.freeze
|
23
|
+
VALID_DATA_USE_LENGTH = [:numeric, :alphanumeric, :hex].freeze
|
24
|
+
VALID_DATA_NON_LENGTH = [:uuid]
|
25
|
+
VALID_DATA_USE = [VALID_DATA_NON_LENGTH, VALID_DATA_USE_LENGTH].flatten.freeze
|
26
|
+
|
27
|
+
class << self
|
28
|
+
# ShortLivedData.create_data_key(user: User.first, ttl: 5.hours)
|
29
|
+
def create_data_key(user:, max_use: nil, data: nil, data_use: :alphanumeric, expires_at: nil, ttl: DEFAULT_TIME_TO_LIVE, reason: 'default', length: LENGTH_OF_HEX, extra: nil)
|
30
|
+
raise ":ttl is expected to be an ActiveSupport::Duration" unless ttl.is_a?(ActiveSupport::Duration)
|
31
|
+
|
32
|
+
if data.nil?
|
33
|
+
data = generate_secure_datum(data_use, length: length)
|
34
|
+
attempt = 0
|
35
|
+
while get_by_data(data: data, reason: reason)
|
36
|
+
Rails.logger.warn "Data key already in use for #{data_use}. Attempt #{attempt} for reason #{reason} for user_id #{user.id}"
|
37
|
+
data = generate_secure_hex(data_use, length: length)
|
38
|
+
attempt +=1
|
39
|
+
raise "Failed to generate unique id. Attempted #{attempt} times" if attempt >= MAX_ATTEMPTS
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# expires at takes precedence since this is a dangerous oeration and should only be done with caution
|
44
|
+
# dangerous because of time zone checks --- we dont do that
|
45
|
+
death_time = Time.now + ttl
|
46
|
+
death_time = expires_at if expires_at
|
47
|
+
|
48
|
+
create(user_id: user.id, data: data, death_time: death_time, reason: reason, extra: extra, exclusive_use_count_max: max_use)
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_by_data(data:, reason: nil)
|
52
|
+
# data is indexed and uniq
|
53
|
+
params = { data: data, reason: reason }.compact
|
54
|
+
where(params).first
|
55
|
+
end
|
56
|
+
|
57
|
+
def generate_secure_datum(data_use, length: LENGTH_OF_HEX)
|
58
|
+
case data_use.to_sym
|
59
|
+
when :numeric
|
60
|
+
return rand.to_s[2..(2+(length-1))]
|
61
|
+
when *VALID_DATA_USE_LENGTH
|
62
|
+
return SecureRandom.public_send(data_use, length)
|
63
|
+
when *VALID_DATA_NON_LENGTH
|
64
|
+
return SecureRandom.public_send(data_use)
|
65
|
+
else
|
66
|
+
raise ArgumentError, "Unexpected data_use: Expected #{VALID_DATA_USE}. given [#{data_use}]"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def find_datum(data:, reason: nil, access_count: true)
|
71
|
+
datum = get_by_data(data: data, reason: reason)
|
72
|
+
|
73
|
+
params = {
|
74
|
+
user: datum&.user,
|
75
|
+
use_count: datum&.exclusive_use_count,
|
76
|
+
max_use_count: datum&.exclusive_use_count_max,
|
77
|
+
valid: datum&.is_valid? || false,
|
78
|
+
invalid_reason: datum&.invalid_reason || ['Forbidden. Invalid usecase'],
|
79
|
+
found: !datum.nil?,
|
80
|
+
extra: datum&.extra,
|
81
|
+
access_count_proc: -> { datum&.add_access_count! }
|
82
|
+
}
|
83
|
+
datum&.add_access_count! if access_count
|
84
|
+
|
85
|
+
return params unless params[:valid]
|
86
|
+
|
87
|
+
if reason && (datum&.reason.to_sym != reason.to_sym)
|
88
|
+
params[:valid] = false
|
89
|
+
params[:invalid_reason] = ['Unknown reason for datum field']
|
90
|
+
end
|
91
|
+
|
92
|
+
params
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def add_access_count!
|
97
|
+
# only update if count is valid and we can add things -- save db call
|
98
|
+
return false unless used_count_valid?
|
99
|
+
|
100
|
+
update_attributes(exclusive_use_count: exclusive_use_count + 1)
|
101
|
+
end
|
102
|
+
|
103
|
+
def invalid_reason
|
104
|
+
arr = []
|
105
|
+
arr << 'too many uses' unless used_count_valid?
|
106
|
+
arr << 'expired' unless still_alive?
|
107
|
+
arr
|
108
|
+
end
|
109
|
+
|
110
|
+
def is_valid?
|
111
|
+
used_count_valid? && still_alive?
|
112
|
+
end
|
113
|
+
|
114
|
+
def still_alive?
|
115
|
+
(death_time).to_f > Time.now.to_f
|
116
|
+
end
|
117
|
+
|
118
|
+
def used_count_valid?
|
119
|
+
return true if exclusive_use_count_max.nil?
|
120
|
+
|
121
|
+
return exclusive_use_count < exclusive_use_count_max
|
122
|
+
end
|
123
|
+
|
124
|
+
def user
|
125
|
+
@user ||= User.find(user_id)
|
126
|
+
end
|
127
|
+
|
128
|
+
def user=(u)
|
129
|
+
update_attributes(user_id: u.id)
|
130
|
+
u.id
|
131
|
+
end
|
132
|
+
end
|
data/app/models/user.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
# == Schema Information
|
2
|
+
#
|
3
|
+
# Table name: users
|
4
|
+
#
|
5
|
+
# id :bigint not null, primary key
|
6
|
+
# first_name :string(255) default(""), not null
|
7
|
+
# last_name :string(255) default(""), not null
|
8
|
+
# phone_number :string(255)
|
9
|
+
# last_mfa_login :datetime
|
10
|
+
# email_validated :boolean default(FALSE)
|
11
|
+
# mfa_enabled :boolean default(FALSE), not null
|
12
|
+
# active :boolean default(TRUE), not null
|
13
|
+
# admin :string(255)
|
14
|
+
# last_known_timezone :string(255)
|
15
|
+
# last_known_timezone_update :datetime
|
16
|
+
# email :string(255) default(""), not null
|
17
|
+
# encrypted_password :string(255) default(""), not null
|
18
|
+
# reset_password_token :string(255)
|
19
|
+
# reset_password_sent_at :datetime
|
20
|
+
# remember_created_at :datetime
|
21
|
+
# sign_in_count :integer default(0), not null
|
22
|
+
# current_sign_in_at :datetime
|
23
|
+
# last_sign_in_at :datetime
|
24
|
+
# current_sign_in_ip :string(255)
|
25
|
+
# last_sign_in_ip :string(255)
|
26
|
+
# created_at :datetime not null
|
27
|
+
# updated_at :datetime not null
|
28
|
+
#
|
29
|
+
class User < RailsBase::ApplicationRecord
|
30
|
+
# Include default devise modules. Others available are:
|
31
|
+
# :confirmable, :lockable, :trackable and :omniauthable
|
32
|
+
devise :database_authenticatable, :registerable,
|
33
|
+
:recoverable, :rememberable, :validatable, :timeoutable, :trackable
|
34
|
+
|
35
|
+
include RailsBase::UserConstants
|
36
|
+
|
37
|
+
validate :enforce_owner, if: :will_save_change_to_admin?
|
38
|
+
validate :enforce_admin_type, if: :will_save_change_to_admin?
|
39
|
+
|
40
|
+
def self._def_admin_convenience_method!(admin_method:)
|
41
|
+
types = RailsBase.config.admin.admin_types
|
42
|
+
#### metods on the instance
|
43
|
+
define_method("at_least_#{admin_method}?") do
|
44
|
+
i = types.find_index(admin.to_sym)
|
45
|
+
i >= types.find_index(admin_method.to_sym)
|
46
|
+
end
|
47
|
+
|
48
|
+
define_method("admin_#{admin_method}?") do
|
49
|
+
admin.to_sym == admin_method
|
50
|
+
end
|
51
|
+
|
52
|
+
define_method("admin_#{admin_method}!") do
|
53
|
+
update_attributes!(admin: admin_method)
|
54
|
+
end
|
55
|
+
|
56
|
+
#### metods on the class
|
57
|
+
define_singleton_method("admin_#{admin_method}s") do
|
58
|
+
where(admin: admin_method)
|
59
|
+
end
|
60
|
+
|
61
|
+
define_singleton_method("admin_#{admin_method}") do
|
62
|
+
arr = [admin_method]
|
63
|
+
arr = [admin_method, '', nil] if ADMIN_ROLE_NONE == admin_method
|
64
|
+
where(admin: arr)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.time_bound
|
69
|
+
Time.zone.now - RailsBase.config.auth.mfa_time_duration
|
70
|
+
end
|
71
|
+
|
72
|
+
def admin
|
73
|
+
(self[:admin].presence || ADMIN_ROLE_NONE).to_sym
|
74
|
+
end
|
75
|
+
|
76
|
+
def full_name
|
77
|
+
"#{first_name} #{last_name}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def past_mfa_time_duration?
|
81
|
+
return true if last_mfa_login.nil?
|
82
|
+
|
83
|
+
last_mfa_login < self.class.time_bound
|
84
|
+
end
|
85
|
+
|
86
|
+
def set_last_mfa_login!(time: Time.zone.now)
|
87
|
+
update(last_mfa_login: time)
|
88
|
+
end
|
89
|
+
|
90
|
+
def masked_phone
|
91
|
+
return nil unless phone_number
|
92
|
+
|
93
|
+
"(#{phone_number[0]}**) ****-**#{phone_number[-2..-1]}"
|
94
|
+
end
|
95
|
+
|
96
|
+
def soft_destroy_user!
|
97
|
+
update(SOFT_DESTROY_PARAMS)
|
98
|
+
end
|
99
|
+
|
100
|
+
def destroy_user!
|
101
|
+
self.delete
|
102
|
+
end
|
103
|
+
|
104
|
+
def inspect_name
|
105
|
+
"[#{id}]: #{full_name}"
|
106
|
+
end
|
107
|
+
|
108
|
+
def update_tz(tz_name:)
|
109
|
+
return if last_known_timezone == tz_name
|
110
|
+
|
111
|
+
Rails.logger.info { "#{id}: Setting tz_name: #{tz_name}" }
|
112
|
+
update_attributes(last_known_timezone: tz_name, last_known_timezone_update: Time.now )
|
113
|
+
end
|
114
|
+
|
115
|
+
def timezone
|
116
|
+
RailsBase.config.user.user_timezone(self)
|
117
|
+
end
|
118
|
+
|
119
|
+
def convert_time(time:)
|
120
|
+
time.in_time_zone(timezone)
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def enforce_admin_type
|
126
|
+
from, to = admin_change_to_be_saved
|
127
|
+
return if RailsBase.config.admin.admin_types.include?(to.to_sym)
|
128
|
+
|
129
|
+
errors.add(:admin, "Undefined admin type. Expected #{RailsBase.config.admin.admin_types}. Given #{to}")
|
130
|
+
end
|
131
|
+
|
132
|
+
def enforce_owner
|
133
|
+
from, to = admin_change_to_be_saved
|
134
|
+
# skip validation event if we are not updating to owner role
|
135
|
+
return if to.to_sym != ADMIN_ROLE_OWNER
|
136
|
+
|
137
|
+
# add 1 because we are trying to change the current user to ADMIN_ROLE_OWNER
|
138
|
+
count = User.where(admin: ADMIN_ROLE_OWNER).count + 1
|
139
|
+
return if count <= RailsBase.config.owner.max
|
140
|
+
|
141
|
+
errors.add(:admin, "unable to have more than #{RailsBase.config.owner.max} owner(s).")
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'twilio_helper'
|
2
|
+
require 'velocity_limiter'
|
3
|
+
|
4
|
+
module RailsBase
|
5
|
+
class AdminRiskyMfaSend < RailsBase::ServiceBase
|
6
|
+
include ActionView::Helpers::DateHelper
|
7
|
+
include VelocityLimiter
|
8
|
+
|
9
|
+
class NoPhoneNumber < StandardError; end
|
10
|
+
|
11
|
+
MAX_USE_COUNT = 1.freeze
|
12
|
+
DATA_USE = :numeric
|
13
|
+
EXPIRES_AT = 1.minutes
|
14
|
+
|
15
|
+
delegate :user, to: :context
|
16
|
+
delegate :reason, to: :context
|
17
|
+
|
18
|
+
def call
|
19
|
+
validate_phone!
|
20
|
+
|
21
|
+
velocity = velocity_limit_reached?
|
22
|
+
context.fail!(message: velocity[:msg]) if velocity[:reached]
|
23
|
+
|
24
|
+
data_point = create_short_lived_data
|
25
|
+
send_twilio!(data_point.data)
|
26
|
+
context.short_lived_data = data_point
|
27
|
+
context.message = "MFA code has been succesfully sent. you have #{EXPIRES_AT}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def send_twilio!(code)
|
31
|
+
TwilioJob.perform_later(message: message(code), to: user.phone_number)
|
32
|
+
log(level: :info, msg: "Sent twilio message to #{user.phone_number}")
|
33
|
+
rescue StandardError => e
|
34
|
+
log(level: :error, msg: "Error caught #{e.class.name}")
|
35
|
+
log(level: :error, msg: "Failed to send sms to #{user.phone_number}")
|
36
|
+
context.fail!(message: "Failed to send sms. Please retry logging in.")
|
37
|
+
end
|
38
|
+
|
39
|
+
def message(code)
|
40
|
+
"Hello #{user.full_name}. Here is your admin verification code #{code}."
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_short_lived_data
|
44
|
+
params = {
|
45
|
+
user: user,
|
46
|
+
max_use: MAX_USE_COUNT,
|
47
|
+
reason: reason,
|
48
|
+
data_use: DATA_USE,
|
49
|
+
expires_at: EXPIRES_AT.from_now,
|
50
|
+
length: Authentication::Constants::MFA_LENGTH,
|
51
|
+
}
|
52
|
+
ShortLivedData.create_data_key(params)
|
53
|
+
end
|
54
|
+
|
55
|
+
def velocity_max_in_frame
|
56
|
+
RailsBase.config.admin.admin_velocity_max_in_frame
|
57
|
+
end
|
58
|
+
|
59
|
+
def velocity_max
|
60
|
+
RailsBase.config.admin.admin_velocity_max
|
61
|
+
end
|
62
|
+
|
63
|
+
def velocity_frame
|
64
|
+
RailsBase.config.admin.admin_velocity_frame
|
65
|
+
end
|
66
|
+
|
67
|
+
def cache_key
|
68
|
+
"#{self.class.name.downcase}.#{user.id}"
|
69
|
+
end
|
70
|
+
|
71
|
+
def validate_phone!
|
72
|
+
context.fail!(message: "No phone for user [#{user.id}] [#{user.phone_number}]") if user.phone_number.nil?
|
73
|
+
end
|
74
|
+
|
75
|
+
def validate!
|
76
|
+
raise "Expected user to be a User. Received #{user.class}" unless user.is_a? User
|
77
|
+
raise "Expected reason to be a present." if reason.nil?
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module RailsBase
|
2
|
+
class AdminUpdateAttribute < RailsBase::ServiceBase
|
3
|
+
delegate :params, to: :context
|
4
|
+
delegate :klass_string, to: :context
|
5
|
+
delegate :admin_user, to: :context
|
6
|
+
|
7
|
+
def call
|
8
|
+
validate_model!
|
9
|
+
validate_model_row!
|
10
|
+
|
11
|
+
attribute = validate_attribute!
|
12
|
+
validate_permission!(attribute: attribute)
|
13
|
+
fail_attribute!(attribute: attribute)
|
14
|
+
|
15
|
+
original_value = model_row.public_send(attribute)
|
16
|
+
begin
|
17
|
+
model_row.update_attributes!(attribute => sanitized_value)
|
18
|
+
rescue ActiveRecord::RecordInvalid => e
|
19
|
+
context.fail!(message: "Failed to update [#{attribute}] with #{sanitized_value} on #{model}##{model_row.id}. #{e.message}")
|
20
|
+
rescue StandardError
|
21
|
+
context.fail!(message: "Failed to update [#{attribute}] with #{sanitized_value} on #{model}##{model_row.id}")
|
22
|
+
end
|
23
|
+
context.attribute = sanitized_value
|
24
|
+
context.original_attribute = original_value
|
25
|
+
context.model = model_row
|
26
|
+
context.message = "#{model}##{params[:id]} has changed attribute [#{attribute}] from [#{original_value}]=>#{sanitized_value}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def fail_attribute!(attribute:)
|
30
|
+
if params[:_fail_].present?
|
31
|
+
log(level: :warn, msg: "FAIL param passed in. Automagic failure for admin update")
|
32
|
+
context.fail!(message: "Failed to update [#{attribute}] with #{sanitized_value} on #{model}##{model_row.id}")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def validate_permission!(attribute:)
|
37
|
+
proc = model::SAFE_AUTOMAGIC_UPGRADE_COLS[attribute]
|
38
|
+
return if proc.call(admin_user)
|
39
|
+
|
40
|
+
context.fail!(message: "User does not have permissions to update #{attribute}")
|
41
|
+
end
|
42
|
+
|
43
|
+
def validate_attribute!
|
44
|
+
attribute = params[:attribute].to_sym
|
45
|
+
unless model::SAFE_AUTOMAGIC_UPGRADE_COLS.keys.include?(attribute)
|
46
|
+
context.fail!(message: "#{attribute} is not part of allowed updatable columns")
|
47
|
+
end
|
48
|
+
attribute
|
49
|
+
end
|
50
|
+
|
51
|
+
def sanitized_value
|
52
|
+
case params[:value]
|
53
|
+
when 'true'
|
54
|
+
true
|
55
|
+
when 'false'
|
56
|
+
false
|
57
|
+
else
|
58
|
+
params[:value]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def model
|
63
|
+
@model ||= klass_string ? klass_string.constantize : User
|
64
|
+
end
|
65
|
+
|
66
|
+
def model_row
|
67
|
+
@model_row ||= begin
|
68
|
+
model.find(params[:id])
|
69
|
+
rescue ActiveRecord::RecordNotFound
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def validate_model_row!
|
75
|
+
return if model_row
|
76
|
+
|
77
|
+
context.fail!(message: "Failed to find id:#{params[:id]} on model: #{model}")
|
78
|
+
end
|
79
|
+
|
80
|
+
def validate_model!
|
81
|
+
begin
|
82
|
+
model
|
83
|
+
rescue StandardError
|
84
|
+
context.fail!(message: "Failed to find model")
|
85
|
+
end
|
86
|
+
|
87
|
+
begin
|
88
|
+
model::SAFE_AUTOMAGIC_UPGRADE_COLS
|
89
|
+
rescue StandardError
|
90
|
+
context.fail!(message: "#{model}::SAFE_AUTOMAGIC_UPGRADE_COLS array does not exist")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def validate!
|
95
|
+
raise "Expected params to be a Hash. Received #{params.class}" unless [ActionController::Parameters, Hash].include?(params.class)
|
96
|
+
raise "Expected params to have a id. Received #{params[:id]}" if params[:id].nil?
|
97
|
+
raise "Expected admin_user." if admin_user.nil?
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|