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"))
|
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
|
# Set to true to enable devise user lockable feature
|
||||||
config.devise_lockable = Rails.application.secrets.devise_lockable
|
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
|
devise_lockable: false
|
||||||
multitenancy: false
|
multitenancy: false
|
||||||
security:
|
security:
|
||||||
|
authentication_logs: false
|
||||||
last_sign_in: false
|
last_sign_in: false
|
||||||
password_complexity: false
|
password_complexity: false
|
||||||
# lockable:
|
# lockable:
|
||||||
@@ -59,6 +60,7 @@ staging:
|
|||||||
managers_application_key: ""
|
managers_application_key: ""
|
||||||
multitenancy: false
|
multitenancy: false
|
||||||
security:
|
security:
|
||||||
|
authentication_logs: false
|
||||||
last_sign_in: false
|
last_sign_in: false
|
||||||
password_complexity: false
|
password_complexity: false
|
||||||
# lockable:
|
# lockable:
|
||||||
@@ -102,6 +104,7 @@ preproduction:
|
|||||||
managers_application_key: ""
|
managers_application_key: ""
|
||||||
multitenancy: false
|
multitenancy: false
|
||||||
security:
|
security:
|
||||||
|
authentication_logs: false
|
||||||
last_sign_in: false
|
last_sign_in: false
|
||||||
password_complexity: false
|
password_complexity: false
|
||||||
# lockable:
|
# lockable:
|
||||||
@@ -150,6 +153,7 @@ production:
|
|||||||
managers_application_key: ""
|
managers_application_key: ""
|
||||||
multitenancy: false
|
multitenancy: false
|
||||||
security:
|
security:
|
||||||
|
authentication_logs: false
|
||||||
last_sign_in: false
|
last_sign_in: false
|
||||||
password_complexity: false
|
password_complexity: false
|
||||||
# lockable:
|
# 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
|
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
|
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