diff --git a/lib/datadog/appsec/configuration/settings.rb b/lib/datadog/appsec/configuration/settings.rb index 74a3aabb62e..1413fde15bb 100644 --- a/lib/datadog/appsec/configuration/settings.rb +++ b/lib/datadog/appsec/configuration/settings.rb @@ -12,14 +12,29 @@ module Settings DEFAULT_OBFUSCATOR_KEY_REGEX = '(?i)pass|pw(?:or)?d|secret|(?:api|private|public|access)[_-]?key|token|consumer[_-]?(?:id|key|secret)|sign(?:ed|ature)|bearer|authorization|jsessionid|phpsessid|asp\.net[_-]sessionid|sid|jwt' DEFAULT_OBFUSCATOR_VALUE_REGEX = '(?i)(?:p(?:ass)?w(?:or)?d|pass(?:[_-]?phrase)?|secret(?:[_-]?key)?|(?:(?:api|private|public|access)[_-]?)key(?:[_-]?id)?|(?:(?:auth|access|id|refresh)[_-]?)?token|consumer[_-]?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?|jsessionid|phpsessid|asp\.net(?:[_-]|-)sessionid|sid|jwt)(?:\s*=[^;]|"\s*:\s*"[^"]+")|bearer\s+[a-z0-9\._\-]+|token:[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\w=-]+\.ey[I-L][\w=-]+(?:\.[\w.+\/=-]+)?|[\-]{5}BEGIN[a-z\s]+PRIVATE\sKEY[\-]{5}[^\-]+[\-]{5}END[a-z\s]+PRIVATE\sKEY|ssh-rsa\s*[a-z0-9\/\.+]{100,}' # rubocop:enable Layout/LineLength + + DISABLED_AUTO_USER_INSTRUMENTATION_MODE = 'disabled' + ANONYMIZATION_AUTO_USER_INSTRUMENTATION_MODE = 'anonymization' + IDENTIFICATION_AUTO_USER_INSTRUMENTATION_MODE = 'identification' + AUTO_USER_INSTRUMENTATION_MODES = [ + DISABLED_AUTO_USER_INSTRUMENTATION_MODE, + ANONYMIZATION_AUTO_USER_INSTRUMENTATION_MODE, + IDENTIFICATION_AUTO_USER_INSTRUMENTATION_MODE + ].freeze + AUTO_USER_INSTRUMENTATION_MODES_ALIASES = { + 'ident' => IDENTIFICATION_AUTO_USER_INSTRUMENTATION_MODE, + 'anon' => ANONYMIZATION_AUTO_USER_INSTRUMENTATION_MODE, + }.freeze + + # NOTE: These two constants are deprecated + SAFE_TRACK_USER_EVENTS_MODE = 'safe' + EXTENDED_TRACK_USER_EVENTS_MODE = 'extended' APPSEC_VALID_TRACK_USER_EVENTS_MODE = [ - 'safe', - 'extended' + SAFE_TRACK_USER_EVENTS_MODE, EXTENDED_TRACK_USER_EVENTS_MODE ].freeze - APPSEC_VALID_TRACK_USER_EVENTS_ENABLED_VALUES = [ - '1', - 'true' - ].concat(APPSEC_VALID_TRACK_USER_EVENTS_MODE).freeze + APPSEC_VALID_TRACK_USER_EVENTS_ENABLED_VALUES = ['1', 'true'].concat( + APPSEC_VALID_TRACK_USER_EVENTS_MODE + ).freeze def self.extended(base) base = base.singleton_class unless base.is_a?(Class) @@ -149,6 +164,29 @@ def self.add_settings!(base) end end + settings :auto_user_instrumentation do + define_method(:enabled?) { get_option(:mode) != DISABLED_AUTO_USER_INSTRUMENTATION_MODE } + + option :mode do |o| + o.type :string + o.env 'DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE' + o.default IDENTIFICATION_AUTO_USER_INSTRUMENTATION_MODE + o.setter do |value| + mode = AUTO_USER_INSTRUMENTATION_MODES_ALIASES.fetch(value, value) + next mode if AUTO_USER_INSTRUMENTATION_MODES.include?(mode) + + Datadog.logger.warn( + 'The appsec.auto_user_instrumentation.mode value provided is not supported. ' \ + "Supported values are: #{AUTO_USER_INSTRUMENTATION_MODES.join(' | ')}. " \ + "Using default value: #{IDENTIFICATION_AUTO_USER_INSTRUMENTATION_MODE}." + ) + + IDENTIFICATION_AUTO_USER_INSTRUMENTATION_MODE + end + end + end + + # DEV-3.0: Remove `track_user_events.enabled` and `track_user_events.mode` options settings :track_user_events do option :enabled do |o| o.default true @@ -161,24 +199,39 @@ def self.add_settings!(base) APPSEC_VALID_TRACK_USER_EVENTS_ENABLED_VALUES.include?(env_value.strip.downcase) end end + o.after_set do + Core.log_deprecation(key: :appsec_track_user_events_enabled) do + 'The appsec.track_user_events.enabled setting has been deprecated for removal. ' \ + 'Please remove it from your Datadog.configure block and use ' \ + 'appsec.auto_user_instrumentation.mode instead.' + end + end end option :mode do |o| o.type :string o.env 'DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING' - o.default 'safe' + o.default SAFE_TRACK_USER_EVENTS_MODE o.setter do |v| if APPSEC_VALID_TRACK_USER_EVENTS_MODE.include?(v) v elsif v == 'disabled' - 'safe' + SAFE_TRACK_USER_EVENTS_MODE else Datadog.logger.warn( 'The appsec.track_user_events.mode value provided is not supported.' \ - 'Supported values are: safe | extended.' \ - 'Using default value `safe`' + "Supported values are: #{APPSEC_VALID_TRACK_USER_EVENTS_MODE.join(' | ')}." \ + "Using default value: #{SAFE_TRACK_USER_EVENTS_MODE}." ) - 'safe' + + SAFE_TRACK_USER_EVENTS_MODE + end + end + o.after_set do + Core.log_deprecation(key: :appsec_track_user_events_mode) do + 'The appsec.track_user_events.mode setting has been deprecated for removal. ' \ + 'Please remove it from your Datadog.configure block and use ' \ + 'appsec.auto_user_instrumentation.mode instead.' end end end diff --git a/lib/datadog/appsec/contrib/devise/configuration.rb b/lib/datadog/appsec/contrib/devise/configuration.rb new file mode 100644 index 00000000000..570aa86bb17 --- /dev/null +++ b/lib/datadog/appsec/contrib/devise/configuration.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Datadog + module AppSec + module Contrib + module Devise + # A temporary configuration module to accomodate new RFC changes. + # NOTE: DEV-3 Remove module + module Configuration + module_function + + # NOTE: DEV-3 Replace method use with `auto_user_instrumentation.enabled?` + def auto_user_instrumentation_enabled? + Datadog.configuration.appsec.auto_user_instrumentation.enabled? && + Datadog.configuration.appsec.track_user_events.enabled + end + + # NOTE: DEV-3 Replace method use with `auto_user_instrumentation.mode` + def auto_user_instrumentation_mode + case Datadog.configuration.appsec.track_user_events.mode + when AppSec::Configuration::Settings::SAFE_TRACK_USER_EVENTS_MODE + AppSec::Configuration::Settings::ANONYMIZATION_AUTO_USER_INSTRUMENTATION_MODE + when AppSec::Configuration::Settings::EXTENDED_TRACK_USER_EVENTS_MODE + AppSec::Configuration::Settings::IDENTIFICATION_AUTO_USER_INSTRUMENTATION_MODE + else + Datadog.configuration.appsec.auto_user_instrumentation.mode + end + end + end + end + end + end +end diff --git a/lib/datadog/appsec/contrib/devise/event.rb b/lib/datadog/appsec/contrib/devise/event.rb index 66223ff703c..26efb313850 100644 --- a/lib/datadog/appsec/contrib/devise/event.rb +++ b/lib/datadog/appsec/contrib/devise/event.rb @@ -8,9 +8,6 @@ module Devise class Event UUID_REGEX = /^\h{8}-\h{4}-\h{4}-\h{4}-\h{12}$/.freeze - SAFE_MODE = 'safe' - EXTENDED_MODE = 'extended' - attr_reader :user_id def initialize(resource, mode) @@ -38,15 +35,15 @@ def extract @user_id = @resource.id case @mode - when EXTENDED_MODE + when AppSec::Configuration::Settings::IDENTIFICATION_AUTO_USER_INSTRUMENTATION_MODE @email = @resource.email @username = @resource.username - when SAFE_MODE + when AppSec::Configuration::Settings::ANONYMIZATION_AUTO_USER_INSTRUMENTATION_MODE @user_id = nil unless @user_id && @user_id.to_s =~ UUID_REGEX else Datadog.logger.warn( - "Invalid automated user evenst mode: `#{@mode}`. "\ - 'Supported modes are: `safe` and `extended`.' + "Invalid auto_user_instrumentation.mode: `#{@mode}`. " \ + "Supported modes are: #{AppSec::Configuration::Settings::AUTO_USER_INSTRUMENTATION_MODES.join(' | ')}." ) end end 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/registration_controller_patch.rb b/lib/datadog/appsec/contrib/devise/patcher/registration_controller_patch.rb index 8db73f60f32..d56bff8aefe 100644 --- a/lib/datadog/appsec/contrib/devise/patcher/registration_controller_patch.rb +++ b/lib/datadog/appsec/contrib/devise/patcher/registration_controller_patch.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative '../configuration' require_relative '../tracking' require_relative '../resource' require_relative '../event' @@ -13,31 +14,23 @@ module Patcher module RegistrationControllerPatch def create return super unless AppSec.enabled? - - track_user_events_configuration = Datadog.configuration.appsec.track_user_events - - return super unless track_user_events_configuration.enabled - - automated_track_user_events_mode = track_user_events_configuration.mode - - appsec_context = Datadog::AppSec.active_context - return super unless appsec_context + return super unless Configuration.auto_user_instrumentation_enabled? + return super unless AppSec.active_context super do |resource| if resource.persisted? devise_resource = Resource.new(resource) - - event_information = Event.new(devise_resource, automated_track_user_events_mode) + event_information = Event.new(devise_resource, Configuration.auto_user_instrumentation_mode) if event_information.user_id - Datadog.logger.debug { 'User Signup Event' } + Datadog.logger.debug { 'AppSec: User signup event' } else - Datadog.logger.warn { 'User Signup Event, but can\'t extract user ID. Tracking empty event' } + Datadog.logger.warn { "AppSec: User signup event, but can't extract user ID. Tracking empty event" } end Tracking.track_signup( - appsec_context.trace, - appsec_context.span, + AppSec.active_context.trace, + AppSec.active_context.span, user_id: event_information.user_id, **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/configuration/settings.rbs b/sig/datadog/appsec/configuration/settings.rbs index 41f594921ff..9333d00061f 100644 --- a/sig/datadog/appsec/configuration/settings.rbs +++ b/sig/datadog/appsec/configuration/settings.rbs @@ -1,7 +1,6 @@ module Datadog module AppSec module Configuration - # Settings module Settings extend Datadog::Core::Configuration::Base::ClassMethods include Datadog::Core::Configuration::Base::InstanceMethods @@ -9,9 +8,26 @@ module Datadog include Datadog::Core::Configuration::Options::InstanceMethods DEFAULT_OBFUSCATOR_KEY_REGEX: ::String + DEFAULT_OBFUSCATOR_VALUE_REGEX: ::String - APPSEC_VALID_TRACK_USER_EVENTS_MODE: ::Array[String] - APPSEC_VALID_TRACK_USER_EVENTS_ENABLED_VALUES: ::Array[String] + + DISABLED_AUTO_USER_INSTRUMENTATION_MODE: ::String + + ANONYMIZATION_AUTO_USER_INSTRUMENTATION_MODE: ::String + + IDENTIFICATION_AUTO_USER_INSTRUMENTATION_MODE: ::String + + AUTO_USER_INSTRUMENTATION_MODES: ::Array[::String] + + AUTO_USER_INSTRUMENTATION_MODES_ALIASES: ::Hash[::String, ::String] + + SAFE_TRACK_USER_EVENTS_MODE: ::String + + EXTENDED_TRACK_USER_EVENTS_MODE: ::String + + APPSEC_VALID_TRACK_USER_EVENTS_MODE: ::Array[::String] + + APPSEC_VALID_TRACK_USER_EVENTS_ENABLED_VALUES: ::Array[::String] def self.extended: (untyped base) -> untyped 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/configuration/settings_spec.rb b/spec/datadog/appsec/configuration/settings_spec.rb index 541546ae364..63445f9e7b4 100644 --- a/spec/datadog/appsec/configuration/settings_spec.rb +++ b/spec/datadog/appsec/configuration/settings_spec.rb @@ -469,6 +469,8 @@ def patcher end describe 'track_user_events' do + before { allow(Datadog).to receive(:logger).and_return(spy(Datadog::Core::Logger)) } + describe '#enabled' do subject(:enabled) { settings.appsec.track_user_events.enabled } @@ -479,6 +481,17 @@ def patcher end end + context 'when deprication message should be emitted' do + let(:track_user_events_enabled) { 'true' } + + it 'writes the deprication message' do + expect(Datadog::Core).to receive(:log_deprecation) do |_, &block| + expect(block.call).to match(/setting has been deprecated for removal/) + end + expect(enabled).to eq(true) + end + end + context 'is not defined' do let(:track_user_events_enabled) { nil } @@ -562,6 +575,18 @@ def patcher settings.appsec.track_user_events.mode = track_user_events_mode end + context 'when deprication message should be emitted' do + let(:track_user_events_mode) { 'extended' } + + it 'writes the deprication message' do + expect(Datadog::Core).to receive(:log_deprecation) do |_, &block| + expect(block.call).to match(/setting has been deprecated for removal/) + end + + set_appsec_track_user_events_mode + end + end + context 'when given a supported value' do let(:track_user_events_mode) { 'extended' } @@ -582,6 +607,92 @@ def patcher end end + describe 'auto_user_instrumentation.mode' do + before { allow(Datadog).to receive(:logger).and_return(logger) } + + let(:logger) { instance_double(Datadog::Core::Logger) } + + context 'when valid value is set' do + before { settings.appsec.auto_user_instrumentation.mode = 'disabled' } + + it { expect(settings.appsec.auto_user_instrumentation.mode).to eq('disabled') } + end + + context 'when valid short value is set' do + before { settings.appsec.auto_user_instrumentation.mode = 'anon' } + + it 'expands the alias value to the long version' do + expect(settings.appsec.auto_user_instrumentation.mode).to eq('anonymization') + end + end + + context 'when invalid value is set' do + it 'sets the value to the default and writes a warning message' do + expect(logger).to receive(:warn).with(/value provided is not supported/) + settings.appsec.auto_user_instrumentation.mode = 'unknown' + + expect(settings.appsec.auto_user_instrumentation.mode).to eq('identification') + end + end + + context 'when valid DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE is set' do + around do |example| + ClimateControl.modify('DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE' => 'disabled') do + example.run + end + end + + it { expect(settings.appsec.auto_user_instrumentation.mode).to eq('disabled') } + end + + context 'when valid DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE short value is set' do + around do |example| + ClimateControl.modify('DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE' => 'anon') do + example.run + end + end + + it 'expands the alias value to the long version' do + expect(settings.appsec.auto_user_instrumentation.mode).to eq('anonymization') + end + end + + context 'when invalid DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE is set' do + around do |example| + ClimateControl.modify('DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE' => 'unknown') do + example.run + end + end + + it 'sets the value to the default and writes a warning message' do + expect(logger).to receive(:warn).with(/value provided is not supported/) + expect(settings.appsec.auto_user_instrumentation.mode).to eq('identification') + end + end + + context 'when no value or env variable is set' do + it { expect(settings.appsec.auto_user_instrumentation.mode).to eq('identification') } + end + end + + describe 'auto_user_instrumentation.enabled?' do + context 'when explicitly disabled' do + before { settings.appsec.auto_user_instrumentation.mode = 'disabled' } + + it { expect(settings.appsec.auto_user_instrumentation).not_to be_enabled } + end + + context 'when explicitly enabled' do + before { settings.appsec.auto_user_instrumentation.mode = 'identification' } + + it { expect(settings.appsec.auto_user_instrumentation).to be_enabled } + end + + context 'when default value is used' do + it { expect(settings.appsec.auto_user_instrumentation).to be_enabled } + end + end + describe 'block' do describe 'templates' do [ diff --git a/spec/datadog/appsec/contrib/devise/configuration_spec.rb b/spec/datadog/appsec/contrib/devise/configuration_spec.rb new file mode 100644 index 00000000000..7a009bfca0c --- /dev/null +++ b/spec/datadog/appsec/contrib/devise/configuration_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'datadog/appsec/spec_helper' + +RSpec.describe Datadog::AppSec::Contrib::Devise::Configuration do + let(:settings) { Datadog::Core::Configuration::Settings.new } + + before do + allow(Datadog).to receive(:configuration).and_return(settings) + allow(Datadog).to receive(:logger).and_return(instance_double(Datadog::Core::Logger).as_null_object) + end + + describe '.auto_user_instrumentation_enabled?' do + context 'when auto_user_instrumentation is enabled and track_user_events is enabled' do + before do + settings.appsec.auto_user_instrumentation.mode = 'identification' + settings.appsec.track_user_events.enabled = true + end + + it { expect(described_class).to be_auto_user_instrumentation_enabled } + end + + context 'when auto_user_instrumentation is enabled, but track_user_events is disabled' do + before do + settings.appsec.auto_user_instrumentation.mode = 'identification' + settings.appsec.track_user_events.enabled = false + end + + it { expect(described_class).not_to be_auto_user_instrumentation_enabled } + end + + context 'when auto_user_instrumentation is disabled, but track_user_events is enabled' do + before do + settings.appsec.auto_user_instrumentation.mode = 'disabled' + settings.appsec.track_user_events.enabled = true + end + + it { expect(described_class).not_to be_auto_user_instrumentation_enabled } + end + + context 'when auto_user_instrumentation is disabled and track_user_events is disabled' do + before do + settings.appsec.auto_user_instrumentation.mode = 'disabled' + settings.appsec.track_user_events.enabled = false + end + + it { expect(described_class).not_to be_auto_user_instrumentation_enabled } + end + end + + describe '.auto_user_instrumentation_mode' do + context 'when auto_user_instrumentation is identification and track_user_events is extended' do + before do + settings.appsec.auto_user_instrumentation.mode = 'identification' + settings.appsec.track_user_events.mode = 'extended' + end + + it { expect(described_class.auto_user_instrumentation_mode).to eq('identification') } + end + + context 'when auto_user_instrumentation is identification and track_user_events is safe' do + before do + settings.appsec.auto_user_instrumentation.mode = 'identification' + settings.appsec.track_user_events.mode = 'safe' + end + + it { expect(described_class.auto_user_instrumentation_mode).to eq('anonymization') } + end + + context 'when auto_user_instrumentation is anonymization and track_user_events is extended' do + before do + settings.appsec.auto_user_instrumentation.mode = 'anonymization' + settings.appsec.track_user_events.mode = 'extended' + end + + it { expect(described_class.auto_user_instrumentation_mode).to eq('identification') } + end + + context 'when auto_user_instrumentation is anonymization and track_user_events is safe' do + before do + settings.appsec.auto_user_instrumentation.mode = 'anonymization' + settings.appsec.track_user_events.mode = 'safe' + end + + it { expect(described_class.auto_user_instrumentation_mode).to eq('anonymization') } + end + + context 'when auto_user_instrumentation is identification and track_user_events is invalid' do + before do + settings.appsec.auto_user_instrumentation.mode = 'anonymization' + settings.appsec.track_user_events.mode = 'unknown' + end + + it { expect(described_class.auto_user_instrumentation_mode).to eq('anonymization') } + end + end +end diff --git a/spec/datadog/appsec/contrib/devise/event_spec.rb b/spec/datadog/appsec/contrib/devise/event_spec.rb index e7b7ae0bb4e..f0c19a937a0 100644 --- a/spec/datadog/appsec/contrib/devise/event_spec.rb +++ b/spec/datadog/appsec/contrib/devise/event_spec.rb @@ -1,4 +1,8 @@ +# frozen_string_literal: true + require 'datadog/appsec/spec_helper' +require 'datadog/appsec/contrib/support/devise_user_mock' + require 'datadog/appsec/contrib/devise/resource' require 'datadog/appsec/contrib/devise/event' @@ -6,87 +10,60 @@ let(:event) { described_class.new(resource, mode) } let(:resource) { Datadog::AppSec::Contrib::Devise::Resource.new(object) } - let(:object_class) do - Class.new do - attr_reader :id, :uuid, :email, :username + describe '#to_h' do + context 'when resource is nil' do + let(:event) { described_class.new(nil, 'identification') } - def initialize(id: nil, uuid: nil, email: nil, username: nil) - @id = id - @uuid = uuid - @email = email - @username = username - end + it { expect(event.to_h).to eq({}) } end - end - - context 'without resource' do - let(:resource) { nil } - let(:mode) { 'safe' } - - it do - expect(event.to_h).to eq({}) - end - end - - context 'safe mode' do - let(:mode) { 'safe' } - context 'with ID but not UUID' do - let(:object) { object_class.new(id: 1234) } + context 'when mode is invalid' do + let(:event) { described_class.new(resource, 'invalid') } + let(:resource) { Datadog::AppSec::Contrib::Support::DeviseUserMock.new(id: 1234) } - it do - expect(event.user_id).to be_nil + it 'writes warning log message' do + expect(Datadog.logger).to receive(:warn) + expect(event.to_h).to eq({}) end end - context 'with ID as UUID' do - let(:uuid) { '123e4567-e89b-12d3-a456-426655440000' } - let(:object) { object_class.new(uuid: uuid) } - - it do - expect(event.user_id).to eq(uuid) + context 'when mode is identification and different resource attributes present' do + let(:event) { described_class.new(resource, 'identification') } + let(:resource) do + Datadog::AppSec::Contrib::Support::DeviseUserMock.new(id: 1234, email: 'foo@test.com', username: 'John') end + + it { expect(event.to_h).to eq({ email: 'foo@test.com', username: 'John' }) } end end - context 'extended mode' do - let(:mode) { 'extended' } + describe '#user_id' do + context 'when mode is anonymization and ID is not UUID-like' do + let(:event) { described_class.new(resource, 'anonymization') } + let(:resource) { Datadog::AppSec::Contrib::Support::DeviseUserMock.new(id: 1234) } - context 'ID' do - context 'with ID but not UUID' do - let(:object) { object_class.new(id: 1234) } - - it do - expect(event.user_id).to eq(1234) - end - end + it { expect(event.user_id).to be_nil } + end - context 'with ID as UUID' do - let(:uuid) { '123e4567-e89b-12d3-a456-426655440000' } - let(:object) { object_class.new(uuid: uuid) } + context 'when mode is anonymization and ID is UUID-like' do + let(:event) { described_class.new(resource, 'anonymization') } + let(:resource) { Datadog::AppSec::Contrib::Support::DeviseUserMock.new(id: '00000000-0000-0000-0000-000000000000') } - it do - expect(event.user_id).to eq(uuid) - end - end + it { expect(event.user_id).to eq('00000000-0000-0000-0000-000000000000') } end - context 'Email and username' do - let(:object) { object_class.new(id: 1234, email: 'foo@test.com', username: 'John') } + context 'when mode is identification and ID is not UUID-like' do + let(:event) { described_class.new(resource, 'identification') } + let(:resource) { Datadog::AppSec::Contrib::Support::DeviseUserMock.new(id: 1234) } - it do - expect(event.to_h).to eq({ email: 'foo@test.com', username: 'John' }) - end + it { expect(event.user_id).to eq(1234) } end - end - context 'invalid mode' do - let(:object) { object_class.new(id: 1234) } - let(:mode) { 'invalid' } + context 'when mode is identification and ID is UUID-like' do + let(:event) { described_class.new(resource, 'identification') } + let(:resource) { Datadog::AppSec::Contrib::Support::DeviseUserMock.new(id: '00000000-0000-0000-0000-000000000000') } - it do - expect(Datadog.logger).to receive(:warn) - expect(event.to_h).to eq({}) + it { expect(event.user_id).to eq('00000000-0000-0000-0000-000000000000') } 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 d2fb4e3c338..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 @@ -1,10 +1,19 @@ +# 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/registration_controller_patch' RSpec.describe Datadog::AppSec::Contrib::Devise::Patcher::RegistrationControllerPatch do + 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 + + 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 prepend Datadog::AppSec::Contrib::Devise::Patcher::RegistrationControllerPatch @@ -16,295 +25,355 @@ def initialize(result, resource) def create yield @resource if block_given? + @result end end end - let(:mock_resource) do - Class.new do - attr_reader :id, :email, :username, :persisted + context 'when AppSec is disabled' do + before do + allow(Datadog::AppSec).to receive(:enabled?).and_return(false) - def initialize(id, email, username, persisted) - @id = id - @email = email - @username = username - @persisted = persisted - end - - def persisted? - @persisted - end - - def try(value) - send(value) - end + settings.appsec.track_user_events.enabled = false + settings.appsec.track_user_events.mode = 'safe' end - end - - let(:non_persisted_resource) { mock_resource.new(nil, nil, nil, false) } - let(:persited_resource) { mock_resource.new(SecureRandom.uuid, 'hello@gmail.com', 'John', true) } - let(:automated_track_user_events) { double(enabled: track_user_events_enabled, mode: mode) } - let(:controller) { mock_controller.new(true, resource) } - let(:resource) { non_persisted_resource } - - before do - expect(Datadog::AppSec).to receive(:enabled?).and_return(appsec_enabled) - if appsec_enabled - expect(Datadog.configuration.appsec).to receive(:track_user_events).and_return(automated_track_user_events) - - expect(Datadog::AppSec).to receive(:active_context).and_return(appsec_context) if track_user_events_enabled + let(:controller) { mock_controller.new(true, resource) } + let(:resource) do + Datadog::AppSec::Contrib::Support::DeviseUserMock.new( + id: nil, email: nil, username: nil, persisted: false + ) end - end - context 'AppSec disabled' do - let(:appsec_enabled) { false } - let(:track_user_events_enabled) { false } + context 'when no block is given to registration controller' do + it 'does not track signup event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to_not receive(:track_signup) - it 'do not tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to_not receive(:track_signup) - expect(controller.create).to eq(true) + expect(controller.create).to eq(true) + end end - context 'and a block is given' do - let(:canary) { proc { |resource| } } + context 'when block is given to registration controller' do + let(:canary) { proc { |_resource| } } let(:block) { proc { |resource| canary.call(resource) } } - it 'do not tracks event' do + it 'does not track signup event' do expect(Datadog::AppSec::Contrib::Devise::Tracking).to_not receive(:track_signup) expect(canary).to receive(:call).with(resource) + expect(controller.create(&block)).to eq(true) end end end - context 'Automated user tracking is disabled' do - let(:appsec_enabled) { true } - let(:track_user_events_enabled) { false } - let(:mode) { 'safe' } + context 'when automated user tracking is disabled' do + before do + allow(Datadog::AppSec).to receive(:enabled?).and_return(true) + + settings.appsec.track_user_events.enabled = false + settings.appsec.track_user_events.mode = 'safe' + end - it 'do not tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to_not receive(:track_signup) - expect(controller.create).to eq(true) + let(:controller) { mock_controller.new(true, resource) } + let(:resource) do + Datadog::AppSec::Contrib::Support::DeviseUserMock.new( + id: nil, email: nil, username: nil, persisted: false + ) end - context 'and a block is given' do - let(:canary) { proc { |resource| } } + context 'when no block is given to registration controller' do + it 'does not track signup event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to_not receive(:track_signup) + + expect(controller.create).to eq(true) + end + end + + context 'when block is given to registration controller' do + let(:canary) { proc { |_resource| } } let(:block) { proc { |resource| canary.call(resource) } } - it 'do not tracks event' do + it 'does not track signup event' do expect(Datadog::AppSec::Contrib::Devise::Tracking).to_not receive(:track_signup) expect(canary).to receive(:call).with(resource) + expect(controller.create(&block)).to eq(true) end 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) + + settings.appsec.track_user_events.enabled = true + settings.appsec.track_user_events.mode = 'safe' + end + + let(:controller) { mock_controller.new(true, resource) } + let(:resource) do + Datadog::AppSec::Contrib::Support::DeviseUserMock.new( + id: nil, email: nil, username: nil, persisted: false + ) + end - it 'do not tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to_not receive(:track_signup) - expect(controller.create).to eq(true) + context 'when no block is given to registration controller' do + it 'does not track signup event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to_not receive(:track_signup) + + expect(controller.create).to eq(true) + end end - context 'and a block is given' do - let(:canary) { proc { |resource| } } + context 'when block is given to registration controller' do + let(:canary) { proc { |_resource| } } let(:block) { proc { |resource| canary.call(resource) } } - it 'do not tracks event' do + it 'does not track signup event' do expect(Datadog::AppSec::Contrib::Devise::Tracking).to_not receive(:track_signup) expect(canary).to receive(:call).with(resource) + expect(controller.create(&block)).to eq(true) end end end - context 'with persisted resource' 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 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) - context 'with resource ID' do - let(:resource) { persited_resource } + settings.appsec.track_user_events.enabled = true + settings.appsec.track_user_events.mode = 'safe' + end - context 'and a block is given' do - let(:canary) { proc { |resource| } } - let(:block) { proc { |resource| canary.call(resource) } } + let(:controller) { mock_controller.new(true, resource) } + let(:active_context) { instance_double(Datadog::AppSec::Context, trace: double, span: double) } + + context 'when current user has an extractable ID' do + let(:resource) do + Datadog::AppSec::Contrib::Support::DeviseUserMock.new( + id: '00000000-0000-0000-0000-000000000000', + email: 'hello@gmail.com', + username: 'John', + persisted: true + ) + end - context 'safe mode' do - let(:mode) { 'safe' } + context 'when no block is given to registration controller' do + context 'when tracking mode set to safe' do + before { settings.appsec.track_user_events.mode = 'safe' } - it 'tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_signup).with( - appsec_context.trace, - appsec_context.span, - user_id: resource.id, - **{} - ) - expect(canary).to receive(:call).with(resource) - expect(controller.create(&block)).to eq(true) + it 'tracks signup event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_signup) + .with(active_context.trace, active_context.span, user_id: resource.id, **{}) + + expect(controller.create).to eq(true) end end - context 'extended mode' do - let(:mode) { 'extended' } + context 'when tracking mode set to extended' do + before { settings.appsec.track_user_events.mode = 'extended' } - it 'tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_signup).with( - appsec_context.trace, - appsec_context.span, - user_id: resource.id, - **{ email: 'hello@gmail.com', username: 'John' } - ) - expect(canary).to receive(:call).with(resource) - expect(controller.create(&block)).to eq(true) + it 'tracks signup event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_signup) + .with( + active_context.trace, + active_context.span, + user_id: '00000000-0000-0000-0000-000000000000', + **{ email: 'hello@gmail.com', username: 'John' } + ) + + expect(controller.create).to eq(true) end end end - context 'safe mode' do - let(:mode) { 'safe' } + context 'when block is given to registration controller' do + let(:canary) { proc { |_resource| } } + let(:block) { proc { |resource| canary.call(resource) } } - it 'tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_signup).with( - appsec_context.trace, - appsec_context.span, - user_id: resource.id, - **{} - ) - expect(controller.create).to eq(true) + context 'when tracking mode set to safe' do + before { settings.appsec.track_user_events.mode = 'safe' } + + it 'tracks signup event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_signup) + .with( + active_context.trace, + active_context.span, + user_id: '00000000-0000-0000-0000-000000000000', + **{} + ) + expect(canary).to receive(:call).with(resource) + + expect(controller.create(&block)).to eq(true) + end end - end - context 'extended mode' do - let(:mode) { 'extended' } + context 'when tracking mode set to extended' do + before { settings.appsec.track_user_events.mode = 'extended' } + + it 'tracks signup event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_signup) + .with( + active_context.trace, + active_context.span, + user_id: '00000000-0000-0000-0000-000000000000', + **{ email: 'hello@gmail.com', username: 'John' } + ) + expect(canary).to receive(:call).with(resource) - it 'tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_signup).with( - appsec_context.trace, - appsec_context.span, - user_id: resource.id, - **{ email: 'hello@gmail.com', username: 'John' } - ) - expect(controller.create).to eq(true) + expect(controller.create(&block)).to eq(true) + end end end end - context 'without resource ID' do - let(:resource) { mock_resource.new(nil, 'hello@gmail.com', 'John', true) } + context 'when current user does not have an extractable ID' do + let(:resource) do + Datadog::AppSec::Contrib::Support::DeviseUserMock.new( + id: nil, email: 'hello@gmail.com', username: 'John', persisted: true + ) + end - context 'and a block is given' do - let(:canary) { proc { |resource| } } + context 'when block is given to registration controller' do + let(:canary) { proc { |_resource| } } let(:block) { proc { |resource| canary.call(resource) } } - context 'safe mode' do - let(:mode) { 'safe' } - - it 'tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_signup).with( - appsec_context.trace, - appsec_context.span, - user_id: nil, - **{} - ) + context 'when tracking mode set to safe' do + before { settings.appsec.track_user_events.mode = 'safe' } + + it 'tracks signup event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_signup) + .with( + active_context.trace, + active_context.span, + user_id: nil, + **{} + ) expect(canary).to receive(:call).with(resource) + expect(controller.create(&block)).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_signup).with( - appsec_context.trace, - appsec_context.span, - user_id: nil, - **{ email: 'hello@gmail.com', username: 'John' } - ) + context 'when tracking mode set to extended' do + before { settings.appsec.track_user_events.mode = 'extended' } + + it 'tracks signup event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_signup) + .with( + active_context.trace, + active_context.span, + user_id: nil, + **{ email: 'hello@gmail.com', username: 'John' } + ) expect(canary).to receive(:call).with(resource) + expect(controller.create(&block)).to eq(true) end end end - context 'safe mode' do - let(:mode) { 'safe' } + context 'when no block is given to registration controller' do + context 'when tracking mode set to safe' do + before { settings.appsec.track_user_events.mode = 'safe' } - it 'tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_signup).with( - appsec_context.trace, - appsec_context.span, - user_id: nil, - **{} - ) - expect(controller.create).to eq(true) + it 'tracks signup event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_signup) + .with( + active_context.trace, + active_context.span, + user_id: nil, + **{} + ) + + expect(controller.create).to eq(true) + end end - end - context 'extended mode' do - let(:mode) { 'extended' } + context 'when tracking mode set to extended' do + before { settings.appsec.track_user_events.mode = 'extended' } - it 'tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_signup).with( - appsec_context.trace, - appsec_context.span, - user_id: nil, - **{ email: 'hello@gmail.com', username: 'John' } - ) - expect(controller.create).to eq(true) + it 'tracks signup event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to receive(:track_signup) + .with( + active_context.trace, + active_context.span, + user_id: nil, + **{ email: 'hello@gmail.com', username: 'John' } + ) + + expect(controller.create).to eq(true) + end end end end end - context 'with non persisted resource' do - let(:appsec_enabled) { true } - let(:track_user_events_enabled) { true } - let(:appsec_context) { instance_double(Datadog::AppSec::Context, trace: double, span: double) } - let(:resource) { non_persisted_resource } + 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) - context 'safe mode' do - let(:mode) { 'safe' } + settings.appsec.track_user_events.enabled = true + settings.appsec.track_user_events.mode = 'safe' + end - it 'do not tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to_not receive(:track_signup) - expect(controller.create).to eq(true) + let(:active_context) { instance_double(Datadog::AppSec::Context, trace: double, span: double) } + let(:controller) { mock_controller.new(true, resource) } + let(:resource) do + Datadog::AppSec::Contrib::Support::DeviseUserMock.new( + id: nil, email: nil, username: nil, persisted: false + ) + end + + context 'when block is not given to registration controller' do + context 'when tracking mode set to safe' do + before { settings.appsec.track_user_events.mode = 'safe' } + + it 'does not track signup event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to_not receive(:track_signup) + + expect(controller.create).to eq(true) + end end - context 'and a block is given' do - let(:canary) { proc { |resource| } } - let(:block) { proc { |resource| canary.call(resource) } } + context 'when tracking mode set to extended' do + before { settings.appsec.track_user_events.mode = 'extended' } - it 'do not tracks event' do + it 'does not track signup event' do expect(Datadog::AppSec::Contrib::Devise::Tracking).to_not receive(:track_signup) - expect(canary).to receive(:call).with(resource) - expect(controller.create(&block)).to eq(true) + + expect(controller.create).to eq(true) end end end - context 'extended mode' do - let(:mode) { 'extended' } + context 'when block is given to registration controller' do + let(:canary) { proc { |_resource| } } + let(:block) { proc { |resource| canary.call(resource) } } - it 'tracks event' do - expect(Datadog::AppSec::Contrib::Devise::Tracking).to_not receive(:track_signup) - expect(controller.create).to eq(true) + context 'when tracking mode set to safe' do + before { settings.appsec.track_user_events.mode = 'safe' } + + it 'does not track signup event' do + expect(Datadog::AppSec::Contrib::Devise::Tracking).to_not receive(:track_signup) + expect(canary).to receive(:call).with(resource) + + expect(controller.create(&block)).to eq(true) + end end - context 'and a block is given' do - let(:canary) { proc { |resource| } } - let(:block) { proc { |resource| canary.call(resource) } } + context 'when tracking mode set to extended' do + before { settings.appsec.track_user_events.mode = 'extended' } - it 'do not tracks event' do + it 'does not track signup event' do expect(Datadog::AppSec::Contrib::Devise::Tracking).to_not receive(:track_signup) expect(canary).to receive(:call).with(resource) + expect(controller.create(&block)).to eq(true) end end diff --git a/spec/datadog/appsec/contrib/support/devise_user_mock.rb b/spec/datadog/appsec/contrib/support/devise_user_mock.rb new file mode 100644 index 00000000000..f754e4a0b62 --- /dev/null +++ b/spec/datadog/appsec/contrib/support/devise_user_mock.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Datadog + module AppSec + module Contrib + module Support + # A basic User model mock sufficient for devise testing + DeviseUserMock = Struct.new(:id, :uuid, :email, :username, :persisted, keyword_init: true) do + alias_method :persisted?, :persisted + end + end + end + end +end