Skip to content

Commit d2a9bda

Browse files
authored
Merge pull request #4468 from DataDog/appsec-56960-add-collection-of-rack-request-headers-to-identity
[APPSEC-56960] Add collection of request headers for identity.set_user
2 parents 1035971 + f5cff78 commit d2a9bda

File tree

5 files changed

+157
-1
lines changed

5 files changed

+157
-1
lines changed

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

+20
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,26 @@ module Contrib
66
module Rack
77
# Rack integration constants
88
module Ext
9+
IDENTITY_COLLECTABLE_REQUEST_HEADERS = [
10+
'accept-encoding',
11+
'accept-language',
12+
'cf-connecting-ip',
13+
'cf-connecting-ipv6',
14+
'content-encoding',
15+
'content-language',
16+
'content-length',
17+
'fastly-client-ip',
18+
'forwarded',
19+
'forwarded-for',
20+
'host',
21+
'true-client-ip',
22+
'via',
23+
'x-client-ip',
24+
'x-cluster-client-ip',
25+
'x-forwarded',
26+
'x-forwarded-for',
27+
'x-real-ip'
28+
].freeze
929
end
1030
end
1131
end

lib/datadog/appsec/contrib/rack/gateway/watcher.rb

+20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require_relative '../ext'
34
require_relative '../../../instrumentation/gateway'
45
require_relative '../../../event'
56

@@ -110,6 +111,25 @@ def watch_request_body(gateway = Instrumentation.gateway)
110111
stack.call(gateway_request.request)
111112
end
112113
end
114+
115+
# NOTE: In the current state we unable to substibe twice to the same
116+
# event within the same group. Ideally this code should live
117+
# somewhere closer to identity related monitor.
118+
# WARNING: The Gateway is a subject of refactoring
119+
def watch_request_finish(gateway = Instrumentation.gateway)
120+
gateway.watch('rack.request.finish', :appsec) do |stack, gateway_request|
121+
context = gateway_request.env[AppSec::Ext::CONTEXT_KEY]
122+
next stack.call(gateway_request.request) if context.span.nil? || !gateway.pushed?('identity.set_user')
123+
124+
gateway_request.headers.each do |name, value|
125+
next unless Ext::IDENTITY_COLLECTABLE_REQUEST_HEADERS.include?(name)
126+
127+
context.span["http.request.headers.#{name}"] = value
128+
end
129+
130+
stack.call(gateway_request.request)
131+
end
132+
end
113133
end
114134
end
115135
end

lib/datadog/appsec/contrib/rack/request_middleware.rb

+3
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ def call(env)
7777
gateway_response = nil
7878

7979
interrupt_params = catch(::Datadog::AppSec::Ext::INTERRUPT) do
80+
# TODO: This event should be renamed into `rack.request.start` to
81+
# reflect that it's the beginning of the request-cycle
8082
http_response, _gateway_request = Instrumentation.gateway.push('rack.request', gateway_request) do
8183
@app.call(env)
8284
end
@@ -85,6 +87,7 @@ def call(env)
8587
http_response[2], http_response[0], http_response[1], context: ctx
8688
)
8789

90+
Instrumentation.gateway.push('rack.request.finish', gateway_request)
8891
Instrumentation.gateway.push('rack.response', gateway_response)
8992

9093
nil

sig/datadog/appsec/contrib/rack/ext.rbs

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module Datadog
33
module Contrib
44
module Rack
55
module Ext
6-
APP: untyped
6+
IDENTITY_COLLECTABLE_REQUEST_HEADERS: ::Array[::String]
77
end
88
end
99
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# frozen_string_literal: true
2+
3+
require 'datadog/tracing/contrib/support/spec_helper'
4+
require 'datadog/appsec/spec_helper'
5+
require 'rack/test'
6+
7+
require 'datadog/tracing'
8+
require 'datadog/appsec'
9+
10+
RSpec.describe 'Rack-request headers collection for identity.set_user' do
11+
include Rack::Test::Methods
12+
13+
before do
14+
Datadog.configure do |c|
15+
c.tracing.enabled = true
16+
17+
c.appsec.enabled = true
18+
c.appsec.instrument :rack
19+
20+
c.appsec.standalone.enabled = false
21+
c.appsec.waf_timeout = 10_000_000 # in us
22+
c.appsec.ip_passlist = []
23+
c.appsec.ip_denylist = []
24+
c.appsec.user_id_denylist = []
25+
c.appsec.ruleset = :recommended
26+
c.appsec.api_security.enabled = false
27+
c.appsec.api_security.sample_rate = 0.0
28+
29+
c.remote.enabled = false
30+
end
31+
32+
allow(Datadog::AppSec::Instrumentation).to receive(:gateway).and_return(gateway)
33+
34+
# NOTE: Don't reach the agent in any way
35+
allow_any_instance_of(Datadog::Tracing::Transport::HTTP::Client).to receive(:send_request)
36+
allow_any_instance_of(Datadog::Tracing::Transport::Traces::Transport).to receive(:native_events_supported?)
37+
.and_return(true)
38+
39+
Datadog::AppSec::Contrib::Rack::Gateway::Watcher.watch_request_finish
40+
end
41+
42+
after do
43+
Datadog.configuration.reset!
44+
Datadog.registry[:rack].reset_configuration!
45+
end
46+
47+
let(:gateway) { Datadog::AppSec::Instrumentation::Gateway.new }
48+
49+
let(:http_service_entry_span) do
50+
Datadog::Tracing::Transport::TraceFormatter.format!(trace)
51+
spans.find { |s| s.name == 'rack.request' }
52+
end
53+
54+
let(:app) do
55+
stack = Rack::Builder.new do
56+
use Datadog::Tracing::Contrib::Rack::TraceMiddleware
57+
use Datadog::AppSec::Contrib::Rack::RequestMiddleware
58+
59+
map '/with-identity-set-user' do
60+
run(
61+
lambda do |_env|
62+
Datadog::Kit::Identity.set_user(
63+
Datadog::Tracing.active_trace, Datadog::Tracing.active_span, id: '42'
64+
)
65+
66+
[200, { 'Content-Type' => 'text/html' }, ['OK']]
67+
end
68+
)
69+
end
70+
71+
map '/without-identity-set-user' do
72+
run ->(_env) { [200, { 'Content-Type' => 'text/html' }, ['OK']] }
73+
end
74+
end
75+
76+
stack.to_app
77+
end
78+
79+
subject(:response) { last_response }
80+
81+
context 'when identity.set_user event was pushed' do
82+
before do
83+
headers = {
84+
'HTTP_CF_RAY' => '230b030023ae2822-SJC',
85+
'HTTP_CF_CONNECTING_IPV6' => '2001:db8:3333:4444:5555:6666:1.2.3.4'
86+
}
87+
get('/with-identity-set-user', {}, headers)
88+
end
89+
90+
it 'collects identity related request headers' do
91+
expect(response).to be_ok
92+
93+
expect(http_service_entry_span.tags['http.request.headers.cf-connecting-ipv6'])
94+
.to eq('2001:db8:3333:4444:5555:6666:1.2.3.4')
95+
end
96+
end
97+
98+
context 'when identity.set_user event was not pushed' do
99+
before do
100+
headers = {
101+
'HTTP_CF_RAY' => '230b030023ae2822-SJC',
102+
'HTTP_CF_CONNECTING_IPV6' => '2001:db8:3333:4444:5555:6666:1.2.3.4'
103+
}
104+
get('/without-identity-set-user', {}, headers)
105+
end
106+
107+
it 'does not collect identity related request headers' do
108+
expect(response).to be_ok
109+
110+
expect(http_service_entry_span.tags).not_to have_key('http.request.headers.cf-connecting-ipv6')
111+
end
112+
end
113+
end

0 commit comments

Comments
 (0)