Skip to content

Commit 2fd8d6b

Browse files
committed
pass in user id for just in time user impersonation override
1 parent ab34ca8 commit 2fd8d6b

File tree

7 files changed

+151
-1
lines changed

7 files changed

+151
-1
lines changed

Gemfile.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ PATH
22
remote: .
33
specs:
44
zendesk_api (3.1.1)
5+
base64
56
faraday (> 2.0.0)
67
faraday-multipart
78
hashie (>= 3.5.2)

lib/zendesk_api/client.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
require 'zendesk_api/middleware/request/raise_rate_limited'
1010
require 'zendesk_api/middleware/request/upload'
1111
require 'zendesk_api/middleware/request/encode_json'
12+
require 'zendesk_api/middleware/request/api_token_impersonate'
1213
require 'zendesk_api/middleware/request/url_based_access_token'
1314
require 'zendesk_api/middleware/response/callback'
1415
require 'zendesk_api/middleware/response/deflate'
@@ -104,6 +105,25 @@ def initialize
104105
add_warning_callback
105106
end
106107

108+
# token impersonation for the scope of the block
109+
# @param [String] username The username (email) of the user to impersonate
110+
# @yield The block to run while impersonating the user
111+
# @example
112+
# client.api_token_impersonate("[email protected]") do
113+
# client.tickets.create(:subject => "Help!")
114+
# end
115+
#
116+
# # creates a ticket on behalf of otheruser
117+
# @return
118+
# yielded value
119+
def api_token_impersonate(username)
120+
avant = Thread.current[:zendesk_thread_local_username]
121+
Thread.current[:zendesk_thread_local_username] = username
122+
yield
123+
ensure
124+
Thread.current[:zendesk_thread_local_username] = avant
125+
end
126+
107127
# Creates a connection if there is none, otherwise returns the existing connection.
108128
#
109129
# @return [Faraday::Connection] Faraday connection for the client
@@ -180,6 +200,7 @@ def build_connection
180200
end
181201

182202
builder.adapter(*adapter, &config.adapter_proc)
203+
builder.use ZendeskAPI::Middleware::Request::ApiTokenImpersonate
183204
end
184205
end
185206

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
require 'base64'
2+
module ZendeskAPI
3+
# @private
4+
module Middleware
5+
# @private
6+
module Request
7+
# ApiTokenImpersonate
8+
# If Thread.current[:zendesk_thread_local_username] is set, it will modify the Authorization header
9+
# to impersonate that user using the API token from the current Authorization header.
10+
class ApiTokenImpersonate < Faraday::Middleware
11+
def call(env)
12+
if Thread.current[:zendesk_thread_local_username] && env[:request_headers][:authorization] =~ /^Basic /
13+
current_u_p_encoded = env[:request_headers][:authorization].split(/\s+/)[1]
14+
current_u_p = Base64.urlsafe_decode64(current_u_p_encoded)
15+
unless current_u_p.include?("/token:") && (parts = current_u_p.split(":")) && parts.length == 2 && parts[0].include?("/token")
16+
warn "WARNING: ApiTokenImpersonate passed in invalid format. It should be in the format username/token:APITOKEN"
17+
return @app.call(env)
18+
end
19+
20+
next_u_p = "#{Thread.current[:zendesk_thread_local_username]}/token:#{parts[1]}"
21+
env[:request_headers][:authorization] = "Basic #{Base64.urlsafe_encode64(next_u_p)}"
22+
end
23+
@app.call(env)
24+
end
25+
end
26+
end
27+
end
28+
end

spec/core/client_spec.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,4 +360,40 @@ def url.to_str
360360
expect(client.greeting_categories.path).to match(/channels\/voice\/greeting_categories/)
361361
end
362362
end
363+
364+
context "#api_token_impersonate" do
365+
let(:impersonated_username) { "[email protected]" }
366+
let(:api_token) { "abc123" }
367+
let(:client) do
368+
ZendeskAPI::Client.new do |config|
369+
config.url = "https://example.zendesk.com/api/v2"
370+
config.username = "[email protected]"
371+
config.token = api_token
372+
config.adapter = :test
373+
config.adapter_proc = proc do |stub|
374+
stub.get "/api/v2/tickets" do |env|
375+
[200, { 'content-type': "application/json", Authorization: env.request_headers["Authorization"] }, "null"]
376+
end
377+
end
378+
end
379+
end
380+
381+
it "impersonates the user for the scope of the block" do
382+
result = nil
383+
client.api_token_impersonate(impersonated_username) do
384+
response = client.connection.get("/api/v2/tickets")
385+
auth_header = response.env.request_headers["Authorization"]
386+
decoded = Base64.urlsafe_decode64(auth_header.split.last)
387+
expect(decoded).to start_with("#{impersonated_username}/token:")
388+
result = response
389+
end
390+
expect(result).not_to be_nil
391+
end
392+
393+
it "restores the previous username after the block" do
394+
original = Thread.current[:zendesk_thread_local_username]
395+
client.api_token_impersonate(impersonated_username) { 1 }
396+
expect(Thread.current[:zendesk_thread_local_username]).to eq(original)
397+
end
398+
end
363399
end
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
require 'core/spec_helper'
2+
3+
RSpec.describe ZendeskAPI::Middleware::Request::ApiTokenImpersonate do
4+
let(:app) { ->(env) { env } }
5+
let(:middleware) { described_class.new(app) }
6+
let(:username) { 'impersonated_user' }
7+
let(:token) { 'abc123' }
8+
let(:original_username) { 'original_user/token' }
9+
let(:encoded_auth) { Base64.urlsafe_encode64("#{original_username}:#{token}") }
10+
let(:env) do
11+
{
12+
request_headers: {
13+
authorization: "Basic #{encoded_auth}"
14+
}
15+
}
16+
end
17+
18+
after { Thread.current[:zendesk_thread_local_username] = nil }
19+
20+
context 'when local_username is set and authorization is a valid API token' do
21+
it 'impersonates the user by modifying the Authorization header' do
22+
Thread.current[:zendesk_thread_local_username] = username
23+
result = middleware.call(env)
24+
new_auth = result[:request_headers][:authorization]
25+
decoded = Base64.urlsafe_decode64(new_auth.split.last)
26+
expect(decoded).to eq("#{username}/token:#{token}")
27+
end
28+
end
29+
30+
context 'when local_username is not set' do
31+
it 'does not modify the Authorization header' do
32+
result = middleware.call(env)
33+
expect(result[:request_headers][:authorization]).to eq(env[:request_headers][:authorization])
34+
end
35+
end
36+
37+
context 'when authorization header is not Basic' do
38+
it 'does not modify the Authorization header' do
39+
Thread.current[:zendesk_thread_local_username] = username
40+
env[:request_headers][:authorization] = 'Bearer something'
41+
result = middleware.call(env)
42+
expect(result[:request_headers][:authorization]).to eq('Bearer something')
43+
end
44+
end
45+
46+
context 'when authorization does not contain /token:' do
47+
it 'raises an error' do
48+
Thread.current[:zendesk_thread_local_username] = username
49+
env[:request_headers][:authorization] = "Basic #{Base64.urlsafe_encode64('user:abc123')}"
50+
result = middleware.call(env)
51+
expect(result[:request_headers][:authorization]).to eq("Basic #{Base64.urlsafe_encode64('user:abc123')}")
52+
end
53+
end
54+
55+
context 'when authorization is not in valid format' do
56+
it 'raises an error' do
57+
Thread.current[:zendesk_thread_local_username] = username
58+
env[:request_headers][:authorization] = "Basic #{Base64.urlsafe_encode64('user/token:abc123:extra')}"
59+
result = middleware.call(env)
60+
expect(result[:request_headers][:authorization]).to eq("Basic #{Base64.urlsafe_encode64('user/token:abc123:extra')}")
61+
end
62+
end
63+
end

spec/core/middleware/request/retry_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def runtime
1717
expect(client.connection.get("blergh").status).to eq(200)
1818
}
1919

20-
expect(seconds).to be_within(0.2).of(1)
20+
expect(seconds).to be_within(0.3).of(1)
2121
end
2222
end
2323

zendesk_api.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ Gem::Specification.new do |s|
3333
s.add_dependency "inflection"
3434
s.add_dependency "multipart-post", "~> 2.0"
3535
s.add_dependency "mini_mime"
36+
s.add_dependency "base64"
3637
end

0 commit comments

Comments
 (0)