Merge pull request #5302 from consuldemocracy/cabildo_tenerife_authetication_logs
ENS: Log successful and failed sign in attempts
This commit is contained in:
@@ -138,6 +138,9 @@ module Consul
|
||||
|
||||
config.paths["app/views"].unshift(Rails.root.join("app", "views", "custom"))
|
||||
|
||||
# Set to true to enable user authentication log
|
||||
config.authentication_logs = Rails.application.secrets.dig(:security, :authentication_logs) || false
|
||||
|
||||
# Set to true to enable devise user lockable feature
|
||||
config.devise_lockable = Rails.application.secrets.devise_lockable
|
||||
|
||||
|
||||
24
config/initializers/warden.rb
Normal file
24
config/initializers/warden.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
Warden::Manager.after_authentication do |user, auth, opts|
|
||||
if Rails.application.config.authentication_logs
|
||||
request = auth.request
|
||||
login = request.params.dig(opts[:scope].to_s, "login")
|
||||
message = "The user #{login} with IP address: #{request.ip} successfully signed in."
|
||||
AuthenticationLogger.log(message)
|
||||
end
|
||||
end
|
||||
|
||||
Warden::Manager.before_failure do |env, opts|
|
||||
if Rails.application.config.authentication_logs
|
||||
request = Rack::Request.new(env)
|
||||
login = request.params.dig(opts[:scope].to_s, "login")
|
||||
message = "The user #{login} with IP address: #{request.ip} failed to sign in."
|
||||
AuthenticationLogger.log(message)
|
||||
|
||||
user = User.find_by(username: login) || User.find_by(email: login)
|
||||
if user&.failed_attempts == User.maximum_attempts.to_i
|
||||
message = "The user #{login} with IP address: #{request.ip} reached maximum attempts " \
|
||||
"and it's temporarily locked."
|
||||
AuthenticationLogger.log(message)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -21,6 +21,7 @@ development:
|
||||
devise_lockable: false
|
||||
multitenancy: false
|
||||
security:
|
||||
authentication_logs: false
|
||||
last_sign_in: false
|
||||
password_complexity: false
|
||||
# lockable:
|
||||
@@ -59,6 +60,7 @@ staging:
|
||||
managers_application_key: ""
|
||||
multitenancy: false
|
||||
security:
|
||||
authentication_logs: false
|
||||
last_sign_in: false
|
||||
password_complexity: false
|
||||
# lockable:
|
||||
@@ -102,6 +104,7 @@ preproduction:
|
||||
managers_application_key: ""
|
||||
multitenancy: false
|
||||
security:
|
||||
authentication_logs: false
|
||||
last_sign_in: false
|
||||
password_complexity: false
|
||||
# lockable:
|
||||
@@ -150,6 +153,7 @@ production:
|
||||
managers_application_key: ""
|
||||
multitenancy: false
|
||||
security:
|
||||
authentication_logs: false
|
||||
last_sign_in: false
|
||||
password_complexity: false
|
||||
# lockable:
|
||||
|
||||
26
lib/authentication_logger.rb
Normal file
26
lib/authentication_logger.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
class AuthenticationLogger
|
||||
@loggers = {}
|
||||
|
||||
class << self
|
||||
def log(message)
|
||||
logger.tagged(Time.current) do
|
||||
logger.info(message)
|
||||
end
|
||||
end
|
||||
|
||||
def path
|
||||
Rails.root.join("log", Tenant.subfolder_path, "authentication.log")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def logger
|
||||
@loggers[Apartment::Tenant.current] ||= build_logger
|
||||
end
|
||||
|
||||
def build_logger
|
||||
FileUtils.mkdir_p(File.dirname(path))
|
||||
ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(path, level: :info))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -22,4 +22,51 @@ describe Users::SessionsController do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "access logs" do
|
||||
context "when feature is enabled" do
|
||||
before { allow(Rails.application.config).to receive(:authentication_logs).and_return(true) }
|
||||
|
||||
it "when a sign in process succeeds it calls the authentication logger" do
|
||||
message = "The user citizen@consul.org with IP address: 0.0.0.0 successfully signed in."
|
||||
expect(AuthenticationLogger).to receive(:log).with(message)
|
||||
|
||||
post :create, params: { user: { login: "citizen@consul.org", password: "12345678" }}
|
||||
end
|
||||
|
||||
it "when a sign in process fails it calls the authentication logger" do
|
||||
message = "The user citizen@consul.org with IP address: 0.0.0.0 failed to sign in."
|
||||
expect(AuthenticationLogger).to receive(:log).with(message)
|
||||
|
||||
post :create, params: { user: { login: "citizen@consul.org", password: "wrong" }}
|
||||
end
|
||||
|
||||
it "when maximum attempts is reached it tracks the user account lock" do
|
||||
allow(User).to receive(:maximum_attempts).and_return(1)
|
||||
message_1 = "The user citizen@consul.org with IP address: 0.0.0.0 failed to sign in."
|
||||
message_2 = "The user citizen@consul.org with IP address: 0.0.0.0 reached maximum attempts " \
|
||||
"and it's temporarily locked."
|
||||
expect(AuthenticationLogger).to receive(:log).once.with(message_1)
|
||||
expect(AuthenticationLogger).to receive(:log).once.with(message_2)
|
||||
|
||||
post :create, params: { user: { login: "citizen@consul.org", password: "wrong" }}
|
||||
end
|
||||
end
|
||||
|
||||
context "when feature is disabled" do
|
||||
before { allow(Rails.application.config).to receive(:authentication_logs).and_return(false) }
|
||||
|
||||
it "when a sign in process succeeds it does not call the authentication logger" do
|
||||
expect(AuthenticationLogger).not_to receive(:log)
|
||||
|
||||
post :create, params: { user: { login: "citizen@consul.org", password: "12345678" }}
|
||||
end
|
||||
|
||||
it "when a sign in process fails it does not call the authentication logger" do
|
||||
expect(AuthenticationLogger).not_to receive(:log)
|
||||
|
||||
post :create, params: { user: { login: "citizen@consul.org", password: "wrong" }}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
49
spec/lib/authentication_logger_spec.rb
Normal file
49
spec/lib/authentication_logger_spec.rb
Normal file
@@ -0,0 +1,49 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe AuthenticationLogger do
|
||||
describe ".path" do
|
||||
context "when multitenancy is disabled" do
|
||||
before { allow(Rails.application.config).to receive(:multitenancy).and_return(false) }
|
||||
|
||||
it "uses default file" do
|
||||
expect(AuthenticationLogger.path).to eq(Rails.root.join("log", "authentication.log"))
|
||||
end
|
||||
end
|
||||
|
||||
context "when multitenancy is enabled" do
|
||||
before { allow(Rails.application.config).to receive(:multitenancy).and_return(true) }
|
||||
|
||||
it "uses the default file for the public schema" do
|
||||
Tenant.switch("public") do
|
||||
path = Rails.root.join("log", "authentication.log")
|
||||
|
||||
expect(AuthenticationLogger.path).to eq(path)
|
||||
end
|
||||
end
|
||||
|
||||
it "uses a separate file for any other tenant" do
|
||||
create(:tenant, schema: "tenant1")
|
||||
Tenant.switch("tenant1") do
|
||||
path = Rails.root.join("log", "tenants", "tenant1", "authentication.log")
|
||||
|
||||
expect(AuthenticationLogger.path).to eq(path)
|
||||
end
|
||||
|
||||
create(:tenant, schema: "tenant2")
|
||||
Tenant.switch("tenant2") do
|
||||
path = Rails.root.join("log", "tenants", "tenant2", "authentication.log")
|
||||
|
||||
expect(AuthenticationLogger.path).to eq(path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "log" do
|
||||
it "includes current time in each log entry", :with_frozen_time do
|
||||
expect_any_instance_of(ActiveSupport::TaggedLogging).to receive(:tagged).with(Time.current)
|
||||
|
||||
AuthenticationLogger.log("Just logging something!")
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user