securial 0.8.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +14 -16
- data/app/controllers/concerns/securial/identity.rb +18 -9
- data/app/controllers/securial/status_controller.rb +2 -0
- data/app/controllers/securial/users_controller.rb +1 -1
- data/app/views/securial/status/show.json.jbuilder +1 -1
- data/bin/securial +20 -52
- data/db/migrate/20250606182648_seed_roles_and_users.rb +69 -0
- data/lib/generators/securial/install/templates/securial_initializer.erb +115 -18
- data/lib/securial/cli/run.rb +11 -0
- data/lib/securial/cli/securial_new.rb +53 -0
- data/lib/securial/cli/show_help.rb +26 -0
- data/lib/securial/cli/show_version.rb +9 -0
- data/lib/securial/config/configuration.rb +3 -53
- data/lib/securial/config/signature.rb +107 -0
- data/lib/securial/config/validation.rb +58 -14
- data/lib/securial/config.rb +2 -0
- data/lib/securial/engine.rb +2 -0
- data/lib/securial/engine_initializers.rb +21 -2
- data/lib/securial/error/config.rb +0 -28
- data/lib/securial/helpers/key_transformer.rb +33 -0
- data/lib/securial/helpers.rb +1 -0
- data/lib/securial/middleware/response_headers.rb +19 -0
- data/lib/securial/middleware/transform_request_keys.rb +35 -0
- data/lib/securial/middleware/transform_response_keys.rb +47 -0
- data/lib/securial/middleware.rb +3 -0
- data/lib/securial/security/request_rate_limiter.rb +45 -0
- data/lib/securial/security.rb +8 -0
- data/lib/securial/version.rb +1 -1
- data/lib/tasks/securial_routes.rake +26 -0
- metadata +44 -19
- data/lib/securial/config/validation/logger_validation.rb +0 -29
- data/lib/securial/config/validation/mailer_validation.rb +0 -24
- data/lib/securial/config/validation/password_validation.rb +0 -91
- data/lib/securial/config/validation/response_validation.rb +0 -37
- data/lib/securial/config/validation/roles_validation.rb +0 -32
- data/lib/securial/config/validation/security_validation.rb +0 -56
- data/lib/securial/config/validation/session_validation.rb +0 -87
@@ -1,67 +1,17 @@
|
|
1
1
|
require "securial/config/validation"
|
2
|
+
require "securial/config/signature"
|
2
3
|
require "securial/helpers/regex_helper"
|
3
4
|
|
4
5
|
module Securial
|
5
6
|
module Config
|
6
7
|
class Configuration
|
7
|
-
def self.default_config_attributes # rubocop:disable Metrics/MethodLength
|
8
|
-
{
|
9
|
-
# General configuration
|
10
|
-
app_name: "Securial",
|
11
|
-
|
12
|
-
# Logger configuration
|
13
|
-
log_to_file: !Rails.env.test?,
|
14
|
-
log_to_stdout: !Rails.env.test?,
|
15
|
-
log_file_level: :debug,
|
16
|
-
log_stdout_level: :debug,
|
17
|
-
|
18
|
-
# Roles configuration
|
19
|
-
admin_role: :admin,
|
20
|
-
|
21
|
-
session_expiration_duration: 3.minutes,
|
22
|
-
session_secret: "secret",
|
23
|
-
session_algorithm: :hs256,
|
24
|
-
session_refresh_token_expires_in: 1.week,
|
25
|
-
|
26
|
-
# Mailer configuration
|
27
|
-
mailer_sender: "no-reply@example.com",
|
28
|
-
mailer_sign_up_enabled: true,
|
29
|
-
mailer_sign_up_subject: "SECURIAL: Welcome to Our Service",
|
30
|
-
mailer_sign_in_enabled: true,
|
31
|
-
mailer_sign_in_subject: "SECURIAL: Sign In Notification",
|
32
|
-
mailer_update_account_enabled: true,
|
33
|
-
mailer_update_account_subject: "SECURIAL: Account Update Notification",
|
34
|
-
mailer_forgot_password_subject: "SECURIAL: Password Reset Instructions",
|
35
|
-
|
36
|
-
# Password configuration
|
37
|
-
password_min_length: 8,
|
38
|
-
password_max_length: 128,
|
39
|
-
password_complexity: Securial::Helpers::RegexHelper::PASSWORD_REGEX,
|
40
|
-
password_expires: true,
|
41
|
-
password_expires_in: 90.days,
|
42
|
-
reset_password_token_expires_in: 2.hours,
|
43
|
-
reset_password_token_secret: "reset_secret",
|
44
|
-
|
45
|
-
# Response configuration
|
46
|
-
response_keys_format: :snake_case,
|
47
|
-
timestamps_in_response: :all,
|
48
|
-
|
49
|
-
# Security configuration
|
50
|
-
security_headers: :strict,
|
51
|
-
rate_limiting_enabled: true,
|
52
|
-
rate_limit_requests_per_minute: 60,
|
53
|
-
rate_limit_response_status: 429,
|
54
|
-
rate_limit_response_message: "Too many requests, please try again later.",
|
55
|
-
}
|
56
|
-
end
|
57
|
-
|
58
8
|
def initialize
|
59
|
-
|
9
|
+
Securial::Config::Signature.default_config_attributes.each do |attr, default_value|
|
60
10
|
instance_variable_set("@#{attr}", default_value)
|
61
11
|
end
|
62
12
|
end
|
63
13
|
|
64
|
-
default_config_attributes.each_key do |attr|
|
14
|
+
Securial::Config::Signature.default_config_attributes.each_key do |attr|
|
65
15
|
define_method(attr) { instance_variable_get("@#{attr}") }
|
66
16
|
|
67
17
|
define_method("#{attr}=") do |value|
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Securial
|
2
|
+
module Config
|
3
|
+
module Signature
|
4
|
+
LOG_LEVELS = %i[debug info warn error fatal unknown].freeze
|
5
|
+
SESSION_ALGORITHMS = %i[hs256 hs384 hs512].freeze
|
6
|
+
SECURITY_HEADERS = %i[strict default none].freeze
|
7
|
+
TIMESTAMP_OPTIONS = %i[all admins_only none].freeze
|
8
|
+
RESPONSE_KEYS_FORMATS = %i[snake_case lowerCamelCase UpperCamelCase].freeze
|
9
|
+
|
10
|
+
extend self
|
11
|
+
|
12
|
+
def config_signature
|
13
|
+
[
|
14
|
+
general_signature,
|
15
|
+
logger_signature,
|
16
|
+
roles_signature,
|
17
|
+
session_signature,
|
18
|
+
mailer_signature,
|
19
|
+
password_signature,
|
20
|
+
response_signature,
|
21
|
+
security_signature,
|
22
|
+
].reduce({}, :merge)
|
23
|
+
end
|
24
|
+
|
25
|
+
def default_config_attributes
|
26
|
+
config_signature.transform_values do |options|
|
27
|
+
options[:default]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def general_signature
|
34
|
+
{
|
35
|
+
app_name: { type: String, required: true, default: "Securial" },
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def logger_signature
|
40
|
+
{
|
41
|
+
log_to_file: { type: [TrueClass, FalseClass], required: true, default: Rails.env.test? ? false : true },
|
42
|
+
log_file_level: { type: Symbol, required: "log_to_file", allowed_values: LOG_LEVELS, default: :debug },
|
43
|
+
log_to_stdout: { type: [TrueClass, FalseClass], required: true, default: Rails.env.test? ? false : true },
|
44
|
+
log_stdout_level: { type: Symbol, required: "log_to_stdout", allowed_values: LOG_LEVELS, default: :debug },
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def roles_signature
|
49
|
+
{
|
50
|
+
admin_role: { type: String, required: true, default: "admin" },
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def session_signature
|
55
|
+
{
|
56
|
+
session_expiration_duration: { type: ActiveSupport::Duration, required: true, default: 3.minutes },
|
57
|
+
session_secret: { type: String, required: true, default: "secret" },
|
58
|
+
session_algorithm: { type: Symbol, required: true, allowed_values: SESSION_ALGORITHMS, default: :hs256 },
|
59
|
+
session_refresh_token_expires_in: { type: ActiveSupport::Duration, required: true, default: 1.week },
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
def mailer_signature
|
64
|
+
{
|
65
|
+
mailer_sender: { type: String, required: true, default: "no-reply@example.com" },
|
66
|
+
mailer_sign_up_enabled: { type: [TrueClass, FalseClass], required: true, default: true },
|
67
|
+
mailer_sign_up_subject: { type: String, required: "mailer_sign_up_enabled", default: "SECURIAL: Welcome to Our Service" },
|
68
|
+
mailer_sign_in_enabled: { type: [TrueClass, FalseClass], required: true, default: true },
|
69
|
+
mailer_sign_in_subject: { type: String, required: "mailer_sign_in_enabled", default: "SECURIAL: Sign In Notification" },
|
70
|
+
mailer_update_account_enabled: { type: [TrueClass, FalseClass], required: true, default: true },
|
71
|
+
mailer_update_account_subject: { type: String, required: "mailer_update_account_enabled", default: "SECURIAL: Account Update Notification" },
|
72
|
+
mailer_forgot_password_subject: { type: String, required: true, default: "SECURIAL: Password Reset Instructions" },
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
def password_signature
|
77
|
+
{
|
78
|
+
password_min_length: { type: Numeric, required: true, default: 8 },
|
79
|
+
password_max_length: { type: Numeric, required: true, default: 128 },
|
80
|
+
password_complexity: { type: Regexp, required: true, default: Securial::Helpers::RegexHelper::PASSWORD_REGEX },
|
81
|
+
password_expires: { type: [TrueClass, FalseClass], required: true, default: true },
|
82
|
+
password_expires_in: { type: ActiveSupport::Duration, required: "password_expires", default: 90.days },
|
83
|
+
reset_password_token_expires_in: { type: ActiveSupport::Duration, required: true, default: 2.hours },
|
84
|
+
reset_password_token_secret: { type: String, required: true, default: "reset_secret" },
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
def response_signature
|
89
|
+
{
|
90
|
+
response_keys_format: { type: Symbol, required: true, allowed_values:
|
91
|
+
RESPONSE_KEYS_FORMATS, default: :snake_case, },
|
92
|
+
timestamps_in_response: { type: Symbol, required: true, allowed_values: TIMESTAMP_OPTIONS, default: :all },
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
def security_signature
|
97
|
+
{
|
98
|
+
security_headers: { type: Symbol, required: true, allowed_values: SECURITY_HEADERS, default: :strict },
|
99
|
+
rate_limiting_enabled: { type: [TrueClass, FalseClass], required: true, default: true },
|
100
|
+
rate_limit_requests_per_minute: { type: Numeric, required: "rate_limiting_enabled", default: 60 },
|
101
|
+
rate_limit_response_status: { type: Numeric, required: "rate_limiting_enabled", default: 429 },
|
102
|
+
rate_limit_response_message: { type: String, required: "rate_limiting_enabled", default: "Too many requests, please try again later." },
|
103
|
+
}
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -1,24 +1,68 @@
|
|
1
1
|
require "securial/logger"
|
2
|
-
require "securial/config/validation/logger_validation"
|
3
|
-
require "securial/config/validation/roles_validation"
|
4
|
-
require "securial/config/validation/session_validation"
|
5
|
-
require "securial/config/validation/mailer_validation"
|
6
|
-
require "securial/config/validation/password_validation"
|
7
|
-
require "securial/config/validation/response_validation"
|
8
|
-
require "securial/config/validation/security_validation"
|
9
2
|
|
10
3
|
module Securial
|
11
4
|
module Config
|
12
5
|
module Validation
|
13
6
|
class << self
|
14
7
|
def validate_all!(securial_config)
|
15
|
-
Securial::Config::
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
8
|
+
signature = Securial::Config::Signature.config_signature
|
9
|
+
|
10
|
+
validate_required_fields!(signature, securial_config)
|
11
|
+
validate_types_and_values!(signature, securial_config)
|
12
|
+
validate_password_lengths!(securial_config)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def validate_required_fields!(signature, config)
|
18
|
+
signature.each do |key, options|
|
19
|
+
value = config.send(key)
|
20
|
+
required = options[:required]
|
21
|
+
if required == true && value.nil?
|
22
|
+
raise_error("#{key} is required but not provided.")
|
23
|
+
elsif required.is_a?(String)
|
24
|
+
dynamic_required = config.send(required)
|
25
|
+
signature[key][:required] = dynamic_required
|
26
|
+
if dynamic_required && value.nil?
|
27
|
+
raise_error("#{key} is required but not provided when #{required} is true.")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_types_and_values!(signature, config)
|
34
|
+
signature.each do |key, options|
|
35
|
+
next unless signature[key][:required]
|
36
|
+
value = config.send(key)
|
37
|
+
types = Array(options[:type])
|
38
|
+
|
39
|
+
unless types.any? { |type| value.is_a?(type) }
|
40
|
+
raise_error("#{key} must be of type(s) #{types.join(', ')}, but got #{value.class}.")
|
41
|
+
end
|
42
|
+
|
43
|
+
if options[:type] == ActiveSupport::Duration && value <= 0
|
44
|
+
raise_error("#{key} must be a positive duration, but got #{value}.")
|
45
|
+
end
|
46
|
+
|
47
|
+
if options[:type] == Numeric && value < 0
|
48
|
+
raise_error("#{key} must be a non-negative numeric value, but got #{value}.")
|
49
|
+
end
|
50
|
+
|
51
|
+
if options[:allowed_values] && options[:allowed_values].exclude?(value)
|
52
|
+
raise_error("#{key} must be one of #{options[:allowed_values].join(', ')}, but got #{value}.")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def validate_password_lengths!(config)
|
58
|
+
if config.password_min_length > config.password_max_length
|
59
|
+
raise_error("password_min_length cannot be greater than password_max_length.")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def raise_error(msg)
|
64
|
+
Securial.logger.fatal msg
|
65
|
+
raise Securial::Error::Config::InvalidConfigurationError, msg
|
22
66
|
end
|
23
67
|
end
|
24
68
|
end
|
data/lib/securial/config.rb
CHANGED
data/lib/securial/engine.rb
CHANGED
@@ -5,7 +5,7 @@ module Securial
|
|
5
5
|
:password,
|
6
6
|
:password_confirmation,
|
7
7
|
:password_reset_token,
|
8
|
-
:reset_password_token
|
8
|
+
:reset_password_token,
|
9
9
|
]
|
10
10
|
end
|
11
11
|
|
@@ -16,10 +16,29 @@ module Securial
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
-
initializer "securial.
|
19
|
+
initializer "securial.security.request_rate_limiter" do |app|
|
20
|
+
if Securial.configuration.rate_limiting_enabled
|
21
|
+
Securial::Security::RequestRateLimiter.apply!
|
22
|
+
app.middleware.use Rack::Attack
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
initializer "securial.middleware.transform_request_keys" do |app|
|
27
|
+
app.middleware.use Securial::Middleware::TransformRequestKeys
|
28
|
+
end
|
29
|
+
|
30
|
+
initializer "securial.middleware.transform_response_keys" do |app|
|
31
|
+
app.middleware.use Securial::Middleware::TransformResponseKeys
|
32
|
+
end
|
33
|
+
|
34
|
+
initializer "securial.middleware.logger" do |app|
|
20
35
|
app.middleware.use Securial::Middleware::RequestTagLogger
|
21
36
|
end
|
22
37
|
|
38
|
+
initializer "securial.middleware.response_headers" do |app|
|
39
|
+
app.middleware.use Securial::Middleware::ResponseHeaders
|
40
|
+
end
|
41
|
+
|
23
42
|
initializer "securial.extend_application_controller" do
|
24
43
|
ActiveSupport.on_load(:action_controller_base) { include Securial::Identity }
|
25
44
|
ActiveSupport.on_load(:action_controller_api) { include Securial::Identity }
|
@@ -4,34 +4,6 @@ module Securial
|
|
4
4
|
class InvalidConfigurationError < Securial::Error::BaseError
|
5
5
|
default_message "Invalid configuration for Securial"
|
6
6
|
end
|
7
|
-
|
8
|
-
class LoggerValidationError < InvalidConfigurationError
|
9
|
-
default_message "Logger configuration validation failed"
|
10
|
-
end
|
11
|
-
|
12
|
-
class RolesValidationError < InvalidConfigurationError
|
13
|
-
default_message "Roles configuration validation failed"
|
14
|
-
end
|
15
|
-
|
16
|
-
class SessionValidationError < InvalidConfigurationError
|
17
|
-
default_message "Session configuration validation failed"
|
18
|
-
end
|
19
|
-
|
20
|
-
class MailerValidationError < InvalidConfigurationError
|
21
|
-
default_message "Mailer configuration validation failed"
|
22
|
-
end
|
23
|
-
|
24
|
-
class PasswordValidationError < InvalidConfigurationError
|
25
|
-
default_message "Password configuration validation failed"
|
26
|
-
end
|
27
|
-
|
28
|
-
class ResponseValidationError < InvalidConfigurationError
|
29
|
-
default_message "Response configuration validation failed"
|
30
|
-
end
|
31
|
-
|
32
|
-
class SecurityValidationError < InvalidConfigurationError
|
33
|
-
default_message "Security configuration validation failed"
|
34
|
-
end
|
35
7
|
end
|
36
8
|
end
|
37
9
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Securial
|
2
|
+
module Helpers
|
3
|
+
module KeyTransformer
|
4
|
+
module_function
|
5
|
+
|
6
|
+
def camelize(str, format)
|
7
|
+
case format
|
8
|
+
when :lowerCamelCase
|
9
|
+
str.to_s.camelize(:lower)
|
10
|
+
when :UpperCamelCase
|
11
|
+
str.to_s.camelize
|
12
|
+
else
|
13
|
+
str.to_s
|
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
|
33
|
+
end
|
data/lib/securial/helpers.rb
CHANGED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Securial
|
2
|
+
module Middleware
|
3
|
+
# This middleware removes sensitive headers from the request environment.
|
4
|
+
# It is designed to enhance security by ensuring that sensitive information
|
5
|
+
# is not inadvertently logged or processed.
|
6
|
+
class ResponseHeaders
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
status, headers, response = @app.call(env)
|
13
|
+
headers["X-Securial-Mounted"] = "true"
|
14
|
+
headers["X-Securial-Developer"] = "Aly Badawy - https://alybadawy.com | @alybadawy"
|
15
|
+
[status, headers, response]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Securial
|
2
|
+
module Middleware
|
3
|
+
# This middleware transforms request keys to a specified format.
|
4
|
+
# It uses the KeyTransformer helper to apply the transformation.
|
5
|
+
#
|
6
|
+
# It reads the request body if the content type is JSON and transforms
|
7
|
+
# the keys to underscore format. If the body is not valid JSON, it does nothing.
|
8
|
+
class TransformRequestKeys
|
9
|
+
def initialize(app)
|
10
|
+
@app = app
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
if env["CONTENT_TYPE"]&.include?("application/json")
|
15
|
+
req = Rack::Request.new(env)
|
16
|
+
if (req.body&.size || 0) > 0
|
17
|
+
raw = req.body.read
|
18
|
+
req.body.rewind
|
19
|
+
begin
|
20
|
+
parsed = JSON.parse(raw)
|
21
|
+
transformed = Securial::Helpers::KeyTransformer.deep_transform_keys(parsed) do |key|
|
22
|
+
Securial::Helpers::KeyTransformer.underscore(key)
|
23
|
+
end
|
24
|
+
env["rack.input"] = StringIO.new(JSON.dump(transformed))
|
25
|
+
env["rack.input"].rewind
|
26
|
+
rescue JSON::ParserError
|
27
|
+
# no-op
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
@app.call(env)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Securial
|
2
|
+
module Middleware
|
3
|
+
# This middleware transforms response keys to a specified format.
|
4
|
+
# It uses the KeyTransformer helper to apply the transformation.
|
5
|
+
#
|
6
|
+
# It reads the response body if the content type is JSON and transforms
|
7
|
+
# the keys to the specified format (default is lowerCamelCase).
|
8
|
+
class TransformResponseKeys
|
9
|
+
def initialize(app, format: :lowerCamelCase)
|
10
|
+
@app = app
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
status, headers, response = @app.call(env)
|
15
|
+
|
16
|
+
if json_response?(headers)
|
17
|
+
body = extract_body(response)
|
18
|
+
|
19
|
+
if body.present?
|
20
|
+
format = Securial.configuration.response_keys_format
|
21
|
+
transformed = Securial::Helpers::KeyTransformer.deep_transform_keys(JSON.parse(body)) do |key|
|
22
|
+
Securial::Helpers::KeyTransformer.camelize(key, format)
|
23
|
+
end
|
24
|
+
|
25
|
+
new_body = [JSON.generate(transformed)]
|
26
|
+
headers["Content-Length"] = new_body.first.bytesize.to_s
|
27
|
+
return [status, headers, new_body]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
[status, headers, response]
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def json_response?(headers)
|
37
|
+
headers["Content-Type"]&.include?("application/json")
|
38
|
+
end
|
39
|
+
|
40
|
+
def extract_body(response)
|
41
|
+
response_body = ""
|
42
|
+
response.each { |part| response_body << part }
|
43
|
+
response_body
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/securial/middleware.rb
CHANGED
@@ -0,0 +1,45 @@
|
|
1
|
+
require "rack/attack"
|
2
|
+
require "securial/config"
|
3
|
+
|
4
|
+
module Securial
|
5
|
+
module Security
|
6
|
+
module RequestRateLimiter
|
7
|
+
module_function
|
8
|
+
|
9
|
+
def apply! # rubocop:disable Metrics/MethodLength
|
10
|
+
resp_status = Securial.configuration.rate_limit_response_status
|
11
|
+
resp_message = Securial.configuration.rate_limit_response_message
|
12
|
+
throttle_configs = [
|
13
|
+
{ name: "securial/logins/ip", path: "sessions/login", key: ->(req) { req.ip } },
|
14
|
+
{ name: "securial/logins/email", path: "sessions/login", key: ->(req) { req.params["email_address"].to_s.downcase.strip } },
|
15
|
+
{ name: "securial/password_resets/ip", path: "password/forgot", key: ->(req) { req.ip } },
|
16
|
+
{ name: "securial/password_resets/email", path: "password/forgot", key: ->(req) { req.params["email_address"].to_s.downcase.strip } },
|
17
|
+
]
|
18
|
+
|
19
|
+
throttle_configs.each do |config|
|
20
|
+
Rack::Attack.throttle(config[:name],
|
21
|
+
limit: ->(_req) { Securial.configuration.rate_limit_requests_per_minute },
|
22
|
+
period: 1.minute
|
23
|
+
) do |req|
|
24
|
+
if req.path.include?(config[:path]) && req.post?
|
25
|
+
config[:key].call(req)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Custom response for throttled requests
|
31
|
+
Rack::Attack.throttled_responder = lambda do |request|
|
32
|
+
retry_after = (request.env["rack.attack.match_data"] || {})[:period]
|
33
|
+
[
|
34
|
+
resp_status,
|
35
|
+
{
|
36
|
+
"Content-Type" => "application/json",
|
37
|
+
"Retry-After" => retry_after.to_s,
|
38
|
+
},
|
39
|
+
[{ error: resp_message }.to_json],
|
40
|
+
]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/securial/version.rb
CHANGED
@@ -0,0 +1,26 @@
|
|
1
|
+
namespace :securial do
|
2
|
+
desc "Print all routes for the Securial engine"
|
3
|
+
task routes: :environment do
|
4
|
+
engine = Securial::Engine
|
5
|
+
routes = engine.routes
|
6
|
+
|
7
|
+
puts "Routes for Securial::Engine:"
|
8
|
+
all_routes = routes.routes.map do |route|
|
9
|
+
{
|
10
|
+
verb: route.verb,
|
11
|
+
path: route.path.spec.to_s.gsub("(.:format)", ""),
|
12
|
+
name: route.name,
|
13
|
+
controller: route.defaults[:controller],
|
14
|
+
action: route.defaults[:action],
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
# Print in a table format with adjusted column widths
|
19
|
+
puts "%-10s %-35s %-35s %-30s" % ["Verb", "Path", "Name", "Controller#Action"]
|
20
|
+
puts "-" * 110
|
21
|
+
all_routes.each do |r|
|
22
|
+
ctrl_action = [r[:controller], r[:action]].compact.join("#")
|
23
|
+
puts "%-10s %-35s %-35s %-30s" % [r[:verb], r[:path], r[:name], ctrl_action]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|