securial 0.4.2 → 0.6.1
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/README.md +50 -14
- data/app/controllers/concerns/securial/identity.rb +4 -12
- data/app/controllers/securial/sessions_controller.rb +20 -8
- data/app/models/concerns/securial/password_resettable.rb +15 -0
- data/app/models/securial/session.rb +2 -2
- data/app/views/securial/roles/_securial_role.json.jbuilder +1 -2
- data/app/views/securial/sessions/_session.json.jbuilder +1 -2
- data/app/views/securial/shared/_timestamps.json.jbuilder +8 -0
- data/app/views/securial/users/_securial_user.json.jbuilder +3 -2
- data/config/routes.rb +0 -1
- data/db/migrate/20250517155521_create_securial_users.rb +10 -7
- data/lib/generators/securial/install/templates/securial_initializer.erb +53 -12
- data/lib/generators/securial/install/views_generastor.rb +25 -0
- data/lib/generators/securial/jbuilder/templates/_resource.json.erb +1 -2
- data/lib/securial/auth/_index.rb +3 -0
- data/lib/securial/auth/auth_encoder.rb +56 -0
- data/lib/securial/auth/errors.rb +15 -0
- data/lib/securial/auth/session_creator.rb +21 -0
- data/lib/securial/config/_index.rb +3 -0
- data/lib/securial/config/configuration.rb +62 -0
- data/lib/securial/config/errors.rb +20 -0
- data/lib/securial/config/validation.rb +249 -0
- data/lib/securial/engine.rb +41 -38
- data/lib/securial/helpers/_index.rb +2 -0
- data/lib/securial/helpers/regex_helper.rb +7 -7
- data/lib/securial/inspectors/_index.rb +1 -0
- data/lib/securial/inspectors/route_inspector.rb +52 -0
- data/lib/securial/key_transformer.rb +32 -0
- data/lib/securial/logger.rb +2 -2
- data/lib/securial/middleware/_index.rb +3 -0
- data/lib/securial/middleware/transform_request_keys.rb +33 -0
- data/lib/securial/middleware/transform_response_keys.rb +45 -0
- data/lib/securial/rack_attack.rb +48 -0
- data/lib/securial/version.rb +1 -1
- data/lib/securial.rb +7 -72
- metadata +37 -154
- data/app/views/securial/passwords/_password.json.jbuilder +0 -2
- data/app/views/securial/passwords/index.json.jbuilder +0 -1
- data/app/views/securial/passwords/show.json.jbuilder +0 -1
- data/db/migrate/20250524210207_add_password_reset_fields_to_securial_users.rb +0 -6
- data/lib/securial/configuration.rb +0 -35
- data/lib/securial/errors/config_errors.rb +0 -12
- data/lib/securial/errors/session_errors.rb +0 -6
- data/lib/securial/helpers/auth_helper.rb +0 -46
- data/lib/securial/route_inspector.rb +0 -50
@@ -0,0 +1,249 @@
|
|
1
|
+
require "securial/logger"
|
2
|
+
|
3
|
+
module Securial
|
4
|
+
module Config
|
5
|
+
module Validation
|
6
|
+
class << self # rubocop:disable Metrics/ClassLength
|
7
|
+
def validate_all!(config)
|
8
|
+
validate_admin_role!(config)
|
9
|
+
validate_session_config!(config)
|
10
|
+
validate_mailer_sender!(config)
|
11
|
+
validate_password_config!(config)
|
12
|
+
validate_response_config!(config)
|
13
|
+
validate_security_config!(config)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def validate_admin_role!(config)
|
19
|
+
if config.admin_role.nil? || config.admin_role.to_s.strip.empty?
|
20
|
+
error_message = "Admin role is not set."
|
21
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
22
|
+
raise Securial::Config::Errors::ConfigAdminRoleError, error_message
|
23
|
+
end
|
24
|
+
|
25
|
+
unless config.admin_role.is_a?(Symbol) || config.admin_role.is_a?(String)
|
26
|
+
error_message = "Admin role must be a Symbol or String."
|
27
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
28
|
+
raise Securial::Config::Errors::ConfigAdminRoleError, error_message
|
29
|
+
end
|
30
|
+
|
31
|
+
if config.admin_role.to_s.pluralize.downcase == "accounts"
|
32
|
+
error_message = "The admin role cannot be 'account' or 'accounts' as it conflicts with the default routes."
|
33
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
34
|
+
raise Securial::Config::Errors::ConfigAdminRoleError, error_message
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate_session_config!(config)
|
39
|
+
validate_session_expiry_duration!(config)
|
40
|
+
validate_session_algorithm!(config)
|
41
|
+
validate_session_secret!(config)
|
42
|
+
end
|
43
|
+
|
44
|
+
def validate_session_expiry_duration!(config)
|
45
|
+
if config.session_expiration_duration.nil?
|
46
|
+
error_message = "Session expiration duration is not set."
|
47
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
48
|
+
raise Securial::Config::Errors::ConfigSessionExpirationDurationError, error_message
|
49
|
+
end
|
50
|
+
if config.session_expiration_duration.class != ActiveSupport::Duration
|
51
|
+
error_message = "Session expiration duration must be an ActiveSupport::Duration."
|
52
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
53
|
+
raise Securial::Config::Errors::ConfigSessionExpirationDurationError, error_message
|
54
|
+
end
|
55
|
+
if config.session_expiration_duration <= 0
|
56
|
+
Securial::ENGINE_LOGGER.fatal("Session expiration duration must be greater than 0.")
|
57
|
+
raise Securial::Config::Errors::ConfigSessionExpirationDurationError, "Session expiration duration must be greater than 0."
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def validate_session_algorithm!(config)
|
62
|
+
if config.session_algorithm.blank?
|
63
|
+
error_message = "Session algorithm is not set."
|
64
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
65
|
+
raise Securial::Config::Errors::ConfigSessionAlgorithmError, error_message
|
66
|
+
end
|
67
|
+
unless config.session_algorithm.is_a?(Symbol)
|
68
|
+
error_message = "Session algorithm must be a Symbol."
|
69
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
70
|
+
raise Securial::Config::Errors::ConfigSessionAlgorithmError, error_message
|
71
|
+
end
|
72
|
+
valid_algorithms = Securial::Config::VALID_SESSION_ENCRYPTION_ALGORITHMS
|
73
|
+
unless valid_algorithms.include?(config.session_algorithm)
|
74
|
+
error_message = "Invalid session algorithm. Valid options are: #{valid_algorithms.map(&:inspect).join(', ')}."
|
75
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
76
|
+
raise Securial::Config::Errors::ConfigSessionAlgorithmError, error_message
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def validate_session_secret!(config)
|
81
|
+
if config.session_secret.blank?
|
82
|
+
error_message = "Session secret is not set."
|
83
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
84
|
+
raise Securial::Config::Errors::ConfigSessionSecretError, error_message
|
85
|
+
end
|
86
|
+
unless config.session_secret.is_a?(String)
|
87
|
+
error_message = "Session secret must be a String."
|
88
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
89
|
+
raise Securial::Config::Errors::ConfigSessionSecretError, error_message
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def validate_mailer_sender!(config)
|
94
|
+
if config.mailer_sender.blank?
|
95
|
+
error_message = "Mailer sender is not set."
|
96
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
97
|
+
raise Securial::Config::Errors::ConfigMailerSenderError, error_message
|
98
|
+
end
|
99
|
+
if config.mailer_sender !~ URI::MailTo::EMAIL_REGEXP
|
100
|
+
error_message = "Mailer sender is not a valid email address."
|
101
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
102
|
+
raise Securial::Config::Errors::ConfigMailerSenderError, error_message
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def validate_password_config!(config)
|
107
|
+
validate_password_reset_subject!(config)
|
108
|
+
validate_password_min_max_length!(config)
|
109
|
+
validate_password_complexity!(config)
|
110
|
+
validate_password_expiration!(config)
|
111
|
+
validate_password_reset_token!(config)
|
112
|
+
end
|
113
|
+
|
114
|
+
def validate_password_reset_token!(config)
|
115
|
+
if config.reset_password_token_secret.blank?
|
116
|
+
error_message = "Reset password token secret is not set."
|
117
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
118
|
+
raise Securial::Config::Errors::ConfigPasswordError, error_message
|
119
|
+
end
|
120
|
+
unless config.reset_password_token_secret.is_a?(String)
|
121
|
+
error_message = "Reset password token secret must be a String."
|
122
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
123
|
+
raise Securial::Config::Errors::ConfigPasswordError, error_message
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def validate_password_reset_subject!(config)
|
128
|
+
if config.password_reset_email_subject.blank?
|
129
|
+
error_message = "Password reset email subject is not set."
|
130
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
131
|
+
raise Securial::Config::Errors::ConfigPasswordError, error_message
|
132
|
+
end
|
133
|
+
unless config.password_reset_email_subject.is_a?(String)
|
134
|
+
error_message = "Password reset email subject must be a String."
|
135
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
136
|
+
raise Securial::Config::Errors::ConfigPasswordError, error_message
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def validate_password_min_max_length!(config)
|
141
|
+
unless config.password_min_length.is_a?(Integer) && config.password_min_length > 0
|
142
|
+
error_message = "Password minimum length must be a positive integer."
|
143
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
144
|
+
raise Securial::Config::Errors::ConfigPasswordError, error_message
|
145
|
+
end
|
146
|
+
unless config.password_max_length.is_a?(Integer) && config.password_max_length >= config.password_min_length
|
147
|
+
error_message = "Password maximum length must be an integer greater than or equal to the minimum length."
|
148
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
149
|
+
raise Securial::Config::Errors::ConfigPasswordError, error_message
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def validate_password_complexity!(config)
|
154
|
+
if config.password_complexity.nil? || !config.password_complexity.is_a?(Regexp)
|
155
|
+
error_message = "Password complexity regex is not set or is not a valid Regexp."
|
156
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
157
|
+
raise Securial::Config::Errors::ConfigPasswordError, error_message
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def validate_password_expiration!(config)
|
162
|
+
unless config.password_expires.is_a?(TrueClass) || config.password_expires.is_a?(FalseClass)
|
163
|
+
error_message = "Password expiration must be a boolean value."
|
164
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
165
|
+
raise Securial::Config::Errors::ConfigPasswordError, error_message
|
166
|
+
end
|
167
|
+
|
168
|
+
if config.password_expires == true && (
|
169
|
+
config.password_expires_in.nil? ||
|
170
|
+
!config.password_expires_in.is_a?(ActiveSupport::Duration) ||
|
171
|
+
config.password_expires_in <= 0
|
172
|
+
)
|
173
|
+
error_message = "Password expiration duration is not set or is not a valid ActiveSupport::Duration."
|
174
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
175
|
+
raise Securial::Config::Errors::ConfigPasswordError, error_message
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def validate_response_config!(config)
|
180
|
+
validate_response_keys_format!(config)
|
181
|
+
validate_timestamps_in_response!(config)
|
182
|
+
end
|
183
|
+
|
184
|
+
def validate_response_keys_format!(config)
|
185
|
+
valid_formats = Securial::Config::VALID_RESPONSE_KEYS_FORMATS
|
186
|
+
unless valid_formats.include?(config.response_keys_format)
|
187
|
+
error_message = "Invalid response_keys_format option. Valid options are: #{valid_formats.map(&:inspect).join(', ')}."
|
188
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
189
|
+
raise Securial::Config::Errors::ConfigResponseError, error_message
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def validate_timestamps_in_response!(config)
|
194
|
+
valid_options = Securial::Config::VALID_TIMESTAMP_OPTIONS
|
195
|
+
unless valid_options.include?(config.timestamps_in_response)
|
196
|
+
error_message = "Invalid timestamps_in_response option. Valid options are: #{valid_options.map(&:inspect).join(', ')}."
|
197
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
198
|
+
raise Securial::Config::Errors::ConfigResponseError, error_message
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def validate_security_config!(config)
|
203
|
+
validate_security_headers!(config)
|
204
|
+
validate_rate_limiting!(config)
|
205
|
+
end
|
206
|
+
|
207
|
+
def validate_security_headers!(config)
|
208
|
+
valid_options = Securial::Config::VALID_SECURITY_HEADERS
|
209
|
+
unless valid_options.include?(config.security_headers)
|
210
|
+
error_message = "Invalid security_headers option. Valid options are: #{valid_options.map(&:inspect).join(', ')}."
|
211
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
212
|
+
raise Securial::Config::Errors::ConfigSecurityError, error_message
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def validate_rate_limiting!(config) # rubocop:disable Metrics/MethodLength
|
217
|
+
unless config.rate_limiting_enabled.is_a?(TrueClass) || config.rate_limiting_enabled.is_a?(FalseClass)
|
218
|
+
error_message = "rate_limiting_enabled must be a boolean value."
|
219
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
220
|
+
raise Securial::Config::Errors::ConfigSecurityError, error_message
|
221
|
+
end
|
222
|
+
|
223
|
+
return unless config.rate_limiting_enabled
|
224
|
+
|
225
|
+
unless
|
226
|
+
config.rate_limit_requests_per_minute.is_a?(Integer) &&
|
227
|
+
config.rate_limit_requests_per_minute > 0
|
228
|
+
|
229
|
+
error_message = "rate_limit_requests_per_minute must be a positive integer when rate limiting is enabled."
|
230
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
231
|
+
raise Securial::Config::Errors::ConfigSecurityError, error_message
|
232
|
+
end
|
233
|
+
|
234
|
+
unless config.rate_limit_response_status.is_a?(Integer) && config.rate_limit_response_status.between?(400, 599)
|
235
|
+
error_message = "rate_limit_response_status must be an HTTP status code between 4xx and 5xx."
|
236
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
237
|
+
raise Securial::Config::Errors::ConfigSecurityError, error_message
|
238
|
+
end
|
239
|
+
|
240
|
+
unless config.rate_limit_response_message.is_a?(String) && !config.rate_limit_response_message.strip.empty?
|
241
|
+
error_message = "rate_limit_response_message must be a non-empty String."
|
242
|
+
Securial::ENGINE_LOGGER.fatal(error_message)
|
243
|
+
raise Securial::Config::Errors::ConfigSecurityError, error_message
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
data/lib/securial/engine.rb
CHANGED
@@ -1,22 +1,22 @@
|
|
1
1
|
require_relative "./logger"
|
2
|
+
require_relative "./key_transformer"
|
3
|
+
require_relative "./config/_index"
|
4
|
+
require_relative "./helpers/_index"
|
5
|
+
require_relative "./auth/_index"
|
6
|
+
require_relative "./inspectors/_index"
|
2
7
|
|
3
|
-
|
4
|
-
|
5
|
-
require_relative "./configuration"
|
6
|
-
|
7
|
-
require_relative "./helpers/auth_helper"
|
8
|
-
require_relative "./helpers/normalizing_helper"
|
9
|
-
require_relative "./helpers/regex_helper"
|
10
|
-
|
11
|
-
require_relative "./route_inspector"
|
12
|
-
|
13
|
-
require_relative "./middleware/request_logger_tag"
|
8
|
+
require_relative "./middleware/_index"
|
14
9
|
require "jwt"
|
15
10
|
|
16
11
|
module Securial
|
17
12
|
class Engine < ::Rails::Engine
|
18
13
|
isolate_namespace Securial
|
19
14
|
|
15
|
+
# Set API-only mode and autoload custom generator paths
|
16
|
+
config.api_only = true
|
17
|
+
config.generators.api_only = true
|
18
|
+
config.autoload_paths += Dir["#{config.root}/lib/generators"]
|
19
|
+
|
20
20
|
initializer "securial.filter_parameters" do |app|
|
21
21
|
app.config.filter_parameters += [
|
22
22
|
:password,
|
@@ -30,37 +30,47 @@ module Securial
|
|
30
30
|
Securial.const_set(:ENGINE_LOGGER, Securial::Logger.build)
|
31
31
|
end
|
32
32
|
|
33
|
-
initializer "securial.
|
34
|
-
|
33
|
+
initializer "securial.log_initialization" do |app|
|
34
|
+
log "[Initializing Engine] Host app: #{app.class.name}"
|
35
35
|
end
|
36
36
|
|
37
|
-
initializer "securial.
|
38
|
-
|
39
|
-
|
40
|
-
|
37
|
+
initializer "securial.validate_config" do
|
38
|
+
log "[Validating configuration] from `config/initializers/securial.rb`..."
|
39
|
+
Securial::Config::Validation.validate_all!(Securial.configuration)
|
40
|
+
end
|
41
|
+
|
42
|
+
initializer "securial.extend_application_controller" do
|
43
|
+
ActiveSupport.on_load(:action_controller_base) { include Securial::Identity }
|
44
|
+
ActiveSupport.on_load(:action_controller_api) { include Securial::Identity }
|
41
45
|
end
|
42
46
|
|
43
|
-
initializer "securial.
|
47
|
+
initializer "securial.factory_bot", after: "factory_bot.set_factory_paths" do
|
48
|
+
FactoryBot.definition_file_paths << Engine.root.join("lib", "securial", "factories") if defined?(FactoryBot)
|
49
|
+
end
|
50
|
+
|
51
|
+
initializer "securial.factory_bot_generator" do
|
44
52
|
require_relative "../generators/factory_bot/model/model_generator"
|
45
53
|
end
|
46
54
|
|
47
|
-
initializer "securial.
|
48
|
-
|
49
|
-
|
55
|
+
initializer "securial.middleware" do |app|
|
56
|
+
middleware.use Securial::Middleware::RequestLoggerTag
|
57
|
+
middleware.use Securial::Middleware::TransformRequestKeys
|
58
|
+
middleware.use Securial::Middleware::TransformResponseKeys
|
59
|
+
if Securial.configuration.rate_limiting_enabled
|
60
|
+
require "rack/attack"
|
61
|
+
require_relative "./rack_attack"
|
62
|
+
middleware.use Rack::Attack
|
50
63
|
end
|
64
|
+
end
|
51
65
|
|
52
|
-
|
53
|
-
|
66
|
+
initializer "securial.log_ready", after: :load_config_initializers do
|
67
|
+
Rails.application.config.after_initialize do
|
68
|
+
log "[Engine fully initialized] Environment: #{Rails.env}"
|
54
69
|
end
|
55
70
|
end
|
56
71
|
|
57
|
-
config.generators.api_only = true
|
58
|
-
|
59
|
-
config.autoload_paths += Dir["#{config.root}/lib/generators"]
|
60
|
-
|
61
72
|
config.generators do |g|
|
62
73
|
g.orm :active_record, primary_key_type: :string
|
63
|
-
|
64
74
|
g.test_framework :rspec,
|
65
75
|
fixtures: false,
|
66
76
|
view_specs: false,
|
@@ -69,21 +79,14 @@ module Securial
|
|
69
79
|
controller_specs: true,
|
70
80
|
request_specs: true,
|
71
81
|
model_specs: true
|
72
|
-
|
73
82
|
g.fixture_replacement :factory_bot, dir: "lib/securial/factories"
|
74
|
-
|
75
|
-
# Add JBuilder configuration
|
76
83
|
g.template_engine :jbuilder
|
77
84
|
end
|
78
85
|
|
79
|
-
|
80
|
-
app.middleware.use Securial::Middleware::RequestLoggerTag
|
81
|
-
end
|
86
|
+
private
|
82
87
|
|
83
|
-
|
84
|
-
|
85
|
-
Securial::ENGINE_LOGGER.info("[Securial] Engine fully initialized in #{Rails.env} environment.")
|
86
|
-
end
|
88
|
+
def log(message)
|
89
|
+
Securial::ENGINE_LOGGER.info("[Securial] #{message}")
|
87
90
|
end
|
88
91
|
end
|
89
92
|
end
|
@@ -4,14 +4,14 @@ module Securial
|
|
4
4
|
USERNAME_REGEX = /\A(?![0-9])[a-zA-Z](?:[a-zA-Z0-9]|[._](?![._]))*[a-zA-Z0-9]\z/
|
5
5
|
PASSWORD_REGEX = %r{\A(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])[a-zA-Z].*\z}
|
6
6
|
|
7
|
-
|
7
|
+
class << self
|
8
|
+
def valid_email?(email)
|
9
|
+
email.match?(EMAIL_REGEX)
|
10
|
+
end
|
8
11
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
def valid_username?(username)
|
14
|
-
username.match?(USERNAME_REGEX)
|
12
|
+
def valid_username?(username)
|
13
|
+
username.match?(USERNAME_REGEX)
|
14
|
+
end
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative "route_inspector"
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Securial
|
2
|
+
module Inspectors
|
3
|
+
module RouteInspector
|
4
|
+
def self.print_routes(controller: nil)
|
5
|
+
filtered = Securial::Engine.routes.routes.select do |r|
|
6
|
+
ctrl = r.defaults[:controller]
|
7
|
+
controller.nil? || ctrl == "securial/#{controller}"
|
8
|
+
end
|
9
|
+
|
10
|
+
print_headers(filtered, controller)
|
11
|
+
print_details(filtered, controller)
|
12
|
+
true
|
13
|
+
end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
private
|
17
|
+
|
18
|
+
# rubocop:disable Rails/Output
|
19
|
+
def print_headers(filtered, controller)
|
20
|
+
Securial::ENGINE_LOGGER.debug "Securial engine routes:"
|
21
|
+
Securial::ENGINE_LOGGER.debug "Total routes: #{filtered.size}"
|
22
|
+
Securial::ENGINE_LOGGER.debug "Filtered by controller: #{controller}" if controller
|
23
|
+
Securial::ENGINE_LOGGER.debug "Filtered routes: #{filtered.size}" if controller
|
24
|
+
Securial::ENGINE_LOGGER.debug "-" * 120
|
25
|
+
Securial::ENGINE_LOGGER.debug "#{'Verb'.ljust(8)} #{'Path'.ljust(45)} #{'Controller#Action'.ljust(40)} Name"
|
26
|
+
Securial::ENGINE_LOGGER.debug "-" * 120
|
27
|
+
end
|
28
|
+
|
29
|
+
def print_details(filtered, controller) # rubocop:disable Rails/Output
|
30
|
+
if filtered.empty?
|
31
|
+
if controller
|
32
|
+
Securial::ENGINE_LOGGER.debug "No routes found for controller: #{controller}"
|
33
|
+
else
|
34
|
+
Securial::ENGINE_LOGGER.debug "No routes found for Securial engine"
|
35
|
+
end
|
36
|
+
Securial::ENGINE_LOGGER.debug "-" * 120
|
37
|
+
return
|
38
|
+
end
|
39
|
+
|
40
|
+
Securial::ENGINE_LOGGER.debug filtered.map { |r|
|
41
|
+
name = r.name || ""
|
42
|
+
verb = r.verb.to_s.ljust(8)
|
43
|
+
path = r.path.spec.to_s.sub(/\(\.:format\)/, "").ljust(45)
|
44
|
+
ctrl_action = "#{r.defaults[:controller]}##{r.defaults[:action]}"
|
45
|
+
"#{verb} #{path} #{ctrl_action.ljust(40)} #{name}"
|
46
|
+
}.join("\n")
|
47
|
+
end
|
48
|
+
# rubocop:enable Rails/Output
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# lib/securial/key_transformer.rb
|
2
|
+
module Securial
|
3
|
+
module KeyTransformer
|
4
|
+
def self.camelize(str, format)
|
5
|
+
return str unless str.is_a?(String)
|
6
|
+
|
7
|
+
case format
|
8
|
+
when :lowerCamelCase
|
9
|
+
str.camelize(:lower)
|
10
|
+
when :UpperCamelCase
|
11
|
+
str.camelize
|
12
|
+
else
|
13
|
+
str
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.underscore(str)
|
18
|
+
str.to_s.underscore
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.deep_transform_keys(obj, &block)
|
22
|
+
case obj
|
23
|
+
when Hash
|
24
|
+
obj.transform_keys(&block).transform_values { |v| deep_transform_keys(v, &block) }
|
25
|
+
when Array
|
26
|
+
obj.map { |e| deep_transform_keys(e, &block) }
|
27
|
+
else
|
28
|
+
obj
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/securial/logger.rb
CHANGED
@@ -7,13 +7,13 @@ module Securial
|
|
7
7
|
def self.build
|
8
8
|
outputs = []
|
9
9
|
|
10
|
-
|
10
|
+
unless Securial.configuration.log_to_file == false
|
11
11
|
log_file = Rails.root.join("log", "securial.log").open("a")
|
12
12
|
log_file.sync = true
|
13
13
|
outputs << log_file
|
14
14
|
end
|
15
15
|
|
16
|
-
|
16
|
+
unless Securial.configuration.log_to_stdout == false
|
17
17
|
outputs << STDOUT
|
18
18
|
end
|
19
19
|
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# lib/securial/middleware/transform_request_keys.rb
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Securial
|
5
|
+
module Middleware
|
6
|
+
class TransformRequestKeys
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
if env["CONTENT_TYPE"]&.include?("application/json") && Securial.configuration.response_keys_format != :snake_case
|
13
|
+
req = Rack::Request.new(env)
|
14
|
+
if (req.body&.size || 0) > 0
|
15
|
+
raw = req.body.read
|
16
|
+
req.body.rewind
|
17
|
+
begin
|
18
|
+
parsed = JSON.parse(raw)
|
19
|
+
transformed = Securial::KeyTransformer.deep_transform_keys(parsed) do |key|
|
20
|
+
Securial::KeyTransformer.underscore(key)
|
21
|
+
end
|
22
|
+
env["rack.input"] = StringIO.new(JSON.dump(transformed))
|
23
|
+
env["rack.input"].rewind
|
24
|
+
rescue JSON::ParserError
|
25
|
+
# noop
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
@app.call(env)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# lib/securial/middleware/transform_response_keys.rb
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Securial
|
5
|
+
module Middleware
|
6
|
+
class TransformResponseKeys
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
status, headers, response = @app.call(env)
|
13
|
+
|
14
|
+
if json_response?(headers)
|
15
|
+
body = extract_body(response)
|
16
|
+
|
17
|
+
if body.present?
|
18
|
+
format = Securial.configuration.response_keys_format
|
19
|
+
transformed = KeyTransformer.deep_transform_keys(JSON.parse(body)) do |key|
|
20
|
+
KeyTransformer.camelize(key, format)
|
21
|
+
end
|
22
|
+
|
23
|
+
new_body = [JSON.generate(transformed)]
|
24
|
+
headers["Content-Length"] = new_body.first.bytesize.to_s
|
25
|
+
return [status, headers, new_body]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
[status, headers, response]
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def json_response?(headers)
|
35
|
+
headers["Content-Type"]&.include?("application/json")
|
36
|
+
end
|
37
|
+
|
38
|
+
def extract_body(response)
|
39
|
+
response_body = ""
|
40
|
+
response.each { |part| response_body << part }
|
41
|
+
response_body
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
Rails.application.config.to_prepare do
|
2
|
+
class Rack::Attack
|
3
|
+
limit = Securial.configuration.rate_limit_requests_per_minute
|
4
|
+
resp_status = Securial.configuration.rate_limit_response_status
|
5
|
+
resp_message = Securial.configuration.rate_limit_response_message
|
6
|
+
|
7
|
+
### Throttle login attempts by IP ###
|
8
|
+
throttle("securial/logins/ip", limit: limit, period: 1.minute) do |req|
|
9
|
+
if req.path.include?("sessions/login") && req.post?
|
10
|
+
req.ip
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
### Throttle login attempts by username ###
|
15
|
+
throttle("securial/logins/email", limit: 5, period: 1.minute) do |req|
|
16
|
+
if req.path.include?("sessions/login") && req.post?
|
17
|
+
req.params["email_address"].to_s.downcase.strip
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
### TODO: Add throttling for other endpoints as needed ###
|
22
|
+
# Example: Throttle password reset requests by email
|
23
|
+
throttle("securial/password_resets/ip", limit: 5, period: 1.minute) do |req|
|
24
|
+
if req.path.include?("password/forgot") && req.post?
|
25
|
+
req.ip
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
throttle("securial/password_resets/email", limit: 5, period: 1.minute) do |req|
|
30
|
+
if req.path.include?("password/forgot") && req.post?
|
31
|
+
req.params["email_address"].to_s.downcase.strip
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
### Custom response ###
|
36
|
+
self.throttled_responder = lambda do |request|
|
37
|
+
retry_after = (request.env["rack.attack.match_data"] || {})[:period]
|
38
|
+
[
|
39
|
+
resp_status,
|
40
|
+
{
|
41
|
+
"Content-Type" => "application/json",
|
42
|
+
"Retry-After" => retry_after.to_s,
|
43
|
+
},
|
44
|
+
[{ error: resp_message }.to_json]
|
45
|
+
]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/securial/version.rb
CHANGED