securial 0.6.1 → 0.7.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/app/controllers/securial/status_controller.rb +1 -1
- data/app/models/securial/role.rb +1 -1
- data/app/models/securial/user.rb +3 -3
- data/lib/generators/securial/install/templates/securial_initializer.erb +1 -1
- data/lib/securial/auth/auth_encoder.rb +39 -42
- data/lib/securial/auth/session_creator.rb +12 -12
- data/lib/securial/auth.rb +10 -0
- data/lib/securial/config/configuration.rb +3 -3
- data/lib/securial/config/validation.rb +29 -29
- data/lib/securial/config.rb +10 -0
- data/lib/securial/engine.rb +26 -15
- data/lib/securial/helpers/normalizing_helper.rb +11 -9
- data/lib/securial/helpers/regex_helper.rb +12 -10
- data/lib/securial/helpers.rb +9 -0
- data/lib/securial/inspectors/route_inspector.rb +11 -11
- data/lib/securial/inspectors.rb +8 -0
- data/lib/securial/logger/broadcaster.rb +48 -0
- data/lib/securial/logger/builder.rb +60 -0
- data/lib/securial/logger/colors.rb +14 -0
- data/lib/securial/logger.rb +4 -67
- data/lib/securial/middlewares/request_logger_tag.rb +19 -0
- data/lib/securial/{middleware → middlewares}/transform_request_keys.rb +2 -2
- data/lib/securial/{middleware → middlewares}/transform_response_keys.rb +15 -8
- data/lib/securial/middlewares.rb +10 -0
- data/lib/securial/security/request_rate_limiter.rb +68 -0
- data/lib/securial/security.rb +8 -0
- data/lib/securial/version.rb +1 -1
- data/lib/securial/version_checker.rb +31 -0
- data/lib/securial.rb +6 -0
- metadata +18 -13
- data/lib/securial/auth/_index.rb +0 -3
- data/lib/securial/config/_index.rb +0 -3
- data/lib/securial/helpers/_index.rb +0 -2
- data/lib/securial/inspectors/_index.rb +0 -1
- data/lib/securial/middleware/_index.rb +0 -3
- data/lib/securial/middleware/request_logger_tag.rb +0 -18
- data/lib/securial/rack_attack.rb +0 -48
- /data/lib/generators/securial/install/{views_generastor.rb → views_generator.rb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6955f517ae29e92b5a870f5e951782374d39f7c5eeb19de024d818106a57cc82
|
4
|
+
data.tar.gz: a91673febf7143ab6b245feab4154d1d511ca62b7f78f6a94c758b974c2860ed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 18a435b9714861bf5f76e516d54b5ed0a3ad4b9d1e192825f2766fab494f90d0579a89d5f5d812b67cc33bb876a174e9341980997ead6a6a5e134bce26637fed
|
7
|
+
data.tar.gz: f584db4b3d36d73d0a5fe83439ef75b409e2fe971337a3ad8b25bb9335727053fed1f272057062a2061b0b2b8c01d866f91e894570c77e4320277ea69ead268c
|
data/app/models/securial/role.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Securial
|
2
2
|
class Role < ApplicationRecord
|
3
|
-
normalizes :role_name, with: ->(e) { Securial::NormalizingHelper.normalize_role_name(e) }
|
3
|
+
normalizes :role_name, with: ->(e) { Securial::Helpers::NormalizingHelper.normalize_role_name(e) }
|
4
4
|
|
5
5
|
validates :role_name, presence: true, uniqueness: { case_sensitive: false }
|
6
6
|
|
data/app/models/securial/user.rb
CHANGED
@@ -2,7 +2,7 @@ module Securial
|
|
2
2
|
class User < ApplicationRecord
|
3
3
|
include Securial::PasswordResettable
|
4
4
|
|
5
|
-
normalizes :email_address, with: ->(e) { Securial::NormalizingHelper.normalize_email_address(e) }
|
5
|
+
normalizes :email_address, with: ->(e) { Securial::Helpers::NormalizingHelper.normalize_email_address(e) }
|
6
6
|
|
7
7
|
validates :email_address,
|
8
8
|
presence: true,
|
@@ -12,7 +12,7 @@ module Securial
|
|
12
12
|
maximum: 255,
|
13
13
|
},
|
14
14
|
format: {
|
15
|
-
with: Securial::RegexHelper::EMAIL_REGEX,
|
15
|
+
with: Securial::Helpers::RegexHelper::EMAIL_REGEX,
|
16
16
|
message: "must be a valid email address",
|
17
17
|
}
|
18
18
|
|
@@ -21,7 +21,7 @@ module Securial
|
|
21
21
|
uniqueness: { case_sensitive: false },
|
22
22
|
length: { maximum: 20 },
|
23
23
|
format: {
|
24
|
-
with: Securial::RegexHelper::USERNAME_REGEX,
|
24
|
+
with: Securial::Helpers::RegexHelper::USERNAME_REGEX,
|
25
25
|
message: "can only contain letters, numbers, underscores, and periods, but cannot start with a number or contain consecutive underscores or periods",
|
26
26
|
}
|
27
27
|
|
@@ -11,7 +11,7 @@ Securial.configure do |config|
|
|
11
11
|
config.log_to_stdout = true # Enable or disable logging to STDOUT
|
12
12
|
|
13
13
|
# Set log level for file logger: :debug, :info, :warn, :error, :fatal, or :unknown
|
14
|
-
config.log_file_level = :
|
14
|
+
config.log_file_level = :debug
|
15
15
|
|
16
16
|
# Set log level for stdout logger
|
17
17
|
config.log_stdout_level = :debug
|
@@ -1,56 +1,53 @@
|
|
1
1
|
module Securial
|
2
2
|
module Auth
|
3
3
|
module AuthEncoder
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
4
|
+
module_function
|
5
|
+
|
6
|
+
def encode(session)
|
7
|
+
return nil unless session && session.class == Securial::Session
|
8
|
+
|
9
|
+
base_payload = {
|
10
|
+
jti: session.id,
|
11
|
+
exp: expiry_duration.from_now.to_i,
|
12
|
+
sub: "session-access-token",
|
13
|
+
refresh_count: session.refresh_count,
|
14
|
+
}
|
15
|
+
|
16
|
+
session_payload = {
|
17
|
+
ip: session.ip_address,
|
18
|
+
agent: session.user_agent,
|
19
|
+
}
|
20
|
+
|
21
|
+
payload = base_payload.merge(session_payload)
|
22
|
+
begin
|
23
|
+
JWT.encode(payload, secret, algorithm, { kid: "hmac" })
|
24
|
+
rescue JWT::EncodeError => e
|
25
|
+
raise Errors::AuthEncodeError, "Failed to encode session: #{e.message}"
|
26
26
|
end
|
27
|
+
end
|
27
28
|
|
28
|
-
|
29
|
-
|
29
|
+
def decode(token)
|
30
|
+
begin
|
30
31
|
decoded = JWT.decode(token, secret, true, { algorithm: algorithm, verify_jti: true, iss: "securial" })
|
31
|
-
|
32
|
-
|
33
|
-
end
|
34
|
-
decoded.first
|
32
|
+
rescue JWT::DecodeError => e
|
33
|
+
raise Securial::Auth::Errors::AuthDecodeError, "Failed to decode session token: #{e.message}"
|
35
34
|
end
|
35
|
+
decoded.first
|
36
|
+
end
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
# Config::Validation.validate_session_secret!(Securial.configuration)
|
41
|
-
Securial.configuration.session_secret
|
42
|
-
end
|
38
|
+
def secret
|
39
|
+
Securial.configuration.session_secret
|
40
|
+
end
|
43
41
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
end
|
42
|
+
def algorithm
|
43
|
+
Securial.configuration.session_algorithm.to_s.upcase
|
44
|
+
end
|
48
45
|
|
49
|
-
|
50
|
-
|
51
|
-
Securial.configuration.session_expiration_duration
|
52
|
-
end
|
46
|
+
def expiry_duration
|
47
|
+
Securial.configuration.session_expiration_duration
|
53
48
|
end
|
49
|
+
|
50
|
+
private_class_method :secret, :algorithm, :expiry_duration
|
54
51
|
end
|
55
52
|
end
|
56
53
|
end
|
@@ -1,19 +1,19 @@
|
|
1
1
|
module Securial
|
2
2
|
module Auth
|
3
3
|
module SessionCreator
|
4
|
-
|
5
|
-
def create_session(user, request)
|
6
|
-
return nil unless user && user.persisted? && request.is_a?(ActionDispatch::Request)
|
4
|
+
module_function
|
7
5
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
)
|
15
|
-
|
16
|
-
|
6
|
+
def create_session(user, request)
|
7
|
+
return nil unless user && user.persisted? && request.is_a?(ActionDispatch::Request)
|
8
|
+
|
9
|
+
user.sessions.create!(
|
10
|
+
user_agent: request.user_agent,
|
11
|
+
ip_address: request.remote_ip,
|
12
|
+
refresh_token: SecureRandom.hex(64),
|
13
|
+
last_refreshed_at: Time.current,
|
14
|
+
refresh_token_expires_at: 1.week.from_now,
|
15
|
+
).tap do |session|
|
16
|
+
Current.session = session
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require "securial/auth/errors"
|
2
|
+
require_relative "auth/auth_encoder"
|
3
|
+
require_relative "auth/session_creator"
|
4
|
+
|
5
|
+
module Securial
|
6
|
+
module Auth
|
7
|
+
# This module serves as a namespace for authentication-related components.
|
8
|
+
# It requires all key auth files: Errors, AuthEncoder, SessionCreator.
|
9
|
+
end
|
10
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
require "securial/helpers"
|
2
2
|
module Securial
|
3
3
|
module Config
|
4
4
|
VALID_SESSION_ENCRYPTION_ALGORITHMS = [:hs256, :hs384, :hs512].freeze
|
@@ -11,7 +11,7 @@ module Securial
|
|
11
11
|
{
|
12
12
|
log_to_file: !Rails.env.test?,
|
13
13
|
log_to_stdout: !Rails.env.test?,
|
14
|
-
log_file_level: :
|
14
|
+
log_file_level: :debug,
|
15
15
|
log_stdout_level: :debug,
|
16
16
|
admin_role: :admin,
|
17
17
|
session_expiration_duration: 3.minutes,
|
@@ -21,7 +21,7 @@ module Securial
|
|
21
21
|
password_reset_email_subject: "SECURIAL: Password Reset Instructions",
|
22
22
|
password_min_length: 8,
|
23
23
|
password_max_length: 128,
|
24
|
-
password_complexity: Securial::RegexHelper::PASSWORD_REGEX,
|
24
|
+
password_complexity: Securial::Helpers::RegexHelper::PASSWORD_REGEX,
|
25
25
|
password_expires: true,
|
26
26
|
password_expires_in: 90.days,
|
27
27
|
reset_password_token_expires_in: 2.hours,
|
@@ -18,19 +18,19 @@ module Securial
|
|
18
18
|
def validate_admin_role!(config)
|
19
19
|
if config.admin_role.nil? || config.admin_role.to_s.strip.empty?
|
20
20
|
error_message = "Admin role is not set."
|
21
|
-
Securial
|
21
|
+
Securial.logger.fatal(error_message)
|
22
22
|
raise Securial::Config::Errors::ConfigAdminRoleError, error_message
|
23
23
|
end
|
24
24
|
|
25
25
|
unless config.admin_role.is_a?(Symbol) || config.admin_role.is_a?(String)
|
26
26
|
error_message = "Admin role must be a Symbol or String."
|
27
|
-
Securial
|
27
|
+
Securial.logger.fatal(error_message)
|
28
28
|
raise Securial::Config::Errors::ConfigAdminRoleError, error_message
|
29
29
|
end
|
30
30
|
|
31
31
|
if config.admin_role.to_s.pluralize.downcase == "accounts"
|
32
32
|
error_message = "The admin role cannot be 'account' or 'accounts' as it conflicts with the default routes."
|
33
|
-
Securial
|
33
|
+
Securial.logger.fatal(error_message)
|
34
34
|
raise Securial::Config::Errors::ConfigAdminRoleError, error_message
|
35
35
|
end
|
36
36
|
end
|
@@ -44,16 +44,16 @@ module Securial
|
|
44
44
|
def validate_session_expiry_duration!(config)
|
45
45
|
if config.session_expiration_duration.nil?
|
46
46
|
error_message = "Session expiration duration is not set."
|
47
|
-
Securial
|
47
|
+
Securial.logger.fatal(error_message)
|
48
48
|
raise Securial::Config::Errors::ConfigSessionExpirationDurationError, error_message
|
49
49
|
end
|
50
50
|
if config.session_expiration_duration.class != ActiveSupport::Duration
|
51
51
|
error_message = "Session expiration duration must be an ActiveSupport::Duration."
|
52
|
-
Securial
|
52
|
+
Securial.logger.fatal(error_message)
|
53
53
|
raise Securial::Config::Errors::ConfigSessionExpirationDurationError, error_message
|
54
54
|
end
|
55
55
|
if config.session_expiration_duration <= 0
|
56
|
-
Securial
|
56
|
+
Securial.logger.fatal("Session expiration duration must be greater than 0.")
|
57
57
|
raise Securial::Config::Errors::ConfigSessionExpirationDurationError, "Session expiration duration must be greater than 0."
|
58
58
|
end
|
59
59
|
end
|
@@ -61,18 +61,18 @@ module Securial
|
|
61
61
|
def validate_session_algorithm!(config)
|
62
62
|
if config.session_algorithm.blank?
|
63
63
|
error_message = "Session algorithm is not set."
|
64
|
-
Securial
|
64
|
+
Securial.logger.fatal(error_message)
|
65
65
|
raise Securial::Config::Errors::ConfigSessionAlgorithmError, error_message
|
66
66
|
end
|
67
67
|
unless config.session_algorithm.is_a?(Symbol)
|
68
68
|
error_message = "Session algorithm must be a Symbol."
|
69
|
-
Securial
|
69
|
+
Securial.logger.fatal(error_message)
|
70
70
|
raise Securial::Config::Errors::ConfigSessionAlgorithmError, error_message
|
71
71
|
end
|
72
72
|
valid_algorithms = Securial::Config::VALID_SESSION_ENCRYPTION_ALGORITHMS
|
73
73
|
unless valid_algorithms.include?(config.session_algorithm)
|
74
74
|
error_message = "Invalid session algorithm. Valid options are: #{valid_algorithms.map(&:inspect).join(', ')}."
|
75
|
-
Securial
|
75
|
+
Securial.logger.fatal(error_message)
|
76
76
|
raise Securial::Config::Errors::ConfigSessionAlgorithmError, error_message
|
77
77
|
end
|
78
78
|
end
|
@@ -80,12 +80,12 @@ module Securial
|
|
80
80
|
def validate_session_secret!(config)
|
81
81
|
if config.session_secret.blank?
|
82
82
|
error_message = "Session secret is not set."
|
83
|
-
Securial
|
83
|
+
Securial.logger.fatal(error_message)
|
84
84
|
raise Securial::Config::Errors::ConfigSessionSecretError, error_message
|
85
85
|
end
|
86
86
|
unless config.session_secret.is_a?(String)
|
87
87
|
error_message = "Session secret must be a String."
|
88
|
-
Securial
|
88
|
+
Securial.logger.fatal(error_message)
|
89
89
|
raise Securial::Config::Errors::ConfigSessionSecretError, error_message
|
90
90
|
end
|
91
91
|
end
|
@@ -93,12 +93,12 @@ module Securial
|
|
93
93
|
def validate_mailer_sender!(config)
|
94
94
|
if config.mailer_sender.blank?
|
95
95
|
error_message = "Mailer sender is not set."
|
96
|
-
Securial
|
96
|
+
Securial.logger.fatal(error_message)
|
97
97
|
raise Securial::Config::Errors::ConfigMailerSenderError, error_message
|
98
98
|
end
|
99
99
|
if config.mailer_sender !~ URI::MailTo::EMAIL_REGEXP
|
100
100
|
error_message = "Mailer sender is not a valid email address."
|
101
|
-
Securial
|
101
|
+
Securial.logger.fatal(error_message)
|
102
102
|
raise Securial::Config::Errors::ConfigMailerSenderError, error_message
|
103
103
|
end
|
104
104
|
end
|
@@ -114,12 +114,12 @@ module Securial
|
|
114
114
|
def validate_password_reset_token!(config)
|
115
115
|
if config.reset_password_token_secret.blank?
|
116
116
|
error_message = "Reset password token secret is not set."
|
117
|
-
Securial
|
117
|
+
Securial.logger.fatal(error_message)
|
118
118
|
raise Securial::Config::Errors::ConfigPasswordError, error_message
|
119
119
|
end
|
120
120
|
unless config.reset_password_token_secret.is_a?(String)
|
121
121
|
error_message = "Reset password token secret must be a String."
|
122
|
-
Securial
|
122
|
+
Securial.logger.fatal(error_message)
|
123
123
|
raise Securial::Config::Errors::ConfigPasswordError, error_message
|
124
124
|
end
|
125
125
|
end
|
@@ -127,12 +127,12 @@ module Securial
|
|
127
127
|
def validate_password_reset_subject!(config)
|
128
128
|
if config.password_reset_email_subject.blank?
|
129
129
|
error_message = "Password reset email subject is not set."
|
130
|
-
Securial
|
130
|
+
Securial.logger.fatal(error_message)
|
131
131
|
raise Securial::Config::Errors::ConfigPasswordError, error_message
|
132
132
|
end
|
133
133
|
unless config.password_reset_email_subject.is_a?(String)
|
134
134
|
error_message = "Password reset email subject must be a String."
|
135
|
-
Securial
|
135
|
+
Securial.logger.fatal(error_message)
|
136
136
|
raise Securial::Config::Errors::ConfigPasswordError, error_message
|
137
137
|
end
|
138
138
|
end
|
@@ -140,12 +140,12 @@ module Securial
|
|
140
140
|
def validate_password_min_max_length!(config)
|
141
141
|
unless config.password_min_length.is_a?(Integer) && config.password_min_length > 0
|
142
142
|
error_message = "Password minimum length must be a positive integer."
|
143
|
-
Securial
|
143
|
+
Securial.logger.fatal(error_message)
|
144
144
|
raise Securial::Config::Errors::ConfigPasswordError, error_message
|
145
145
|
end
|
146
146
|
unless config.password_max_length.is_a?(Integer) && config.password_max_length >= config.password_min_length
|
147
147
|
error_message = "Password maximum length must be an integer greater than or equal to the minimum length."
|
148
|
-
Securial
|
148
|
+
Securial.logger.fatal(error_message)
|
149
149
|
raise Securial::Config::Errors::ConfigPasswordError, error_message
|
150
150
|
end
|
151
151
|
end
|
@@ -153,7 +153,7 @@ module Securial
|
|
153
153
|
def validate_password_complexity!(config)
|
154
154
|
if config.password_complexity.nil? || !config.password_complexity.is_a?(Regexp)
|
155
155
|
error_message = "Password complexity regex is not set or is not a valid Regexp."
|
156
|
-
Securial
|
156
|
+
Securial.logger.fatal(error_message)
|
157
157
|
raise Securial::Config::Errors::ConfigPasswordError, error_message
|
158
158
|
end
|
159
159
|
end
|
@@ -161,7 +161,7 @@ module Securial
|
|
161
161
|
def validate_password_expiration!(config)
|
162
162
|
unless config.password_expires.is_a?(TrueClass) || config.password_expires.is_a?(FalseClass)
|
163
163
|
error_message = "Password expiration must be a boolean value."
|
164
|
-
Securial
|
164
|
+
Securial.logger.fatal(error_message)
|
165
165
|
raise Securial::Config::Errors::ConfigPasswordError, error_message
|
166
166
|
end
|
167
167
|
|
@@ -171,7 +171,7 @@ module Securial
|
|
171
171
|
config.password_expires_in <= 0
|
172
172
|
)
|
173
173
|
error_message = "Password expiration duration is not set or is not a valid ActiveSupport::Duration."
|
174
|
-
Securial
|
174
|
+
Securial.logger.fatal(error_message)
|
175
175
|
raise Securial::Config::Errors::ConfigPasswordError, error_message
|
176
176
|
end
|
177
177
|
end
|
@@ -185,7 +185,7 @@ module Securial
|
|
185
185
|
valid_formats = Securial::Config::VALID_RESPONSE_KEYS_FORMATS
|
186
186
|
unless valid_formats.include?(config.response_keys_format)
|
187
187
|
error_message = "Invalid response_keys_format option. Valid options are: #{valid_formats.map(&:inspect).join(', ')}."
|
188
|
-
Securial
|
188
|
+
Securial.logger.fatal(error_message)
|
189
189
|
raise Securial::Config::Errors::ConfigResponseError, error_message
|
190
190
|
end
|
191
191
|
end
|
@@ -194,7 +194,7 @@ module Securial
|
|
194
194
|
valid_options = Securial::Config::VALID_TIMESTAMP_OPTIONS
|
195
195
|
unless valid_options.include?(config.timestamps_in_response)
|
196
196
|
error_message = "Invalid timestamps_in_response option. Valid options are: #{valid_options.map(&:inspect).join(', ')}."
|
197
|
-
Securial
|
197
|
+
Securial.logger.fatal(error_message)
|
198
198
|
raise Securial::Config::Errors::ConfigResponseError, error_message
|
199
199
|
end
|
200
200
|
end
|
@@ -208,7 +208,7 @@ module Securial
|
|
208
208
|
valid_options = Securial::Config::VALID_SECURITY_HEADERS
|
209
209
|
unless valid_options.include?(config.security_headers)
|
210
210
|
error_message = "Invalid security_headers option. Valid options are: #{valid_options.map(&:inspect).join(', ')}."
|
211
|
-
Securial
|
211
|
+
Securial.logger.fatal(error_message)
|
212
212
|
raise Securial::Config::Errors::ConfigSecurityError, error_message
|
213
213
|
end
|
214
214
|
end
|
@@ -216,7 +216,7 @@ module Securial
|
|
216
216
|
def validate_rate_limiting!(config) # rubocop:disable Metrics/MethodLength
|
217
217
|
unless config.rate_limiting_enabled.is_a?(TrueClass) || config.rate_limiting_enabled.is_a?(FalseClass)
|
218
218
|
error_message = "rate_limiting_enabled must be a boolean value."
|
219
|
-
Securial
|
219
|
+
Securial.logger.fatal(error_message)
|
220
220
|
raise Securial::Config::Errors::ConfigSecurityError, error_message
|
221
221
|
end
|
222
222
|
|
@@ -227,19 +227,19 @@ module Securial
|
|
227
227
|
config.rate_limit_requests_per_minute > 0
|
228
228
|
|
229
229
|
error_message = "rate_limit_requests_per_minute must be a positive integer when rate limiting is enabled."
|
230
|
-
Securial
|
230
|
+
Securial.logger.fatal(error_message)
|
231
231
|
raise Securial::Config::Errors::ConfigSecurityError, error_message
|
232
232
|
end
|
233
233
|
|
234
234
|
unless config.rate_limit_response_status.is_a?(Integer) && config.rate_limit_response_status.between?(400, 599)
|
235
235
|
error_message = "rate_limit_response_status must be an HTTP status code between 4xx and 5xx."
|
236
|
-
Securial
|
236
|
+
Securial.logger.fatal(error_message)
|
237
237
|
raise Securial::Config::Errors::ConfigSecurityError, error_message
|
238
238
|
end
|
239
239
|
|
240
240
|
unless config.rate_limit_response_message.is_a?(String) && !config.rate_limit_response_message.strip.empty?
|
241
241
|
error_message = "rate_limit_response_message must be a non-empty String."
|
242
|
-
Securial
|
242
|
+
Securial.logger.fatal(error_message)
|
243
243
|
raise Securial::Config::Errors::ConfigSecurityError, error_message
|
244
244
|
end
|
245
245
|
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require "securial/config/configuration"
|
2
|
+
require "securial/config/validation"
|
3
|
+
require "securial/config/errors"
|
4
|
+
|
5
|
+
module Securial
|
6
|
+
module Config
|
7
|
+
# This module acts as a namespace for configuration-related components.
|
8
|
+
# It requires all key config files: Configuration, Validation, Errors.
|
9
|
+
end
|
10
|
+
end
|
data/lib/securial/engine.rb
CHANGED
@@ -1,13 +1,15 @@
|
|
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"
|
7
|
-
|
8
|
-
require_relative "./middleware/_index"
|
9
1
|
require "jwt"
|
10
2
|
|
3
|
+
require "securial/logger"
|
4
|
+
require "securial/key_transformer"
|
5
|
+
require "securial/config"
|
6
|
+
require "securial/helpers"
|
7
|
+
require "securial/auth"
|
8
|
+
require "securial/inspectors"
|
9
|
+
require "securial/middlewares"
|
10
|
+
require "securial/security"
|
11
|
+
require "securial/version_checker"
|
12
|
+
|
11
13
|
module Securial
|
12
14
|
class Engine < ::Rails::Engine
|
13
15
|
isolate_namespace Securial
|
@@ -53,13 +55,22 @@ module Securial
|
|
53
55
|
end
|
54
56
|
|
55
57
|
initializer "securial.middleware" do |app|
|
56
|
-
middleware.use Securial::
|
57
|
-
middleware.use Securial::
|
58
|
-
middleware.use Securial::
|
58
|
+
middleware.use Securial::Middlewares::RequestLoggerTag
|
59
|
+
middleware.use Securial::Middlewares::TransformRequestKeys
|
60
|
+
middleware.use Securial::Middlewares::TransformResponseKeys
|
61
|
+
end
|
62
|
+
|
63
|
+
initializer "securial.security.request_rate_limiter" do |app|
|
59
64
|
if Securial.configuration.rate_limiting_enabled
|
60
|
-
|
61
|
-
|
62
|
-
|
65
|
+
Securial::Security::RequestRateLimiter.apply!
|
66
|
+
Rails.application.config.middleware.use Rack::Attack
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
initializer "securial.version_check" do
|
72
|
+
config.after_initialize do
|
73
|
+
Securial::VersionChecker.check_latest_version
|
63
74
|
end
|
64
75
|
end
|
65
76
|
|
@@ -86,7 +97,7 @@ module Securial
|
|
86
97
|
private
|
87
98
|
|
88
99
|
def log(message)
|
89
|
-
Securial
|
100
|
+
Securial.logger.info("[Securial] #{message}")
|
90
101
|
end
|
91
102
|
end
|
92
103
|
end
|
@@ -1,17 +1,19 @@
|
|
1
1
|
module Securial
|
2
|
-
module
|
3
|
-
|
2
|
+
module Helpers
|
3
|
+
module NormalizingHelper
|
4
|
+
module_function
|
4
5
|
|
5
|
-
|
6
|
-
|
6
|
+
def normalize_email_address(email)
|
7
|
+
return "" if email.empty?
|
7
8
|
|
8
|
-
|
9
|
-
|
9
|
+
email.strip.downcase
|
10
|
+
end
|
10
11
|
|
11
|
-
|
12
|
-
|
12
|
+
def normalize_role_name(role_name)
|
13
|
+
return "" if role_name.empty?
|
13
14
|
|
14
|
-
|
15
|
+
role_name.strip.downcase.titleize
|
16
|
+
end
|
15
17
|
end
|
16
18
|
end
|
17
19
|
end
|
@@ -1,16 +1,18 @@
|
|
1
1
|
module Securial
|
2
|
-
module
|
3
|
-
|
4
|
-
|
5
|
-
|
2
|
+
module Helpers
|
3
|
+
module RegexHelper
|
4
|
+
EMAIL_REGEX = URI::MailTo::EMAIL_REGEXP
|
5
|
+
USERNAME_REGEX = /\A(?![0-9])[a-zA-Z](?:[a-zA-Z0-9]|[._](?![._]))*[a-zA-Z0-9]\z/
|
6
|
+
PASSWORD_REGEX = %r{\A(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])[a-zA-Z].*\z}
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
class << self
|
9
|
+
def valid_email?(email)
|
10
|
+
email.match?(EMAIL_REGEX)
|
11
|
+
end
|
11
12
|
|
12
|
-
|
13
|
-
|
13
|
+
def valid_username?(username)
|
14
|
+
username.match?(USERNAME_REGEX)
|
15
|
+
end
|
14
16
|
end
|
15
17
|
end
|
16
18
|
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require "securial/helpers/normalizing_helper"
|
2
|
+
require "securial/helpers/regex_helper"
|
3
|
+
|
4
|
+
module Securial
|
5
|
+
module Helpers
|
6
|
+
# This module acts as a namespace for helper modules.
|
7
|
+
# It requires all helper modules to make them available for consumers.
|
8
|
+
end
|
9
|
+
end
|
@@ -17,27 +17,27 @@ module Securial
|
|
17
17
|
|
18
18
|
# rubocop:disable Rails/Output
|
19
19
|
def print_headers(filtered, controller)
|
20
|
-
Securial
|
21
|
-
Securial
|
22
|
-
Securial
|
23
|
-
Securial
|
24
|
-
Securial
|
25
|
-
Securial
|
26
|
-
Securial
|
20
|
+
Securial.logger.debug "Securial engine routes:"
|
21
|
+
Securial.logger.debug "Total routes: #{filtered.size}"
|
22
|
+
Securial.logger.debug "Filtered by controller: #{controller}" if controller
|
23
|
+
Securial.logger.debug "Filtered routes: #{filtered.size}" if controller
|
24
|
+
Securial.logger.debug "-" * 120
|
25
|
+
Securial.logger.debug "#{'Verb'.ljust(8)} #{'Path'.ljust(45)} #{'Controller#Action'.ljust(40)} Name"
|
26
|
+
Securial.logger.debug "-" * 120
|
27
27
|
end
|
28
28
|
|
29
29
|
def print_details(filtered, controller) # rubocop:disable Rails/Output
|
30
30
|
if filtered.empty?
|
31
31
|
if controller
|
32
|
-
Securial
|
32
|
+
Securial.logger.debug "No routes found for controller: #{controller}"
|
33
33
|
else
|
34
|
-
Securial
|
34
|
+
Securial.logger.debug "No routes found for Securial engine"
|
35
35
|
end
|
36
|
-
Securial
|
36
|
+
Securial.logger.debug "-" * 120
|
37
37
|
return
|
38
38
|
end
|
39
39
|
|
40
|
-
Securial
|
40
|
+
Securial.logger.debug filtered.map { |r|
|
41
41
|
name = r.name || ""
|
42
42
|
verb = r.verb.to_s.ljust(8)
|
43
43
|
path = r.path.spec.to_s.sub(/\(\.:format\)/, "").ljust(45)
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Securial
|
2
|
+
module Logger
|
3
|
+
class Broadcaster
|
4
|
+
def initialize(*targets)
|
5
|
+
@targets = targets
|
6
|
+
end
|
7
|
+
|
8
|
+
::Logger::Severity.constants.each do |severity|
|
9
|
+
method = severity.downcase
|
10
|
+
define_method(method) do |*args, &block|
|
11
|
+
@targets.each do |logger|
|
12
|
+
if logger.level <= ::Logger::Severity.const_get(severity)
|
13
|
+
logger.send(method, *args, &block)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def add(severity, message = nil, progname = nil, &block)
|
20
|
+
@targets.each do |logger|
|
21
|
+
if logger.level <= severity.to_i
|
22
|
+
logger.add(severity, message, progname, &block)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def <<(msg)
|
28
|
+
@targets.each { |logger| logger << msg if logger.respond_to?(:<<) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def close
|
32
|
+
@targets.each { |logger| logger.close if logger.respond_to?(:close) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def flush
|
36
|
+
@targets.each { |logger| logger.flush if logger.respond_to?(:flush) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def formatter
|
40
|
+
@targets.first.formatter if @targets.any?
|
41
|
+
end
|
42
|
+
|
43
|
+
def formatter=(formatter)
|
44
|
+
@targets.each { |logger| logger.formatter = formatter }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "active_support/logger"
|
3
|
+
require "active_support/tagged_logging"
|
4
|
+
require_relative "colors"
|
5
|
+
require_relative "broadcaster"
|
6
|
+
|
7
|
+
module Securial
|
8
|
+
module Logger
|
9
|
+
def self.build # rubocop:disable Metrics/MethodLength
|
10
|
+
loggers = []
|
11
|
+
colorize_stdout = false
|
12
|
+
|
13
|
+
unless Securial.configuration.log_to_file == false
|
14
|
+
file_logger = ::Logger.new(Rails.root.join("log", "securial.log"))
|
15
|
+
file_logger.level = resolve_level(Securial.configuration.log_file_level)
|
16
|
+
file_logger.formatter = ::Logger::Formatter.new
|
17
|
+
loggers << file_logger
|
18
|
+
end
|
19
|
+
|
20
|
+
unless Securial.configuration.log_to_stdout == false
|
21
|
+
stdout_logger = ::Logger.new(STDOUT)
|
22
|
+
stdout_logger.level = resolve_level(Securial.configuration.log_stdout_level)
|
23
|
+
stdout_logger.formatter = proc do |severity, timestamp, progname, msg|
|
24
|
+
color = COLORS[severity] || CLEAR
|
25
|
+
padded = severity.ljust(SEVERITY_WIDTH)
|
26
|
+
formatted = "#{timestamp.strftime("%Y-%m-%d %H:%M:%S")} #{padded} -- : #{msg}\n"
|
27
|
+
"#{color}#{formatted}#{CLEAR}"
|
28
|
+
end
|
29
|
+
colorize_stdout = true
|
30
|
+
loggers << stdout_logger
|
31
|
+
end
|
32
|
+
|
33
|
+
if loggers.empty?
|
34
|
+
null_logger = ::Logger.new(IO::NULL)
|
35
|
+
return ActiveSupport::TaggedLogging.new(null_logger)
|
36
|
+
end
|
37
|
+
|
38
|
+
broadcaster = Broadcaster.new(*loggers)
|
39
|
+
tagged_logger = ActiveSupport::TaggedLogging.new(broadcaster)
|
40
|
+
|
41
|
+
if colorize_stdout && !Rails.env.test?
|
42
|
+
tagged_logger.formatter = proc do |severity, timestamp, progname, msg|
|
43
|
+
color = COLORS[severity] || CLEAR
|
44
|
+
padded = severity.ljust(SEVERITY_WIDTH)
|
45
|
+
formatted = "#{timestamp.strftime("%Y-%m-%d %H:%M:%S")} #{padded} -- : #{msg}\n"
|
46
|
+
"#{color}#{formatted}#{CLEAR}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
tagged_logger
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.resolve_level(level)
|
54
|
+
return ::Logger::INFO if level.nil?
|
55
|
+
::Logger.const_get(level.to_s.upcase)
|
56
|
+
rescue NameError
|
57
|
+
::Logger::INFO
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Securial
|
2
|
+
module Logger
|
3
|
+
COLORS = {
|
4
|
+
"DEBUG" => "\e[36m", # cyan
|
5
|
+
"INFO" => "\e[32m", # green
|
6
|
+
"WARN" => "\e[33m", # yellow
|
7
|
+
"ERROR" => "\e[31m", # red
|
8
|
+
"FATAL" => "\e[35m", # magenta
|
9
|
+
"UNKNOWN" => "\e[37m", # white
|
10
|
+
}.freeze
|
11
|
+
CLEAR = "\e[0m"
|
12
|
+
SEVERITY_WIDTH = 5
|
13
|
+
end
|
14
|
+
end
|
data/lib/securial/logger.rb
CHANGED
@@ -1,71 +1,8 @@
|
|
1
|
-
|
2
|
-
require "active_support/logger"
|
3
|
-
require "active_support/tagged_logging"
|
1
|
+
require_relative "logger/builder"
|
4
2
|
|
5
3
|
module Securial
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
unless Securial.configuration.log_to_file == false
|
11
|
-
log_file = Rails.root.join("log", "securial.log").open("a")
|
12
|
-
log_file.sync = true
|
13
|
-
outputs << log_file
|
14
|
-
end
|
15
|
-
|
16
|
-
unless Securial.configuration.log_to_stdout == false
|
17
|
-
outputs << STDOUT
|
18
|
-
end
|
19
|
-
|
20
|
-
if outputs.empty?
|
21
|
-
null_logger = ::Logger.new(IO::NULL)
|
22
|
-
return ActiveSupport::TaggedLogging.new(null_logger)
|
23
|
-
end
|
24
|
-
|
25
|
-
logger = ActiveSupport::Logger.new(MultiIO.new(*outputs))
|
26
|
-
logger.level = resolve_log_level
|
27
|
-
logger.formatter = ::Logger::Formatter.new
|
28
|
-
|
29
|
-
ActiveSupport::TaggedLogging.new(logger)
|
30
|
-
end
|
31
|
-
|
32
|
-
def self.resolve_log_level
|
33
|
-
file_level = Securial.configuration.log_file_level
|
34
|
-
stdout_level = Securial.configuration.log_stdout_level
|
35
|
-
|
36
|
-
# Use the lower (more verbose) level of the two
|
37
|
-
levels = [file_level, stdout_level].compact.map do |lvl|
|
38
|
-
begin
|
39
|
-
::Logger.const_get(lvl.to_s.upcase)
|
40
|
-
rescue NameError
|
41
|
-
nil
|
42
|
-
end
|
43
|
-
end.compact
|
44
|
-
|
45
|
-
levels.min || ::Logger::INFO
|
46
|
-
end
|
47
|
-
|
48
|
-
private
|
49
|
-
|
50
|
-
class MultiIO
|
51
|
-
def initialize(*targets)
|
52
|
-
@targets = targets
|
53
|
-
end
|
54
|
-
|
55
|
-
def write(*args)
|
56
|
-
@targets.each { |t| t.write(*args) }
|
57
|
-
end
|
58
|
-
|
59
|
-
def close
|
60
|
-
@targets.each do |t|
|
61
|
-
next if [STDOUT, STDERR].include?(t)
|
62
|
-
t.close
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
def flush
|
67
|
-
@targets.each { |t| t.flush if t.respond_to?(:flush) }
|
68
|
-
end
|
69
|
-
end
|
4
|
+
module Logger
|
5
|
+
# This module serves as a namespace for logging functionality.
|
6
|
+
# It requires the Logger::Builder to provide logger building capabilities.
|
70
7
|
end
|
71
8
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Securial
|
2
|
+
module Middlewares
|
3
|
+
class RequestLoggerTag
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
request_id = env["action_dispatch.request_id"] || env["HTTP_X_REQUEST_ID"]
|
10
|
+
tags = ["Securial"]
|
11
|
+
tags << request_id if request_id
|
12
|
+
|
13
|
+
logger = Securial.logger || Rails.logger || ::Logger.new(IO::NULL)
|
14
|
+
tagged_logger = logger.is_a?(ActiveSupport::TaggedLogging) ? logger : ActiveSupport::TaggedLogging.new(logger)
|
15
|
+
tagged_logger.tagged(*tags) { @app.call(env) }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -1,8 +1,7 @@
|
|
1
|
-
# lib/securial/middleware/transform_response_keys.rb
|
2
1
|
require "json"
|
3
2
|
|
4
3
|
module Securial
|
5
|
-
module
|
4
|
+
module Middlewares
|
6
5
|
class TransformResponseKeys
|
7
6
|
def initialize(app)
|
8
7
|
@app = app
|
@@ -16,13 +15,21 @@ module Securial
|
|
16
15
|
|
17
16
|
if body.present?
|
18
17
|
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
18
|
|
23
|
-
|
24
|
-
|
25
|
-
|
19
|
+
# Only transform if not snake_case
|
20
|
+
if format != :snake_case
|
21
|
+
begin
|
22
|
+
transformed = Securial::KeyTransformer.deep_transform_keys(JSON.parse(body)) do |key|
|
23
|
+
Securial::KeyTransformer.camelize(key, format)
|
24
|
+
end
|
25
|
+
|
26
|
+
new_body = [JSON.generate(transformed)]
|
27
|
+
headers["Content-Length"] = new_body.first.bytesize.to_s
|
28
|
+
return [status, headers, new_body]
|
29
|
+
rescue JSON::ParserError
|
30
|
+
# If not valid JSON, fall through and return original response
|
31
|
+
end
|
32
|
+
end
|
26
33
|
end
|
27
34
|
end
|
28
35
|
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require_relative "middlewares/transform_request_keys"
|
2
|
+
require_relative "middlewares/transform_response_keys"
|
3
|
+
require_relative "middlewares/request_logger_tag"
|
4
|
+
|
5
|
+
module Securial
|
6
|
+
module Middleware
|
7
|
+
# This module serves as a namespace for middlewares.
|
8
|
+
# It requires the necessary middleware files to provide functionality.
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require "rack/attack"
|
2
|
+
require "securial/config"
|
3
|
+
require "securial/logger"
|
4
|
+
|
5
|
+
module Securial
|
6
|
+
module Security
|
7
|
+
module RequestRateLimiter
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def apply! # rubocop:disable Metrics/MethodLength
|
11
|
+
resp_status = Securial.configuration.rate_limit_response_status
|
12
|
+
resp_message = Securial.configuration.rate_limit_response_message
|
13
|
+
# Throttle login attempts by IP
|
14
|
+
Rack::Attack.throttle("securial/logins/ip",
|
15
|
+
limit: ->(_req) { Securial.configuration.rate_limit_requests_per_minute },
|
16
|
+
period: 1.minute
|
17
|
+
) do |req|
|
18
|
+
if req.path.include?("sessions/login") && req.post?
|
19
|
+
req.ip
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Throttle login attempts by username/email
|
24
|
+
Rack::Attack.throttle("securial/logins/email",
|
25
|
+
limit: ->(_req) { Securial.configuration.rate_limit_requests_per_minute },
|
26
|
+
period: 1.minute
|
27
|
+
) do |req|
|
28
|
+
if req.path.include?("sessions/login") && req.post?
|
29
|
+
req.params["email_address"].to_s.downcase.strip
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Throttle password reset requests by IP
|
34
|
+
Rack::Attack.throttle("securial/password_resets/ip",
|
35
|
+
limit: ->(_req) { Securial.configuration.rate_limit_requests_per_minute },
|
36
|
+
period: 1.minute
|
37
|
+
) do |req|
|
38
|
+
if req.path.include?("password/forgot") && req.post?
|
39
|
+
req.ip
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Throttle password reset requests by email
|
44
|
+
Rack::Attack.throttle("securial/password_resets/email",
|
45
|
+
limit: ->(_req) { Securial.configuration.rate_limit_requests_per_minute },
|
46
|
+
period: 1.minute
|
47
|
+
) do |req|
|
48
|
+
if req.path.include?("password/forgot") && req.post?
|
49
|
+
req.params["email_address"].to_s.downcase.strip
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Custom response for throttled requests
|
54
|
+
Rack::Attack.throttled_responder = lambda do |request|
|
55
|
+
retry_after = (request.env["rack.attack.match_data"] || {})[:period]
|
56
|
+
[
|
57
|
+
resp_status,
|
58
|
+
{
|
59
|
+
"Content-Type" => "application/json",
|
60
|
+
"Retry-After" => retry_after.to_s,
|
61
|
+
},
|
62
|
+
[{ error: resp_message }.to_json]
|
63
|
+
]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/securial/version.rb
CHANGED
@@ -0,0 +1,31 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Securial
|
5
|
+
module VersionChecker
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def check_latest_version
|
9
|
+
begin
|
10
|
+
rubygems_api_url = "https://rubygems.org/api/v1/versions/securial/latest.json"
|
11
|
+
uri = URI(rubygems_api_url)
|
12
|
+
http = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 5, read_timeout: 5)
|
13
|
+
response = http.request(Net::HTTP::Get.new(uri))
|
14
|
+
latest = JSON.parse(response.body)["version"]
|
15
|
+
|
16
|
+
current = Securial::VERSION
|
17
|
+
if Gem::Version.new(latest) > Gem::Version.new(current)
|
18
|
+
Securial.logger.info "A newer version (#{latest}) of Securial is available. You are using #{current}."
|
19
|
+
Securial.logger.info "Please consider updating!"
|
20
|
+
Securial.logger.debug "You can update Securial by running the following command in your terminal:"
|
21
|
+
Securial.logger.debug "`bundle update securial`"
|
22
|
+
else
|
23
|
+
Securial.logger.info "You are using the latest version of Securial (#{current})."
|
24
|
+
Securial.logger.debug "No updates available at this time."
|
25
|
+
end
|
26
|
+
rescue => e
|
27
|
+
Securial.logger.debug("Version check failed: #{e.message}")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/securial.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
require "securial/version"
|
2
2
|
require "securial/engine"
|
3
|
+
require "securial/logger"
|
3
4
|
|
4
5
|
require "jbuilder"
|
5
6
|
|
6
7
|
module Securial
|
7
8
|
class << self
|
8
9
|
attr_accessor :configuration
|
10
|
+
attr_accessor :logger
|
9
11
|
|
10
12
|
def configuration
|
11
13
|
@configuration ||= Securial::Config::Configuration.new
|
@@ -20,6 +22,10 @@ module Securial
|
|
20
22
|
yield(configuration)
|
21
23
|
end
|
22
24
|
|
25
|
+
def logger
|
26
|
+
@logger ||= Securial::Logger.build
|
27
|
+
end
|
28
|
+
|
23
29
|
# Returns the pluralized form of the admin role.
|
24
30
|
# This behavior is intentional and aligns with the project's routing conventions.
|
25
31
|
def admin_namespace
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: securial
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aly Badawy
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-06-01 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rails
|
@@ -251,7 +251,7 @@ files:
|
|
251
251
|
- lib/generators/factory_bot/templates/factory.erb
|
252
252
|
- lib/generators/securial/install/install_generator.rb
|
253
253
|
- lib/generators/securial/install/templates/securial_initializer.erb
|
254
|
-
- lib/generators/securial/install/
|
254
|
+
- lib/generators/securial/install/views_generator.rb
|
255
255
|
- lib/generators/securial/jbuilder/jbuilder_generator.rb
|
256
256
|
- lib/generators/securial/jbuilder/templates/_resource.json.erb
|
257
257
|
- lib/generators/securial/jbuilder/templates/index.json.erb
|
@@ -262,11 +262,11 @@ files:
|
|
262
262
|
- lib/generators/securial/scaffold/templates/routes.erb
|
263
263
|
- lib/generators/securial/scaffold/templates/routing_spec.erb
|
264
264
|
- lib/securial.rb
|
265
|
-
- lib/securial/auth
|
265
|
+
- lib/securial/auth.rb
|
266
266
|
- lib/securial/auth/auth_encoder.rb
|
267
267
|
- lib/securial/auth/errors.rb
|
268
268
|
- lib/securial/auth/session_creator.rb
|
269
|
-
- lib/securial/config
|
269
|
+
- lib/securial/config.rb
|
270
270
|
- lib/securial/config/configuration.rb
|
271
271
|
- lib/securial/config/errors.rb
|
272
272
|
- lib/securial/config/validation.rb
|
@@ -275,25 +275,30 @@ files:
|
|
275
275
|
- lib/securial/factories/securial/roles.rb
|
276
276
|
- lib/securial/factories/securial/sessions.rb
|
277
277
|
- lib/securial/factories/securial/users.rb
|
278
|
-
- lib/securial/helpers
|
278
|
+
- lib/securial/helpers.rb
|
279
279
|
- lib/securial/helpers/normalizing_helper.rb
|
280
280
|
- lib/securial/helpers/regex_helper.rb
|
281
|
-
- lib/securial/inspectors
|
281
|
+
- lib/securial/inspectors.rb
|
282
282
|
- lib/securial/inspectors/route_inspector.rb
|
283
283
|
- lib/securial/key_transformer.rb
|
284
284
|
- lib/securial/logger.rb
|
285
|
-
- lib/securial/
|
286
|
-
- lib/securial/
|
287
|
-
- lib/securial/
|
288
|
-
- lib/securial/
|
289
|
-
- lib/securial/
|
285
|
+
- lib/securial/logger/broadcaster.rb
|
286
|
+
- lib/securial/logger/builder.rb
|
287
|
+
- lib/securial/logger/colors.rb
|
288
|
+
- lib/securial/middlewares.rb
|
289
|
+
- lib/securial/middlewares/request_logger_tag.rb
|
290
|
+
- lib/securial/middlewares/transform_request_keys.rb
|
291
|
+
- lib/securial/middlewares/transform_response_keys.rb
|
292
|
+
- lib/securial/security.rb
|
293
|
+
- lib/securial/security/request_rate_limiter.rb
|
290
294
|
- lib/securial/version.rb
|
295
|
+
- lib/securial/version_checker.rb
|
291
296
|
- lib/tasks/securial_tasks.rake
|
292
297
|
homepage: https://github.com/AlyBadawy/Securial/wiki
|
293
298
|
licenses:
|
294
299
|
- MIT
|
295
300
|
metadata:
|
296
|
-
release_date: '2025-
|
301
|
+
release_date: '2025-06-01'
|
297
302
|
allowed_push_host: https://rubygems.org
|
298
303
|
homepage_uri: https://github.com/AlyBadawy/Securial/wiki
|
299
304
|
source_code_uri: https://github.com/AlyBadawy/Securial
|
data/lib/securial/auth/_index.rb
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
require_relative "route_inspector"
|
@@ -1,18 +0,0 @@
|
|
1
|
-
module Securial
|
2
|
-
module Middleware
|
3
|
-
class RequestLoggerTag
|
4
|
-
def initialize(app)
|
5
|
-
@app = app
|
6
|
-
end
|
7
|
-
|
8
|
-
def call(env)
|
9
|
-
request = ActionDispatch::Request.new(env)
|
10
|
-
request_id = request.request_id || SecureRandom.uuid
|
11
|
-
|
12
|
-
Securial::ENGINE_LOGGER.tagged("Securial", "RequestID:#{request_id}") do
|
13
|
-
@app.call(env)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
data/lib/securial/rack_attack.rb
DELETED
@@ -1,48 +0,0 @@
|
|
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
|
File without changes
|