Skip to content

Commit 1f5bbc8

Browse files
committed
Add Personal Access Token (PAT) auth support
When a PAT is configured, use Bearer auth exclusively. PAT auth is a separate auth path that replaces API key + App key entirely - send only Authorization: Bearer <PAT> header, no DD-API-KEY or DD-APPLICATION-KEY.
1 parent 3f580d9 commit 1f5bbc8

File tree

4 files changed

+128
-2
lines changed

4 files changed

+128
-2
lines changed

lib/datadog_api_client/api_client.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def calculate_retry_interval(response, backoff_base, backoff_multiplier, attempt
154154
#Redact api and app key in the request header
155155
def sanitize_request_header(request_header)
156156
sanitized_headers= request_header.dup
157-
keys_to_redact = ["DD-API-KEY", "DD-APPLICATION-KEY"]
157+
keys_to_redact = ["DD-API-KEY", "DD-APPLICATION-KEY", "Authorization"]
158158
keys_to_redact.each do |key_to_redact|
159159
if sanitized_headers.key?(key_to_redact)
160160
sanitized_headers[key_to_redact] = "REDACTED"
@@ -377,9 +377,18 @@ def build_request_url(path, opts = {})
377377
# @param [Hash] query_params Query parameters
378378
# @param [String] auth_names Authentication scheme name
379379
def update_params_for_auth!(header_params, query_params, auth_names)
380-
Array(auth_names).each do |auth_name|
380+
auth_names = Array(auth_names)
381+
382+
# When a personal access token is configured, use Bearer auth exclusively.
383+
# PAT auth is a separate auth path that replaces API key + App key entirely.
384+
if @config.personal_access_token
385+
auth_names = [:bearerAuth]
386+
end
387+
388+
auth_names.each do |auth_name|
381389
auth_setting = @config.auth_settings[auth_name]
382390
next unless auth_setting
391+
next if auth_setting[:value].nil? || auth_setting[:value].to_s.empty?
383392
case auth_setting[:in]
384393
when 'header' then header_params[auth_setting[:key]] = auth_setting[:value]
385394
when 'query' then query_params[auth_setting[:key]] = auth_setting[:value]

lib/datadog_api_client/configuration.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ class Configuration
6565
# Defines the access token (Bearer) used with OAuth2.
6666
attr_accessor :access_token
6767

68+
# Defines the personal access token (PAT) used for Bearer authentication.
69+
# When set, sends Authorization: Bearer <token> header in place of DD-APPLICATION-KEY.
70+
attr_accessor :personal_access_token
71+
6872
# Set this to enable/disable debugging. When enabled (set to true), HTTP request/response
6973
# details will be logged with `logger.debug` (see the `logger` attribute).
7074
# Default to false.
@@ -408,6 +412,7 @@ def initialize
408412
@server_variables[:site] = ENV['DD_SITE'] if ENV.key? 'DD_SITE'
409413
@api_key['apiKeyAuth'] = ENV['DD_API_KEY'] if ENV.key? 'DD_API_KEY'
410414
@api_key['appKeyAuth'] = ENV['DD_APP_KEY'] if ENV.key? 'DD_APP_KEY'
415+
@personal_access_token = ENV['DD_PAT'] if ENV.key? 'DD_PAT'
411416

412417
yield(self) if block_given?
413418
end
@@ -503,6 +508,13 @@ def auth_settings
503508
key: 'DD-APPLICATION-KEY',
504509
value: api_key_with_prefix('appKeyAuth')
505510
},
511+
bearerAuth:
512+
{
513+
type: 'http',
514+
in: 'header',
515+
key: 'Authorization',
516+
value: personal_access_token ? "Bearer #{personal_access_token}" : nil
517+
},
506518
}
507519
end
508520

spec/api_client_spec.rb

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,69 @@
206206
expect(api_client.sanitize_filename('.\sun.gif')).to eq('sun.gif')
207207
end
208208
end
209+
210+
describe '#update_params_for_auth!' do
211+
let(:config) { DatadogAPIClient::Configuration.new }
212+
let(:api_client) { DatadogAPIClient::APIClient.new(config) }
213+
214+
context 'when personal access token is configured' do
215+
before do
216+
config.api_key = 'test_api_key'
217+
config.application_key = 'test_app_key'
218+
config.personal_access_token = 'ddpat_test_pat'
219+
end
220+
221+
it 'sends only Bearer Authorization header, no API key or app key' do
222+
header_params = {}
223+
query_params = {}
224+
api_client.update_params_for_auth!(header_params, query_params, [:apiKeyAuth, :appKeyAuth])
225+
expect(header_params['Authorization']).to eq('Bearer ddpat_test_pat')
226+
expect(header_params).not_to have_key('DD-API-KEY')
227+
expect(header_params).not_to have_key('DD-APPLICATION-KEY')
228+
end
229+
230+
it 'sends only Bearer even when bearerAuth is already in auth_names' do
231+
header_params = {}
232+
query_params = {}
233+
api_client.update_params_for_auth!(header_params, query_params, [:apiKeyAuth, :appKeyAuth, :bearerAuth])
234+
expect(header_params['Authorization']).to eq('Bearer ddpat_test_pat')
235+
expect(header_params).not_to have_key('DD-API-KEY')
236+
expect(header_params).not_to have_key('DD-APPLICATION-KEY')
237+
end
238+
end
239+
240+
context 'when personal access token is not configured' do
241+
before do
242+
config.api_key = 'test_api_key'
243+
config.application_key = 'test_app_key'
244+
end
245+
246+
it 'uses API key and app key, no Bearer header' do
247+
header_params = {}
248+
query_params = {}
249+
api_client.update_params_for_auth!(header_params, query_params, [:apiKeyAuth, :appKeyAuth])
250+
expect(header_params['DD-API-KEY']).to eq('test_api_key')
251+
expect(header_params['DD-APPLICATION-KEY']).to eq('test_app_key')
252+
expect(header_params).not_to have_key('Authorization')
253+
end
254+
end
255+
end
256+
257+
describe '#sanitize_request_header' do
258+
let(:api_client) { DatadogAPIClient::APIClient.new }
259+
260+
it 'redacts sensitive headers including Authorization' do
261+
headers = {
262+
'DD-API-KEY' => 'secret_api_key',
263+
'DD-APPLICATION-KEY' => 'secret_app_key',
264+
'Authorization' => 'Bearer ddapp_secret_pat',
265+
'Content-Type' => 'application/json'
266+
}
267+
sanitized = api_client.sanitize_request_header(headers)
268+
expect(sanitized['DD-API-KEY']).to eq('REDACTED')
269+
expect(sanitized['DD-APPLICATION-KEY']).to eq('REDACTED')
270+
expect(sanitized['Authorization']).to eq('REDACTED')
271+
expect(sanitized['Content-Type']).to eq('application/json')
272+
end
273+
end
209274
end

spec/configuration_spec.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,45 @@
5858
end
5959
end
6060

61+
describe '#personal_access_token' do
62+
let(:config) { DatadogAPIClient::Configuration.new }
63+
64+
it 'defaults to nil' do
65+
expect(config.personal_access_token).to be_nil
66+
end
67+
68+
it 'can be set directly' do
69+
config.personal_access_token = 'ddapp_test_token_123'
70+
expect(config.personal_access_token).to eq('ddapp_test_token_123')
71+
end
72+
73+
it 'loads from DD_PAT environment variable' do
74+
allow(ENV).to receive(:key?).and_call_original
75+
allow(ENV).to receive(:key?).with('DD_PAT').and_return(true)
76+
allow(ENV).to receive(:[]).and_call_original
77+
allow(ENV).to receive(:[]).with('DD_PAT').and_return('ddapp_env_token')
78+
new_config = DatadogAPIClient::Configuration.new
79+
expect(new_config.personal_access_token).to eq('ddapp_env_token')
80+
end
81+
end
82+
83+
describe '#auth_settings' do
84+
let(:config) { DatadogAPIClient::Configuration.new }
85+
86+
it 'includes bearerAuth with nil value when PAT not set' do
87+
expect(config.auth_settings[:bearerAuth]).not_to be_nil
88+
expect(config.auth_settings[:bearerAuth][:value]).to be_nil
89+
end
90+
91+
it 'includes bearerAuth with Bearer token when PAT is set' do
92+
config.personal_access_token = 'ddapp_my_token'
93+
auth = config.auth_settings[:bearerAuth]
94+
expect(auth[:type]).to eq('http')
95+
expect(auth[:in]).to eq('header')
96+
expect(auth[:key]).to eq('Authorization')
97+
expect(auth[:value]).to eq('Bearer ddapp_my_token')
98+
end
99+
end
100+
61101

62102
end

0 commit comments

Comments
 (0)