Skip to content

Commit 32e424b

Browse files
grishacolinmarc
authored andcommitted
Add SASL GSSAPI (Kerberos) support
1 parent f8a964c commit 32e424b

File tree

4 files changed

+109
-52
lines changed

4 files changed

+109
-52
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,17 @@ Connecting with a specific Hive version (0.12) and using the `:http` transport:
117117
connection.fetch('SHOW TABLES')
118118
end
119119

120-
We have not tested the SASL connection, as we don't run SASL; pull requests and testing are welcomed.
120+
Connecting with SASL and Kerberos v5:
121+
122+
RBHive.tcli_connect('hive.hadoop.forward.co.uk', 10_000, {
123+
:transport => :sasl,
124+
:sasl_params => {
125+
:mechanism => 'GSSAPI',
126+
:remote_host => 'example.com',
127+
:remote_principal => 'hive/[email protected]'
128+
) do |connection|
129+
connection.fetch("show tables")
130+
end
121131

122132
#### Hiveserver2 protocol versions
123133

lib/rbhive/t_c_l_i_connection.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,9 @@ def method_missing(meth, *args)
375375
private
376376

377377
def prepare_open_session(client_protocol)
378-
req = ::Hive2::Thrift::TOpenSessionReq.new( @options[:sasl_params].nil? ? [] : @options[:sasl_params] )
378+
req = ::Hive2::Thrift::TOpenSessionReq.new( @options[:sasl_params].nil? ? [] : {
379+
:username => @options[:sasl_params][:username],
380+
:password => @options[:sasl_params][:password]})
379381
req.client_protocol = client_protocol
380382
req
381383
end

lib/thrift/sasl_client_transport.rb

Lines changed: 94 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
module Thrift
2-
class SaslClientTransport < BufferedTransport
3-
attr_reader :challenge
1+
require 'gssapi'
42

3+
module Thrift
4+
class SaslClientTransport < FramedTransport
55
STATUS_BYTES = 1
66
PAYLOAD_LENGTH_BYTES = 4
7-
AUTH_MECHANISM = 'PLAIN'
87
NEGOTIATION_STATUS = {
98
START: 0x01,
109
OK: 0x02,
@@ -15,76 +14,122 @@ class SaslClientTransport < BufferedTransport
1514

1615
def initialize(transport, sasl_params={})
1716
super(transport)
18-
@challenge = nil
17+
@sasl_complete = nil
1918
@sasl_username = sasl_params.fetch(:username, 'anonymous')
2019
@sasl_password = sasl_params.fetch(:password, 'anonymous')
20+
@sasl_mechanism = sasl_params.fetch(:mechanism, 'PLAIN')
21+
22+
unless ['PLAIN', 'GSSAPI'].include? @sasl_mechanism
23+
raise "Unknown SASL mechanism: #{@sasl_mechanism}"
24+
end
25+
26+
if @sasl_mechanism == 'GSSAPI'
27+
@sasl_remote_principal = sasl_params[:remote_principal]
28+
@sasl_remote_host = sasl_params[:remote_host]
29+
@gsscli = GSSAPI::Simple.new(@sasl_remote_host, @sasl_remote_principal)
30+
end
2131
end
2232

2333
def read(sz)
24-
len, = @transport.read(PAYLOAD_LENGTH_BYTES).unpack('l>') if @rbuf.nil?
25-
sz = len if len && sz > len
26-
@index += sz
27-
ret = @rbuf.slice(@index - sz, sz) || Bytes.empty_byte_buffer
28-
if ret.length == 0
29-
@rbuf = @transport.read(len) rescue Bytes.empty_byte_buffer
30-
@index = sz
31-
ret = @rbuf.slice(0, sz) || Bytes.empty_byte_buffer
32-
end
33-
ret
34+
handshake! unless @sasl_complete
35+
super(sz)
3436
end
3537

3638
def read_byte
37-
reset_buffer! if @index >= @rbuf.size
38-
@index += 1
39-
Bytes.get_string_byte(@rbuf, @index - 1)
39+
handshake! unless @sasl_complete
40+
super
4041
end
4142

42-
def read_into_buffer(buffer, size)
43-
i = 0
44-
while i < size
45-
reset_buffer! if @index >= @rbuf.size
46-
byte = Bytes.get_string_byte(@rbuf, @index)
47-
Bytes.set_string_byte(buffer, i, byte)
48-
@index += 1
49-
i += 1
50-
end
51-
i
43+
def read_into_buffer(buf, size)
44+
handshake! unless @sasl_complete
45+
super(buf, size)
5246
end
5347

5448
def write(buf)
55-
initiate_hand_shake if @challenge.nil?
56-
header = [buf.length].pack('l>')
57-
@wbuf << (header + Bytes.force_binary_encoding(buf))
49+
handshake! unless @sasl_complete
50+
super(buf)
5851
end
5952

60-
protected
53+
def flush
54+
handshake! unless @sasl_complete
55+
super
56+
end
6157

62-
def initiate_hand_shake
63-
header = [NEGOTIATION_STATUS[:START], AUTH_MECHANISM.length].pack('cl>')
64-
@transport.write header + AUTH_MECHANISM
65-
message = "[#{AUTH_MECHANISM}]\u0000#{@sasl_username}\u0000#{@sasl_password}"
66-
header = [NEGOTIATION_STATUS[:OK], message.length].pack('cl>')
67-
@transport.write header + message
68-
status, len = @transport.read(STATUS_BYTES + PAYLOAD_LENGTH_BYTES).unpack('cl>')
58+
private
59+
60+
def handshake!
61+
case @sasl_mechanism
62+
when 'PLAIN'
63+
handshake_plain!
64+
when 'GSSAPI'
65+
handshake_gssapi!
66+
end
67+
end
68+
69+
def handshake_plain!
70+
token = "[#{@sasl_mechanism}]\u0000#{@sasl_username}\u0000#{@sasl_password}"
71+
write_handshake_message(NEGOTIATION_STATUS[:START], @sasl_mechanism)
72+
write_handshake_message(NEGOTIATION_STATUS[:OK], token)
73+
74+
status, msg = read_handshake_message
6975
case status
70-
when NEGOTIATION_STATUS[:BAD], NEGOTIATION_STATUS[:ERROR]
71-
raise @transport.to_io.read(len)
7276
when NEGOTIATION_STATUS[:COMPLETE]
73-
@challenge = @transport.to_io.read len
77+
@sasl_complete = true
7478
when NEGOTIATION_STATUS[:OK]
7579
raise "Failed to complete challenge exchange: only NONE supported currently"
7680
end
7781
end
7882

79-
private
83+
def handshake_gssapi!
84+
token = @gsscli.init_context
85+
write_handshake_message(NEGOTIATION_STATUS[:START], @sasl_mechanism)
86+
write_handshake_message(NEGOTIATION_STATUS[:OK], token)
87+
88+
status, msg = read_handshake_message
89+
case status
90+
when NEGOTIATION_STATUS[:COMPLETE]
91+
raise "Unexpected COMPLETE from server"
92+
when NEGOTIATION_STATUS[:OK]
93+
unless @gsscli.init_context(msg)
94+
raise "GSSAPI: challenge provided by server could not be verified"
95+
end
96+
97+
write_handshake_message(NEGOTIATION_STATUS[:OK], "")
98+
99+
status, msg = read_handshake_message
100+
case status
101+
when NEGOTIATION_STATUS[:COMPLETE]
102+
raise "Unexpected COMPLETE from server"
103+
when NEGOTIATION_STATUS[:OK]
104+
unwrapped = @gsscli.unwrap_message(msg)
105+
rewrapped = @gsscli.wrap_message(unwrapped)
106+
107+
write_handshake_message(NEGOTIATION_STATUS[:COMPLETE], rewrapped)
80108

81-
def reset_buffer!
82-
len, = @transport.read(PAYLOAD_LENGTH_BYTES).unpack('l>')
83-
@rbuf = @transport.read(len)
84-
while @rbuf.size < len
85-
@rbuf << @transport.read(len - @rbuf.size)
109+
status, msg = read_handshake_message
110+
case status
111+
when NEGOTIATION_STATUS[:COMPLETE]
112+
@sasl_complete = true
113+
when NEGOTIATION_STATUS[:OK]
114+
raise "Failed to complete GSS challenge exchange"
115+
end
116+
end
86117
end
87-
@index = 0
118+
end
119+
120+
def read_handshake_message
121+
status, len = @transport.read(STATUS_BYTES + PAYLOAD_LENGTH_BYTES).unpack('cl>')
122+
body = @transport.to_io.read(len)
123+
if [NEGOTIATION_STATUS[:BAD], NEGOTIATION_STATUS[:ERROR]].include?(status)
124+
raise "Exception from server: #{body}"
125+
end
126+
127+
[status, body]
128+
end
129+
130+
def write_handshake_message(status, message)
131+
header = [status, message.length].pack('cl>')
132+
@transport.write(header + message)
88133
end
89134
end
90135

@@ -93,5 +138,4 @@ def get_transport(transport)
93138
return SaslClientTransport.new(transport)
94139
end
95140
end
96-
97141
end

rbhive.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Gem::Specification.new do |spec|
1919
spec.require_paths = ['lib']
2020

2121
spec.add_dependency('thrift', '~> 0.9')
22+
spec.add_dependency('gssapi', '~> 1.2')
2223
spec.add_dependency('json')
2324

2425
spec.add_development_dependency 'rake'

0 commit comments

Comments
 (0)