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..6f43b86928d 100644 --- a/spec/datadog/kit/appsec/events_spec.rb +++ b/spec/datadog/kit/appsec/events_spec.rb @@ -93,6 +93,17 @@ end 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 + 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 +192,13 @@ end 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 + 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 +304,17 @@ end 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 + 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') } 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?