Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ PATH
remote: .
specs:
zendesk_api (3.1.1)
base64
faraday (> 2.0.0)
faraday-multipart
hashie (>= 3.5.2)
Expand Down
21 changes: 21 additions & 0 deletions lib/zendesk_api/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
require 'zendesk_api/middleware/request/raise_rate_limited'
require 'zendesk_api/middleware/request/upload'
require 'zendesk_api/middleware/request/encode_json'
require 'zendesk_api/middleware/request/api_token_impersonate'
require 'zendesk_api/middleware/request/url_based_access_token'
require 'zendesk_api/middleware/response/callback'
require 'zendesk_api/middleware/response/deflate'
Expand Down Expand Up @@ -104,6 +105,25 @@ def initialize
add_warning_callback
end

# token impersonation for the scope of the block
# @param [String] username The username (email) of the user to impersonate
# @yield The block to run while impersonating the user
# @example
# client.api_token_impersonate("[email protected]") do
# client.tickets.create(:subject => "Help!")
# end
#
# # creates a ticket on behalf of otheruser
# @return
# yielded value
def api_token_impersonate(username)
avant = Thread.current[:zendesk_thread_local_username]
Thread.current[:zendesk_thread_local_username] = username
yield
ensure
Thread.current[:zendesk_thread_local_username] = avant
end

# Creates a connection if there is none, otherwise returns the existing connection.
#
# @return [Faraday::Connection] Faraday connection for the client
Expand Down Expand Up @@ -180,6 +200,7 @@ def build_connection
end

builder.adapter(*adapter, &config.adapter_proc)
builder.use ZendeskAPI::Middleware::Request::ApiTokenImpersonate
end
end

Expand Down
28 changes: 28 additions & 0 deletions lib/zendesk_api/middleware/request/api_token_impersonate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require 'base64'
module ZendeskAPI
# @private
module Middleware
# @private
module Request
# ApiTokenImpersonate
# If Thread.current[:zendesk_thread_local_username] is set, it will modify the Authorization header
# to impersonate that user using the API token from the current Authorization header.
class ApiTokenImpersonate < Faraday::Middleware
def call(env)
if Thread.current[:zendesk_thread_local_username] && env[:request_headers][:authorization] =~ /^Basic /
current_u_p_encoded = env[:request_headers][:authorization].split(/\s+/)[1]
current_u_p = Base64.urlsafe_decode64(current_u_p_encoded)
unless current_u_p.include?("/token:") && (parts = current_u_p.split(":")) && parts.length == 2 && parts[0].include?("/token")
warn "WARNING: ApiTokenImpersonate passed in invalid format. It should be in the format username/token:APITOKEN"
return @app.call(env)
end

next_u_p = "#{Thread.current[:zendesk_thread_local_username]}/token:#{parts[1]}"
env[:request_headers][:authorization] = "Basic #{Base64.urlsafe_encode64(next_u_p)}"
end
@app.call(env)
end
end
end
end
end
36 changes: 36 additions & 0 deletions spec/core/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -360,4 +360,40 @@ def url.to_str
expect(client.greeting_categories.path).to match(/channels\/voice\/greeting_categories/)
end
end

context "#api_token_impersonate" do
let(:impersonated_username) { "[email protected]" }
let(:api_token) { "abc123" }
let(:client) do
ZendeskAPI::Client.new do |config|
config.url = "https://example.zendesk.com/api/v2"
config.username = "[email protected]"
config.token = api_token
config.adapter = :test
config.adapter_proc = proc do |stub|
stub.get "/api/v2/tickets" do |env|
[200, { 'content-type': "application/json", Authorization: env.request_headers["Authorization"] }, "null"]
end
end
end
end

it "impersonates the user for the scope of the block" do
result = nil
client.api_token_impersonate(impersonated_username) do
response = client.connection.get("/api/v2/tickets")
auth_header = response.env.request_headers["Authorization"]
decoded = Base64.urlsafe_decode64(auth_header.split.last)
expect(decoded).to start_with("#{impersonated_username}/token:")
result = response
end
expect(result).not_to be_nil
end

it "restores the previous username after the block" do
original = Thread.current[:zendesk_thread_local_username]
client.api_token_impersonate(impersonated_username) { 1 }
expect(Thread.current[:zendesk_thread_local_username]).to eq(original)
end
end
end
63 changes: 63 additions & 0 deletions spec/core/middleware/request/api_token_impersonate_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
require 'core/spec_helper'

RSpec.describe ZendeskAPI::Middleware::Request::ApiTokenImpersonate do
let(:app) { ->(env) { env } }
let(:middleware) { described_class.new(app) }
let(:username) { 'impersonated_user' }
let(:token) { 'abc123' }
let(:original_username) { 'original_user/token' }
let(:encoded_auth) { Base64.urlsafe_encode64("#{original_username}:#{token}") }
let(:env) do
{
request_headers: {
authorization: "Basic #{encoded_auth}"
}
}
end

after { Thread.current[:zendesk_thread_local_username] = nil }

context 'when local_username is set and authorization is a valid API token' do
it 'impersonates the user by modifying the Authorization header' do
Thread.current[:zendesk_thread_local_username] = username
result = middleware.call(env)
new_auth = result[:request_headers][:authorization]
decoded = Base64.urlsafe_decode64(new_auth.split.last)
expect(decoded).to eq("#{username}/token:#{token}")
end
end

context 'when local_username is not set' do
it 'does not modify the Authorization header' do
result = middleware.call(env)
expect(result[:request_headers][:authorization]).to eq(env[:request_headers][:authorization])
end
end

context 'when authorization header is not Basic' do
it 'does not modify the Authorization header' do
Thread.current[:zendesk_thread_local_username] = username
env[:request_headers][:authorization] = 'Bearer something'
result = middleware.call(env)
expect(result[:request_headers][:authorization]).to eq('Bearer something')
end
end

context 'when authorization does not contain /token:' do
it 'raises an error' do
Thread.current[:zendesk_thread_local_username] = username
env[:request_headers][:authorization] = "Basic #{Base64.urlsafe_encode64('user:abc123')}"
result = middleware.call(env)
expect(result[:request_headers][:authorization]).to eq("Basic #{Base64.urlsafe_encode64('user:abc123')}")
end
end

context 'when authorization is not in valid format' do
it 'raises an error' do
Thread.current[:zendesk_thread_local_username] = username
env[:request_headers][:authorization] = "Basic #{Base64.urlsafe_encode64('user/token:abc123:extra')}"
result = middleware.call(env)
expect(result[:request_headers][:authorization]).to eq("Basic #{Base64.urlsafe_encode64('user/token:abc123:extra')}")
end
end
end
2 changes: 1 addition & 1 deletion spec/core/middleware/request/retry_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def runtime
expect(client.connection.get("blergh").status).to eq(200)
}

expect(seconds).to be_within(0.2).of(1)
expect(seconds).to be_within(0.3).of(1)
end
end

Expand Down
1 change: 1 addition & 0 deletions zendesk_api.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ Gem::Specification.new do |s|
s.add_dependency "inflection"
s.add_dependency "multipart-post", "~> 2.0"
s.add_dependency "mini_mime"
s.add_dependency "base64"
end