Skip to content

Commit 23fb133

Browse files
authored
Merge pull request #4433 from DataDog/appsec-56683-change-devise-auto-user-instrumentation-to-v3
[APPSEC-56683] Introduce automated user tracking V3
2 parents 1f877ff + 905407c commit 23fb133

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+3101
-1542
lines changed

.github/workflows/system-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ permissions: {}
2222
env:
2323
REGISTRY: ghcr.io
2424
REPO: ghcr.io/datadog/dd-trace-rb
25-
SYSTEM_TESTS_REF: main # This must always be set to `main` on dd-trace-rb's master branch
25+
SYSTEM_TESTS_REF: appsec-56683-rewrite-rails-devise-application
2626

2727
jobs:
2828
changes:

Matrixfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@
283283
'appsec:devise' => {
284284
# NOTE: JRuby bundler failed to install some dependencies https://github.com/ruby/psych/issues/700
285285
# and it could be re-enabled when upstream fix the issue
286-
'devise-min' => '✅ 2.5 / ✅ 2.6 / 2.7 / ❌ 3.0 / ❌ 3.1 / ❌ 3.2 / ❌ 3.3 / ❌ 3.4 / ❌ jruby',
286+
'devise-min' => '✅ 2.5 / ✅ 2.6 / 2.7 / ❌ 3.0 / ❌ 3.1 / ❌ 3.2 / ❌ 3.3 / ❌ 3.4 / ❌ jruby',
287287
'devise-latest' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ❌ jruby'
288288
},
289289
'appsec:rails' => {

lib/datadog/appsec/anonymizer.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
3+
require 'digest/sha2'
4+
5+
module Datadog
6+
module AppSec
7+
# Manual anonymization of the potential PII data
8+
module Anonymizer
9+
def self.anonymize(payload)
10+
raise ArgumentError, "expected String, received #{payload.class}" unless payload.is_a?(String)
11+
12+
"anon_#{Digest::SHA256.hexdigest(payload)[0, 32]}"
13+
end
14+
end
15+
end
16+
end

lib/datadog/appsec/configuration/settings.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,10 +238,10 @@ def self.add_settings!(base)
238238
Datadog.logger.warn(
239239
'The appsec.auto_user_instrumentation.mode value provided is not supported. ' \
240240
"Supported values are: #{AUTO_USER_INSTRUMENTATION_MODES.join(' | ')}. " \
241-
"Using default value: #{IDENTIFICATION_AUTO_USER_INSTRUMENTATION_MODE}."
241+
"Using value: #{DISABLED_AUTO_USER_INSTRUMENTATION_MODE}."
242242
)
243243

244-
IDENTIFICATION_AUTO_USER_INSTRUMENTATION_MODE
244+
DISABLED_AUTO_USER_INSTRUMENTATION_MODE
245245
end
246246
end
247247
end

lib/datadog/appsec/contrib/devise/configuration.rb

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,11 @@ module Devise
77
# A temporary configuration module to accomodate new RFC changes.
88
# NOTE: DEV-3 Remove module
99
module Configuration
10-
MODES_CONVERSION_RULES = {
11-
track_user_to_auto_instrumentation: {
12-
AppSec::Configuration::Settings::SAFE_TRACK_USER_EVENTS_MODE =>
10+
TRACK_USER_EVENTS_CONVERSION_RULES = {
11+
AppSec::Configuration::Settings::SAFE_TRACK_USER_EVENTS_MODE =>
1312
AppSec::Configuration::Settings::ANONYMIZATION_AUTO_USER_INSTRUMENTATION_MODE,
14-
AppSec::Configuration::Settings::EXTENDED_TRACK_USER_EVENTS_MODE =>
13+
AppSec::Configuration::Settings::EXTENDED_TRACK_USER_EVENTS_MODE =>
1514
AppSec::Configuration::Settings::IDENTIFICATION_AUTO_USER_INSTRUMENTATION_MODE
16-
}.freeze,
17-
auto_instrumentation_to_track_user: {
18-
AppSec::Configuration::Settings::ANONYMIZATION_AUTO_USER_INSTRUMENTATION_MODE =>
19-
AppSec::Configuration::Settings::SAFE_TRACK_USER_EVENTS_MODE,
20-
AppSec::Configuration::Settings::IDENTIFICATION_AUTO_USER_INSTRUMENTATION_MODE =>
21-
AppSec::Configuration::Settings::EXTENDED_TRACK_USER_EVENTS_MODE
22-
}.freeze
2315
}.freeze
2416

2517
module_function
@@ -44,30 +36,14 @@ def auto_user_instrumentation_mode
4436
appsec.auto_user_instrumentation.mode
4537
appsec.track_user_events.mode
4638

47-
if !appsec.auto_user_instrumentation.options[:mode].default_precedence? &&
48-
appsec.track_user_events.options[:mode].default_precedence?
49-
return appsec.auto_user_instrumentation.mode
50-
end
51-
52-
if appsec.auto_user_instrumentation.options[:mode].default_precedence?
53-
return MODES_CONVERSION_RULES[:track_user_to_auto_instrumentation].fetch(
39+
if !appsec.track_user_events.options[:mode].default_precedence? &&
40+
appsec.auto_user_instrumentation.options[:mode].default_precedence?
41+
return TRACK_USER_EVENTS_CONVERSION_RULES.fetch(
5442
appsec.track_user_events.mode, appsec.auto_user_instrumentation.mode
5543
)
5644
end
5745

58-
identification_mode = AppSec::Configuration::Settings::IDENTIFICATION_AUTO_USER_INSTRUMENTATION_MODE
59-
if appsec.auto_user_instrumentation.mode == identification_mode ||
60-
appsec.track_user_events.mode == AppSec::Configuration::Settings::EXTENDED_TRACK_USER_EVENTS_MODE
61-
return identification_mode
62-
end
63-
64-
AppSec::Configuration::Settings::ANONYMIZATION_AUTO_USER_INSTRUMENTATION_MODE
65-
end
66-
67-
# NOTE: Remove in next version of tracking
68-
def track_user_events_mode
69-
MODES_CONVERSION_RULES[:auto_instrumentation_to_track_user]
70-
.fetch(auto_user_instrumentation_mode, Datadog.configuration.appsec.track_user_events.mode)
46+
appsec.auto_user_instrumentation.mode
7147
end
7248
end
7349
end
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# frozen_string_literal: true
2+
3+
require_relative '../../anonymizer'
4+
5+
module Datadog
6+
module AppSec
7+
module Contrib
8+
module Devise
9+
# Extracts user identification data from Devise resources.
10+
# Supports both regular and anonymized data extraction modes.
11+
class DataExtractor
12+
PRIORITY_ORDERED_ID_KEYS = [:id, 'id', :uuid, 'uuid'].freeze
13+
PRIORITY_ORDERED_LOGIN_KEYS = [:email, 'email', :username, 'username', :login, 'login'].freeze
14+
15+
def initialize(mode:)
16+
@mode = mode
17+
@devise_scopes = {}
18+
end
19+
20+
def extract_id(object)
21+
return if object.nil?
22+
23+
if object.respond_to?(:[])
24+
id = object[PRIORITY_ORDERED_ID_KEYS.find { |key| object[key] }]
25+
scope = find_devise_scope(object)
26+
27+
id = "#{scope}:#{id}" if id && scope
28+
return transform(id)
29+
end
30+
31+
id = object.id if object.respond_to?(:id)
32+
id ||= object.uuid if object.respond_to?(:uuid)
33+
34+
scope = find_devise_scope(object)
35+
id = "#{scope}:#{id}" if id && scope
36+
37+
transform(id)
38+
end
39+
40+
def extract_login(object)
41+
return if object.nil?
42+
43+
if object.respond_to?(:[])
44+
login = object[PRIORITY_ORDERED_LOGIN_KEYS.find { |key| object[key] }]
45+
return transform(login)
46+
end
47+
48+
login = object.email if object.respond_to?(:email)
49+
login ||= object.username if object.respond_to?(:username)
50+
login ||= object.login if object.respond_to?(:login)
51+
52+
transform(login)
53+
end
54+
55+
private
56+
57+
def find_devise_scope(object)
58+
return if ::Devise.mappings.count == 1
59+
60+
@devise_scopes[object.class.name] ||= begin
61+
::Devise.mappings.each_value.find { |mapping| mapping.class_name == object.class.name }&.name
62+
end
63+
end
64+
65+
def transform(value)
66+
return if value.nil?
67+
return value.to_s unless anonymize?
68+
69+
Anonymizer.anonymize(value.to_s)
70+
end
71+
72+
def anonymize?
73+
@mode == AppSec::Configuration::Settings::ANONYMIZATION_AUTO_USER_INSTRUMENTATION_MODE
74+
end
75+
end
76+
end
77+
end
78+
end
79+
end

lib/datadog/appsec/contrib/devise/event.rb

Lines changed: 0 additions & 54 deletions
This file was deleted.

lib/datadog/appsec/contrib/devise/ext.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,27 @@ module Contrib
66
module Devise
77
# Devise integration constants
88
module Ext
9+
EVENT_LOGIN_SUCCESS = 'users.login.success'
10+
EVENT_LOGIN_FAILURE = 'users.login.failure'
11+
EVENT_SIGNUP = 'users.signup'
12+
13+
TAG_DD_USR_ID = '_dd.appsec.usr.id'
14+
TAG_DD_USR_LOGIN = '_dd.appsec.usr.login'
15+
TAG_DD_SIGNUP_MODE = '_dd.appsec.events.users.signup.auto.mode'
16+
TAG_DD_COLLECTION_MODE = '_dd.appsec.user.collection_mode'
17+
TAG_DD_LOGIN_SUCCESS_MODE = '_dd.appsec.events.users.login.success.auto.mode'
18+
TAG_DD_LOGIN_FAILURE_MODE = '_dd.appsec.events.users.login.failure.auto.mode'
19+
20+
TAG_USR_ID = 'usr.id'
21+
TAG_SIGNUP_TRACK = 'appsec.events.users.signup.track'
22+
TAG_SIGNUP_USR_ID = 'appsec.events.users.signup.usr.id'
23+
TAG_SIGNUP_USR_LOGIN = 'appsec.events.users.signup.usr.login'
24+
TAG_LOGIN_FAILURE_TRACK = 'appsec.events.users.login.failure.track'
25+
TAG_LOGIN_FAILURE_USR_ID = 'appsec.events.users.login.failure.usr.id'
26+
TAG_LOGIN_FAILURE_USR_LOGIN = 'appsec.events.users.login.failure.usr.login'
27+
TAG_LOGIN_FAILURE_USR_EXISTS = 'appsec.events.users.login.failure.usr.exists'
28+
TAG_LOGIN_SUCCESS_TRACK = 'appsec.events.users.login.success.track'
29+
TAG_LOGIN_SUCCESS_USR_LOGIN = 'appsec.events.users.login.success.usr.login'
930
end
1031
end
1132
end

lib/datadog/appsec/contrib/devise/integration.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# frozen_string_literal: true
22

33
require_relative '../integration'
4-
54
require_relative 'patcher'
65

76
module Datadog

lib/datadog/appsec/contrib/devise/patcher.rb

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
# frozen_string_literal: true
22

3-
require_relative 'patcher/authenticatable_patch'
4-
require_relative 'patcher/rememberable_patch'
5-
require_relative 'patcher/registration_controller_patch'
3+
require_relative '../../../core/utils/only_once'
4+
5+
require_relative 'tracking_middleware'
6+
require_relative 'patches/signup_tracking_patch'
7+
require_relative 'patches/signin_tracking_patch'
8+
require_relative 'patches/skip_signin_tracking_patch'
69

710
module Datadog
811
module AppSec
912
module Contrib
1013
module Devise
11-
# Patcher for AppSec on Devise
14+
# Devise patcher
1215
module Patcher
16+
GUARD_ONCE_PER_APP = Hash.new do |hash, key|
17+
hash[key] = Datadog::Core::Utils::OnlyOnce.new
18+
end
19+
1320
module_function
1421

1522
def patched?
@@ -21,29 +28,35 @@ def target_version
2128
end
2229

2330
def patch
24-
patch_authenticatable_strategy
25-
patch_rememberable_strategy
26-
patch_registration_controller
27-
28-
Patcher.instance_variable_set(:@patched, true)
29-
end
30-
31-
def patch_authenticatable_strategy
32-
::Devise::Strategies::Authenticatable.prepend(AuthenticatablePatch)
33-
end
31+
::ActiveSupport.on_load(:before_initialize) do |app|
32+
GUARD_ONCE_PER_APP[app].run do
33+
begin
34+
app.middleware.insert_after(Warden::Manager, TrackingMiddleware)
35+
rescue RuntimeError
36+
AppSec.telemetry.error('AppSec: unable to insert Devise TrackingMiddleware')
37+
end
38+
end
39+
end
3440

35-
def patch_rememberable_strategy
36-
return unless ::Devise::STRATEGIES.include?(:rememberable)
41+
::ActiveSupport.on_load(:after_initialize) do
42+
if ::Devise::RegistrationsController.descendants.empty?
43+
::Devise::RegistrationsController.prepend(Patches::SignupTrackingPatch)
44+
else
45+
::Devise::RegistrationsController.descendants.each do |controller|
46+
controller.prepend(Patches::SignupTrackingPatch)
47+
end
48+
end
49+
end
3750

38-
# Rememberable strategy is required in autoloaded Rememberable model
39-
::Devise::Models::Rememberable # rubocop:disable Lint/Void
40-
::Devise::Strategies::Rememberable.prepend(RememberablePatch)
41-
end
51+
::Devise::Strategies::Authenticatable.prepend(Patches::SigninTrackingPatch)
4252

43-
def patch_registration_controller
44-
::ActiveSupport.on_load(:after_initialize) do
45-
::Devise::RegistrationsController.prepend(RegistrationControllerPatch)
53+
if ::Devise::STRATEGIES.include?(:rememberable)
54+
# Rememberable strategy is required in autoloaded Rememberable model
55+
require 'devise/models/rememberable'
56+
::Devise::Strategies::Rememberable.prepend(Patches::SkipSigninTrackingPatch)
4657
end
58+
59+
Patcher.instance_variable_set(:@patched, true)
4760
end
4861
end
4962
end

0 commit comments

Comments
 (0)