diff --git a/lib/datadog/appsec/contrib/devise/patcher/authenticatable_patch.rb b/lib/datadog/appsec/contrib/devise/patcher/authenticatable_patch.rb index 51d0b06619d..7e3187cf0ef 100644 --- a/lib/datadog/appsec/contrib/devise/patcher/authenticatable_patch.rb +++ b/lib/datadog/appsec/contrib/devise/patcher/authenticatable_patch.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative '../configuration' require_relative '../tracking' require_relative '../resource' require_relative '../event' @@ -14,33 +15,27 @@ module AuthenticatablePatch # rubocop:disable Metrics/MethodLength def validate(resource, &block) result = super - return result unless AppSec.enabled? - return result if @_datadog_skip_track_login_event - - track_user_events_configuration = Datadog.configuration.appsec.track_user_events - - return result unless track_user_events_configuration.enabled - - automated_track_user_events_mode = track_user_events_configuration.mode - appsec_context = Datadog::AppSec.active_context - - return result unless appsec_context + return result unless AppSec.enabled? + return result if @_datadog_appsec_skip_track_login_event + return result unless Configuration.auto_user_instrumentation_enabled? + return result unless AppSec.active_context devise_resource = resource ? Resource.new(resource) : nil - - event_information = Event.new(devise_resource, automated_track_user_events_mode) + event_information = Event.new(devise_resource, Configuration.auto_user_instrumentation_mode) if result if event_information.user_id - Datadog.logger.debug { 'User Login Event success' } + Datadog.logger.debug { 'AppSec: User successful login event' } else - Datadog.logger.debug { 'User Login Event success, but can\'t extract user ID. Tracking empty event' } + Datadog.logger.debug do + "AppSec: User successful login event, but can't extract user ID. Tracking empty event" + end end Tracking.track_login_success( - appsec_context.trace, - appsec_context.span, + AppSec.active_context.trace, + AppSec.active_context.span, user_id: event_information.user_id, **event_information.to_h ) @@ -52,15 +47,15 @@ def validate(resource, &block) if resource user_exists = true - Datadog.logger.debug { 'User Login Event failure users exists' } + Datadog.logger.debug { 'AppSec: User failed login event, but user exists' } else user_exists = false - Datadog.logger.debug { 'User Login Event failure user do not exists' } + Datadog.logger.debug { 'AppSec: User failed login event and user does not exist' } end Tracking.track_login_failure( - appsec_context.trace, - appsec_context.span, + AppSec.active_context.trace, + AppSec.active_context.span, user_id: event_information.user_id, user_exists: user_exists, **event_information.to_h diff --git a/lib/datadog/appsec/contrib/devise/patcher/rememberable_patch.rb b/lib/datadog/appsec/contrib/devise/patcher/rememberable_patch.rb index 28dd78d2712..87760c54372 100644 --- a/lib/datadog/appsec/contrib/devise/patcher/rememberable_patch.rb +++ b/lib/datadog/appsec/contrib/devise/patcher/rememberable_patch.rb @@ -9,7 +9,7 @@ module Patcher # Rememberable strategy as Login Success events. module RememberablePatch def validate(*args) - @_datadog_skip_track_login_event = true + @_datadog_appsec_skip_track_login_event = true super end diff --git a/sig/datadog/appsec/contrib/devise/configuration.rbs b/sig/datadog/appsec/contrib/devise/configuration.rbs new file mode 100644 index 00000000000..c7f9be9643c --- /dev/null +++ b/sig/datadog/appsec/contrib/devise/configuration.rbs @@ -0,0 +1,13 @@ +module Datadog + module AppSec + module Contrib + module Devise + module Configuration + def self?.auto_user_instrumentation_enabled?: () -> bool + + def self?.auto_user_instrumentation_mode: () -> ::String + end + end + end + end +end diff --git a/spec/datadog/appsec/contrib/devise/patcher/authenticatable_patch_spec.rb b/spec/datadog/appsec/contrib/devise/patcher/authenticatable_patch_spec.rb index e2741ddb90e..572f85b92bc 100644 --- a/spec/datadog/appsec/contrib/devise/patcher/authenticatable_patch_spec.rb +++ b/spec/datadog/appsec/contrib/devise/patcher/authenticatable_patch_spec.rb @@ -1,244 +1,315 @@ +# frozen_string_literal: true + require 'datadog/appsec/spec_helper' +require 'datadog/appsec/contrib/support/devise_user_mock' -require 'securerandom' require 'datadog/appsec/contrib/devise/patcher' require 'datadog/appsec/contrib/devise/patcher/authenticatable_patch' RSpec.describe Datadog::AppSec::Contrib::Devise::Patcher::AuthenticatablePatch do - let(:mock_klass) do - Class.new do - prepend Datadog::AppSec::Contrib::Devise::Patcher::AuthenticatablePatch + before do + allow(Datadog).to receive(:logger).and_return(instance_double(Datadog::Core::Logger).as_null_object) + allow(Datadog).to receive(:configuration).and_return(settings) + end - def initialize(result) - @result = result + let(:settings) { Datadog::Core::Configuration::Settings.new } + # NOTE: This spec needs to be changed to use actual devise controller instead + let(:mock_controller) do + Class.new do + def initialize(success:) + @success = success end def validate(resource, &block) - @result + @success end + + prepend Datadog::AppSec::Contrib::Devise::Patcher::AuthenticatablePatch end end - let(:mock_resource) do - Class.new do - attr_reader :id, :email, :username + context 'when AppSec is disabled' do + before do + allow(Datadog::AppSec).to receive(:enabled?).and_return(false) - def initialize(id, email, username) - @id = id - @email = email - @username = username - end + settings.appsec.track_user_events.enabled = false + settings.appsec.track_user_events.mode = 'safe' end - end - let(:nil_resource) { nil } - let(:resource) { mock_resource.new(SecureRandom.uuid, 'hello@gmail.com', 'John') } - let(:mode) { 'safe' } - let(:automated_track_user_events) { double(enabled: track_user_events_enabled, mode: mode) } - let(:success_login) { mock_klass.new(true) } - let(:failed_login) { mock_klass.new(false) } + let(:controller) { mock_controller.new(success: true) } + let(:resource) do + Datadog::AppSec::Contrib::Support::DeviseUserMock.new( + id: '00000000-0000-0000-0000-000000000000', email: 'hello@gmail.com', username: 'John' + ) + end - before do - allow(Datadog::AppSec).to receive(:enabled?).and_return(appsec_enabled) - if appsec_enabled - allow(Datadog.configuration.appsec).to receive(:track_user_events).and_return(automated_track_user_events) + it 'does not track successful signin event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to_not receive(:track_login_success) - allow(Datadog::AppSec).to receive(:active_context).and_return(appsec_context) if track_user_events_enabled + expect(controller.validate(resource)).to eq(true) end end - context 'AppSec disabled' do - let(:appsec_enabled) { false } - let(:track_user_events_enabled) { false } + context 'when automated user tracking is disabled' do + before do + allow(Datadog::AppSec).to receive(:enabled?).and_return(true) - it 'do not tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to_not receive(:track_login_success) - expect(success_login.validate(resource)).to eq(true) + settings.appsec.track_user_events.enabled = true + settings.appsec.track_user_events.mode = 'safe' end - end - context 'Automated user tracking is disabled' do - let(:appsec_enabled) { true } - let(:track_user_events_enabled) { false } + let(:controller) { mock_controller.new(success: true) } + let(:resource) do + Datadog::AppSec::Contrib::Support::DeviseUserMock.new( + id: '00000000-0000-0000-0000-000000000000', email: 'hello@gmail.com', username: 'John' + ) + end - it 'do not tracks event' do + it 'does not track successful signin event' do expect(Datadog::AppSec::Contrib::Devise::Tracking).to_not receive(:track_login_success) - expect(success_login.validate(resource)).to eq(true) + + expect(controller.validate(resource)).to eq(true) end end - context 'AppSec context is nil' do - let(:appsec_enabled) { true } - let(:track_user_events_enabled) { true } - let(:mode) { 'safe' } - let(:appsec_context) { nil } + context 'when AppSec active context is not set' do + before do + allow(Datadog::AppSec).to receive(:enabled?).and_return(true) + allow(Datadog::AppSec).to receive(:active_context).and_return(nil) - it 'do not tracks event' do + settings.appsec.track_user_events.enabled = true + settings.appsec.track_user_events.mode = 'safe' + end + + let(:controller) { mock_controller.new(success: true) } + let(:resource) do + Datadog::AppSec::Contrib::Support::DeviseUserMock.new( + id: '00000000-0000-0000-0000-000000000000', email: 'hello@gmail.com', username: 'John' + ) + end + + it 'does not track successful signin event' do expect(Datadog::AppSec::Contrib::Devise::Tracking).to_not receive(:track_login_success) - expect(success_login.validate(resource)).to eq(true) + + expect(controller.validate(resource)).to eq(true) end end - context 'when logging in from Rememberable devise strategy' do - let(:appsec_enabled) { true } - let(:track_user_events_enabled) { true } - let(:appsec_context) { instance_double(Datadog::AppSec::Context, trace: double, span: double) } + context 'when successfully signin via Rememberable strategy' do + before do + allow(Datadog::AppSec).to receive(:enabled?).and_return(true) + allow(Datadog::AppSec).to receive(:active_context).and_return(active_context) + + settings.appsec.track_user_events.enabled = true + settings.appsec.track_user_events.mode = 'safe' + end - let(:mock_klass) do + let(:active_context) { instance_double(Datadog::AppSec::Context, trace: double, span: double) } + let(:controller) { mock_controller.new(success: true) } + let(:mock_controller) do Class.new do + def initialize(success:) + @result = success + end + def validate(resource, &block) @result end prepend Datadog::AppSec::Contrib::Devise::Patcher::AuthenticatablePatch prepend Datadog::AppSec::Contrib::Devise::Patcher::RememberablePatch - - def initialize(result) - @result = result - end end end + let(:resource) do + Datadog::AppSec::Contrib::Support::DeviseUserMock.new( + id: '00000000-0000-0000-0000-000000000000', email: 'hello@gmail.com', username: 'John' + ) + end - it 'does not track login event' do + it 'does not track successful signin event' do expect(Datadog::AppSec::Contrib::Devise::Tracking).to_not receive(:track_login_success) - expect(success_login.validate(resource)).to eq(true) + expect(controller.validate(resource)).to eq(true) end end - context 'successful login' do - let(:appsec_enabled) { true } - let(:track_user_events_enabled) { true } - let(:appsec_context) { instance_double(Datadog::AppSec::Context, trace: double, span: double) } - - context 'with resource ID' do - context 'safe mode' do - let(:mode) { 'safe' } - - it 'tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_login_success).with( - appsec_context.trace, - appsec_context.span, - user_id: resource.id, - **{} - ) - expect(success_login.validate(resource)).to eq(true) + context 'when authentication is successful' do + before do + allow(Datadog::AppSec).to receive(:enabled?).and_return(true) + allow(Datadog::AppSec).to receive(:active_context).and_return(active_context) + + settings.appsec.track_user_events.enabled = true + settings.appsec.track_user_events.mode = 'safe' + end + + let(:active_context) { instance_double(Datadog::AppSec::Context, trace: double, span: double) } + let(:controller) { mock_controller.new(success: true) } + let(:resource) do + Datadog::AppSec::Contrib::Support::DeviseUserMock.new( + id: '00000000-0000-0000-0000-000000000000', email: 'hello@gmail.com', username: 'John' + ) + end + + context 'when user resource was found and has an ID' do + context 'when tracking mode set to safe' do + before { settings.appsec.track_user_events.mode = 'safe' } + + it 'tracks successful signin event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_login_success) + .with( + active_context.trace, + active_context.span, + user_id: '00000000-0000-0000-0000-000000000000', + **{} + ) + + expect(controller.validate(resource)).to eq(true) end end - context 'extended mode' do - let(:mode) { 'extended' } - - it 'tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_login_success).with( - appsec_context.trace, - appsec_context.span, - user_id: resource.id, - **{ username: 'John', email: 'hello@gmail.com' } - ) - expect(success_login.validate(resource)).to eq(true) + context 'when tracking mode set to extended' do + before { settings.appsec.track_user_events.mode = 'extended' } + + it 'tracks successful signin event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_login_success) + .with( + active_context.trace, + active_context.span, + user_id: '00000000-0000-0000-0000-000000000000', + **{ username: 'John', email: 'hello@gmail.com' } + ) + + expect(controller.validate(resource)).to eq(true) end end end - context 'without resource ID' do - let(:resource) { mock_resource.new(nil, 'hello@gmail.com', 'John') } + context 'when user resource was found, but has no ID' do + let(:resource) do + Datadog::AppSec::Contrib::Support::DeviseUserMock.new( + id: nil, email: 'hello@gmail.com', username: 'John' + ) + end + + context 'when tracking mode set to safe' do + before { settings.appsec.track_user_events.mode = 'safe' } - context 'safe mode' do - let(:mode) { 'safe' } + it 'tracks successful signin event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_login_success) + .with( + active_context.trace, + active_context.span, + user_id: nil, + **{} + ) - it 'tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_login_success).with( - appsec_context.trace, - appsec_context.span, - user_id: nil, - **{} - ) - expect(success_login.validate(resource)).to eq(true) + expect(controller.validate(resource)).to eq(true) end end - context 'extended mode' do - let(:mode) { 'extended' } - - it 'tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_login_success).with( - appsec_context.trace, - appsec_context.span, - user_id: nil, - **{ username: 'John', email: 'hello@gmail.com' } - ) - expect(success_login.validate(resource)).to eq(true) + context 'when tracking mode set to extended' do + before { settings.appsec.track_user_events.mode = 'extended' } + + it 'tracks successful signin event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_login_success) + .with( + active_context.trace, + active_context.span, + user_id: nil, + **{ username: 'John', email: 'hello@gmail.com' } + ) + + expect(controller.validate(resource)).to eq(true) end end end end - context 'unsuccessful login' do - let(:appsec_enabled) { true } - let(:track_user_events_enabled) { true } - let(:appsec_context) { instance_double(Datadog::AppSec::Context, trace: double, span: double) } - - context 'with resource' do - context 'safe mode' do - let(:mode) { 'safe' } - - it 'tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_login_failure).with( - appsec_context.trace, - appsec_context.span, - user_id: resource.id, - user_exists: true, - **{} - ) - expect(failed_login.validate(resource)).to eq(false) + context 'when authentication is unsuccessful' do + before do + allow(Datadog::AppSec).to receive(:enabled?).and_return(true) + allow(Datadog::AppSec).to receive(:active_context).and_return(active_context) + + settings.appsec.track_user_events.enabled = true + settings.appsec.track_user_events.mode = 'safe' + end + + let(:active_context) { instance_double(Datadog::AppSec::Context, trace: double, span: double) } + let(:controller) { mock_controller.new(success: false) } + let(:resource) do + Datadog::AppSec::Contrib::Support::DeviseUserMock.new( + id: '00000000-0000-0000-0000-000000000000', email: 'hello@gmail.com', username: 'John' + ) + end + + context 'when user resource was found' do + context 'when tracking mode set to safe' do + before { settings.appsec.track_user_events.mode = 'safe' } + + it 'tracks unsuccessful signin event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_login_failure) + .with( + active_context.trace, + active_context.span, + user_id: '00000000-0000-0000-0000-000000000000', + user_exists: true, + **{} + ) + + expect(controller.validate(resource)).to eq(false) end end - context 'extended mode' do - let(:mode) { 'extended' } - - it 'tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_login_failure).with( - appsec_context.trace, - appsec_context.span, - user_id: resource.id, - user_exists: true, - **{ username: 'John', email: 'hello@gmail.com' } - ) - expect(failed_login.validate(resource)).to eq(false) + context 'when tracking mode set to extended' do + before { settings.appsec.track_user_events.mode = 'extended' } + + it 'tracks unsuccessful signin event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_login_failure) + .with( + active_context.trace, + active_context.span, + user_id: '00000000-0000-0000-0000-000000000000', + user_exists: true, + **{ username: 'John', email: 'hello@gmail.com' } + ) + + expect(controller.validate(resource)).to eq(false) end end end - context 'without resource' do - context 'safe mode' do - let(:mode) { 'safe' } - - it 'tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_login_failure).with( - appsec_context.trace, - appsec_context.span, - user_id: nil, - user_exists: false, - **{} - ) - expect(failed_login.validate(nil_resource)).to eq(false) + context 'when user resource was not found' do + context 'when tracking mode set to safe' do + before { settings.appsec.track_user_events.mode = 'safe' } + + it 'tracks unsuccessful signin event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_login_failure) + .with( + active_context.trace, + active_context.span, + user_id: nil, + user_exists: false, + **{} + ) + + expect(controller.validate(nil)).to eq(false) end end - context 'extended mode' do - let(:mode) { 'extended' } - - it 'tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_login_failure).with( - appsec_context.trace, - appsec_context.span, - user_id: nil, - user_exists: false, - **{} - ) - expect(failed_login.validate(nil_resource)).to eq(false) + context 'when tracking mode set to extended' do + before { settings.appsec.track_user_events.mode = 'extended' } + + it 'tracks unsuccessful signin event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_login_failure) + .with( + active_context.trace, + active_context.span, + user_id: nil, + user_exists: false, + **{} + ) + + expect(controller.validate(nil)).to eq(false) end end end diff --git a/spec/datadog/appsec/contrib/devise/patcher/registration_controller_patch_spec.rb b/spec/datadog/appsec/contrib/devise/patcher/registration_controller_patch_spec.rb index f109e560250..ac117bb0409 100644 --- a/spec/datadog/appsec/contrib/devise/patcher/registration_controller_patch_spec.rb +++ b/spec/datadog/appsec/contrib/devise/patcher/registration_controller_patch_spec.rb @@ -67,7 +67,7 @@ def create end end - context 'when Automated user tracking is disabled' do + context 'when automated user tracking is disabled' do before do allow(Datadog::AppSec).to receive(:enabled?).and_return(true) @@ -140,7 +140,7 @@ def create end end - context 'when authentication defines current user as persisted resource' do + context 'when registration defines current user as persisted resource' do before do allow(Datadog::AppSec).to receive(:enabled?).and_return(true) allow(Datadog::AppSec).to receive(:active_context).and_return(active_context) @@ -313,7 +313,7 @@ def create end end - context 'when authentication defines current user as non-persisted resource' do + context 'when registration defines current user as non-persisted resource' do before do allow(Datadog::AppSec).to receive(:enabled?).and_return(true) allow(Datadog::AppSec).to receive(:active_context).and_return(active_context)