From 0d2bc4339603570c2cbf9c3f90ff9e463e8595f5 Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Tue, 1 Apr 2025 14:47:29 +0200 Subject: [PATCH 1/3] Add handling of string keys for Kit:AppSec::Events --- lib/datadog/kit/appsec/events.rb | 9 ++++--- sig/datadog/kit/appsec/events.rbs | 19 +++++++++++--- spec/datadog/kit/appsec/events_spec.rb | 35 ++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/lib/datadog/kit/appsec/events.rb b/lib/datadog/kit/appsec/events.rb index 9811727185a..255268f5124 100644 --- a/lib/datadog/kit/appsec/events.rb +++ b/lib/datadog/kit/appsec/events.rb @@ -10,6 +10,7 @@ module Events LOGIN_SUCCESS_EVENT = 'users.login.success' LOGIN_FAILURE_EVENT = 'users.login.failure' SIGNUP_EVENT = 'users.signup' + USER_LOGIN_KEYS = ['usr.login', :'usr.login'].freeze class << self # Attach login success event information to the trace @@ -30,10 +31,11 @@ def track_login_success(trace = nil, span = nil, user:, **others) set_trace_and_span_context('track_login_success', trace, span) do |active_trace, active_span| user_options = user.dup user_id = user_options.delete(:id) - user_login = user_options[:login] || others[:'usr.login'] || user_id + user_login = user_options[:login] || others[:'usr.login'] || others['usr.login'] || user_id raise ArgumentError, 'missing required key: :user => { :id }' if user_id.nil? + others = others.reject { |key, _| USER_LOGIN_KEYS.include?(key) } others[:'usr.login'] = user_login track(LOGIN_SUCCESS_EVENT, active_trace, active_span, **others) @@ -58,7 +60,7 @@ def track_login_success(trace = nil, span = nil, user:, **others) # event information to attach to the trace. def track_login_failure(trace = nil, span = nil, user_exists:, user_id: nil, **others) set_trace_and_span_context('track_login_failure', trace, span) do |active_trace, active_span| - others[:'usr.login'] = user_id if user_id && !others.key?(:'usr.login') + others[:'usr.login'] = user_id if user_id && !others.key?(:'usr.login') && !others.key?('usr.login') track(LOGIN_FAILURE_EVENT, active_trace, active_span, **others) active_span.set_tag('appsec.events.users.login.failure.usr.id', user_id) if user_id @@ -84,10 +86,11 @@ def track_signup(trace = nil, span = nil, user:, **others) set_trace_and_span_context('track_signup', trace, span) do |active_trace, active_span| user_options = user.dup user_id = user_options.delete(:id) - user_login = user_options[:login] || others[:'usr.login'] || user_id + user_login = user_options[:login] || others[:'usr.login'] || others['usr.login'] || user_id raise ArgumentError, 'missing required key: :user => { :id }' if user_id.nil? + others = others.reject { |key, _| USER_LOGIN_KEYS.include?(key) } others[:'usr.login'] = user_login track(SIGNUP_EVENT, active_trace, active_span, **others) diff --git a/sig/datadog/kit/appsec/events.rbs b/sig/datadog/kit/appsec/events.rbs index f674d4f91ba..89c7f5e6899 100644 --- a/sig/datadog/kit/appsec/events.rbs +++ b/sig/datadog/kit/appsec/events.rbs @@ -3,13 +3,26 @@ module Datadog module AppSec module Events LOGIN_SUCCESS_EVENT: ::String + LOGIN_FAILURE_EVENT: ::String - def self.track_login_success: (Datadog::Tracing::TraceOperation trace, user: Hash[::Symbol, ::String | nil], **::Hash[::Symbol, ::String | nil] others) -> void + SIGNUP_EVENT: ::String + + USER_LOGIN_KEYS: Array[String | Symbol] + + def self.track_login_success: (?Tracing::TraceOperation? trace, ?Tracing::SpanOperation? span, user: Hash[Symbol, String], **Hash[Symbol | String, String] others) -> void + + def self.track_login_failure: (?Tracing::TraceOperation? trace, ?Tracing::SpanOperation? span, user_exists: bool, ?user_id: String?, **Hash[Symbol | String, String] others) -> void + + def self.track_signup: (?Tracing::TraceOperation? trace, ?Tracing::SpanOperation? span, user: Hash[Symbol, String], **Hash[Symbol | String, String] others) -> void + + def self.track: (String event, ?Tracing::TraceOperation? trace, ?Tracing::SpanOperation? span, **Hash[Symbol, String] others) -> void + + private - def self.track_login_failure: (Datadog::Tracing::TraceOperation trace, user_id: ::String, user_exists: bool, **::Hash[::Symbol, ::String | nil] others) -> void + def self.set_trace_and_span_context: (String method, ?Tracing::TraceOperation? trace, ?Tracing::SpanOperation? span) { (Tracing::TraceOperation, Tracing::SpanOperation) -> void } -> void - def self.track: (::String | ::Symbol event, Datadog::Tracing::TraceOperation trace, **::Hash[::Symbol, ::String | nil] others) -> void + def self.check_trace_span_integrity: (Tracing::TraceOperation trace, Tracing::SpanOperation span) -> void end end end diff --git a/spec/datadog/kit/appsec/events_spec.rb b/spec/datadog/kit/appsec/events_spec.rb index 64e2ee67498..33801b2d819 100644 --- a/spec/datadog/kit/appsec/events_spec.rb +++ b/spec/datadog/kit/appsec/events_spec.rb @@ -93,6 +93,19 @@ end end + if PlatformHelpers.mri? && PlatformHelpers.engine_version >= Gem::Version.new('2.7') + it 'sets additional user login data from other string keys as tags' do + trace_op.measure('root') do |span, _| + expect { described_class.track_login_success(trace_op, user: { id: '42' }, 'usr.login' => 'hey') } + .to change { span.tags }.to include( + 'usr.id' => '42', + 'usr.login' => 'hey', + 'appsec.events.users.login.success.usr.login' => 'hey' + ) + end + end + end + it 'sets event tracking key on trace' do trace_op.measure('root') do |span, _| expect { described_class.track_login_success(trace_op, user: { id: '42' }) } @@ -181,6 +194,15 @@ end end + if PlatformHelpers.mri? && PlatformHelpers.engine_version >= Gem::Version.new('2.7') + it 'sets additional user login data from other string keys as tags' do + trace_op.measure('root') do |span, _| + expect { described_class.track_login_failure(trace_op, user_id: '42', user_exists: true, 'usr.login' => 'hey') } + .to change { span.tags }.to include('appsec.events.users.login.failure.usr.login' => 'hey') + end + end + end + it 'sets event tracking key on trace' do trace_op.measure('root') do |span, _trace| described_class.track_login_failure(trace_op, user_id: '42', user_exists: true) @@ -286,6 +308,19 @@ end end + if PlatformHelpers.mri? && PlatformHelpers.engine_version >= Gem::Version.new('2.7') + it 'sets additional user login data from other string keys as tags' do + trace_op.measure('root') do |span, _| + expect { described_class.track_signup(trace_op, user: { id: '42' }, 'usr.login' => 'hey') } + .to change { span.tags }.to include( + 'usr.id' => '42', + 'usr.login' => 'hey', + 'appsec.events.users.signup.usr.login' => 'hey' + ) + end + end + end + it 'sets additional user login data as tags with user data priority' do trace_op.measure('root') do |span, _| expect { described_class.track_signup(trace_op, user: { id: '42', login: 'hey' }, 'usr.login': 'extra') } From 55063e790cb53753462dc77cc2596bc31515c8e6 Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Tue, 1 Apr 2025 15:40:43 +0200 Subject: [PATCH 2/3] Add PlatformHelper.engine_version_matches? helper It will allow us to have guard-clause for some specific cases. Example: ```ruby it 'runs only on Ruby > 3.0', ruby: '> 3.0' do expect(something).to be(true) end ``` --- spec/spec_helper.rb | 23 +++++++++++++++++++++++ spec/support/platform_helpers.rb | 25 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d35a7041a59..ef3872bfe41 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -103,6 +103,29 @@ config.pending_failure_output = :full end + # Guard-clause to skip tests that require a specific Ruby version. + # Should work on anything that supports filters, i.e it/describe/context. + # + # Examples: + # + # 1. Guard with explicit matcher `>` (greater than) + # Supported operators: `>`, `>=`, `==`, `!=`, `<`, `<=` + # + # WARNING: Space between operator and version is required. + # + # it 'runs only for specific Ruby version', ruby: '> 2.7' do + # expect(something).to be_good + # end + # + # 2. Guard with implicit matcher `==` (equal to) + # + # it 'runs only for Ruby 2.7.x', ruby: '2.7' do + # expect(something).to be_good + # end + config.before(:each, ruby: ->(value) { !PlatformHelpers.ruby_version_matches?(value) }) do |example| + skip "Test requires Ruby #{example.metadata[:ruby]}" + end + config.before(:example, ractors: true) do unless config.filter_manager.inclusions[:ractors] skip 'Skipping ractor tests. Use rake spec:profiling:ractors or pass -t ractors to rspec to run.' diff --git a/spec/support/platform_helpers.rb b/spec/support/platform_helpers.rb index 590a332cbd9..dd98f0e3339 100644 --- a/spec/support/platform_helpers.rb +++ b/spec/support/platform_helpers.rb @@ -3,6 +3,9 @@ require 'os' module PlatformHelpers + EQUALITY_OPERATOR = '==' + ALLOWED_COMPARISON_OPERATORS = %w[> >= == != < <=].freeze + module_function # Ruby runtime engines @@ -24,6 +27,28 @@ def engine_version Gem::Version.new(version) end + def ruby_version_matches?(matcher_with_ruby_version) + ruby_version = Gem::Version.new(RUBY_VERSION) + operator, guard_version = matcher_with_ruby_version.split(' ', 2).tap { |array| array.unshift('==') if array.size == 1 } + + unless ALLOWED_COMPARISON_OPERATORS.include?(operator) + message = "Unsupported operator: #{operator}. Supported operators: #{ALLOWED_COMPARISON_OPERATORS.join(', ')}" + raise ArgumentError, message + end + + unless Gem::Version.correct?(guard_version) + message = "Invalid version: #{guard_version}. Make sure to add space between operator and version." + raise ArgumentError, message + end + + if operator == EQUALITY_OPERATOR && guard_version.count('.') < 3 + version = Gem::Version.new("#{guard_version}.0") + (version...version.bump).cover?(ruby_version) + else + ruby_version.send(operator, Gem::Version.new(guard_version)) + end + end + # Operating systems def linux? From 2c399f53433ee7a488b71c02d84ad05c411c33c7 Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Tue, 1 Apr 2025 15:42:53 +0200 Subject: [PATCH 3/3] Adjust tests with new Ruby version helper --- spec/datadog/kit/appsec/events_spec.rb | 46 +++++++++++--------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/spec/datadog/kit/appsec/events_spec.rb b/spec/datadog/kit/appsec/events_spec.rb index 33801b2d819..6f43b86928d 100644 --- a/spec/datadog/kit/appsec/events_spec.rb +++ b/spec/datadog/kit/appsec/events_spec.rb @@ -93,16 +93,14 @@ end end - if PlatformHelpers.mri? && PlatformHelpers.engine_version >= Gem::Version.new('2.7') - it 'sets additional user login data from other string keys as tags' do - trace_op.measure('root') do |span, _| - expect { described_class.track_login_success(trace_op, user: { id: '42' }, 'usr.login' => 'hey') } - .to change { span.tags }.to include( - 'usr.id' => '42', - 'usr.login' => 'hey', - 'appsec.events.users.login.success.usr.login' => 'hey' - ) - end + it 'sets additional user login data from other string keys as tags', ruby: '>= 2.7' do + trace_op.measure('root') do |span, _| + expect { described_class.track_login_success(trace_op, user: { id: '42' }, 'usr.login' => 'hey') } + .to change { span.tags }.to include( + 'usr.id' => '42', + 'usr.login' => 'hey', + 'appsec.events.users.login.success.usr.login' => 'hey' + ) end end @@ -194,12 +192,10 @@ end end - if PlatformHelpers.mri? && PlatformHelpers.engine_version >= Gem::Version.new('2.7') - it 'sets additional user login data from other string keys as tags' do - trace_op.measure('root') do |span, _| - expect { described_class.track_login_failure(trace_op, user_id: '42', user_exists: true, 'usr.login' => 'hey') } - .to change { span.tags }.to include('appsec.events.users.login.failure.usr.login' => 'hey') - end + it 'sets additional user login data from other string keys as tags', ruby: '>= 2.7' do + trace_op.measure('root') do |span, _| + expect { described_class.track_login_failure(trace_op, user_id: '42', user_exists: true, 'usr.login' => 'hey') } + .to change { span.tags }.to include('appsec.events.users.login.failure.usr.login' => 'hey') end end @@ -308,16 +304,14 @@ end end - if PlatformHelpers.mri? && PlatformHelpers.engine_version >= Gem::Version.new('2.7') - it 'sets additional user login data from other string keys as tags' do - trace_op.measure('root') do |span, _| - expect { described_class.track_signup(trace_op, user: { id: '42' }, 'usr.login' => 'hey') } - .to change { span.tags }.to include( - 'usr.id' => '42', - 'usr.login' => 'hey', - 'appsec.events.users.signup.usr.login' => 'hey' - ) - end + it 'sets additional user login data from other string keys as tags', ruby: '>= 2.7' do + trace_op.measure('root') do |span, _| + expect { described_class.track_signup(trace_op, user: { id: '42' }, 'usr.login' => 'hey') } + .to change { span.tags }.to include( + 'usr.id' => '42', + 'usr.login' => 'hey', + 'appsec.events.users.signup.usr.login' => 'hey' + ) end end