Skip to content

Commit 31c7c87

Browse files
committed
Merge pull request #459 from estolfo/RUBY-530-gssapi-mri
RUBY-530 sasl gssapi support for MRI
2 parents 21d84dd + 9bf6131 commit 31c7c87

File tree

10 files changed

+303
-37
lines changed

10 files changed

+303
-37
lines changed

ext/csasl/csasl.c

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Copyright (C) 2014 MongoDB, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include <ruby.h>
16+
#include <sasl/sasl.h>
17+
#include <sasl/saslutil.h>
18+
19+
static void mongo_sasl_conn_free(void* data) {
20+
sasl_conn_t *conn = (sasl_conn_t*) data;
21+
// Ideally we would use sasl_client_done() but that's only available as of cyrus sasl 2.1.25
22+
if(conn) sasl_done();
23+
}
24+
25+
static sasl_conn_t* mongo_sasl_context(VALUE self) {
26+
sasl_conn_t* conn;
27+
VALUE context;
28+
context = rb_iv_get(self, "@context");
29+
Data_Get_Struct(context, sasl_conn_t, conn);
30+
return conn;
31+
}
32+
33+
static VALUE a_init(VALUE self, VALUE user_name, VALUE host_name, VALUE service_name, VALUE canonicalize_host_name)
34+
{
35+
if (sasl_client_init(NULL) == SASL_OK) {
36+
rb_iv_set(self, "@valid", Qtrue);
37+
rb_iv_set(self, "@user_name", user_name);
38+
rb_iv_set(self, "@host_name", host_name);
39+
rb_iv_set(self, "@service_name", service_name);
40+
rb_iv_set(self, "@canonicalize_host_name", canonicalize_host_name);
41+
}
42+
43+
else {
44+
rb_iv_set(self, "@valid", Qfalse);
45+
}
46+
47+
return self;
48+
}
49+
50+
static VALUE valid(VALUE self) {
51+
return rb_iv_get(self, "@valid");
52+
}
53+
54+
int is_sasl_failure(int result)
55+
{
56+
if (result < 0) {
57+
return 1;
58+
}
59+
60+
return 0;
61+
}
62+
63+
static int sasl_interact(VALUE self, int id, const char **result, unsigned *len) {
64+
switch (id) {
65+
case SASL_CB_AUTHNAME:
66+
case SASL_CB_USER:
67+
{
68+
VALUE user_name;
69+
user_name = rb_iv_get(self, "@user_name");
70+
*result = RSTRING_PTR(user_name);
71+
if (len) {
72+
*len = RSTRING_LEN(user_name);
73+
}
74+
return SASL_OK;
75+
}
76+
}
77+
78+
return SASL_FAIL;
79+
}
80+
81+
static VALUE initialize_challenge(VALUE self) {
82+
int result;
83+
char encoded_payload[4096];
84+
const char *raw_payload;
85+
unsigned int raw_payload_len, encoded_payload_len;
86+
const char *mechanism_list = "GSSAPI";
87+
const char *mechanism_selected = "GSSAPI";
88+
VALUE context;
89+
sasl_conn_t *conn;
90+
sasl_callback_t client_interact [] = {
91+
{ SASL_CB_AUTHNAME, (int (*)(void))sasl_interact, (void*)self },
92+
{ SASL_CB_USER, (int (*)(void))sasl_interact, (void*)self },
93+
{ SASL_CB_LIST_END, NULL, NULL }
94+
};
95+
96+
const char *servicename = RSTRING_PTR(rb_iv_get(self, "@service_name"));
97+
const char *hostname = RSTRING_PTR(rb_iv_get(self, "@host_name"));
98+
99+
result = sasl_client_new(servicename, hostname, NULL, NULL, client_interact, 0, &conn);
100+
101+
if (result != SASL_OK) {
102+
sasl_dispose(&conn);
103+
return Qfalse;
104+
}
105+
106+
context = Data_Wrap_Struct(rb_cObject, NULL, mongo_sasl_conn_free, conn);
107+
rb_iv_set(self, "@context", context);
108+
109+
result = sasl_client_start(conn, mechanism_list, NULL, &raw_payload, &raw_payload_len, &mechanism_selected);
110+
if (is_sasl_failure(result)) {
111+
return Qfalse;
112+
}
113+
114+
if (result != SASL_CONTINUE) {
115+
return Qfalse;
116+
}
117+
118+
result = sasl_encode64(raw_payload, raw_payload_len, encoded_payload, sizeof(encoded_payload), &encoded_payload_len);
119+
if (is_sasl_failure(result)) {
120+
return Qfalse;
121+
}
122+
123+
encoded_payload[encoded_payload_len] = 0;
124+
return rb_str_new(encoded_payload, encoded_payload_len);
125+
}
126+
127+
static VALUE evaluate_challenge(VALUE self, VALUE rb_payload) {
128+
char base_payload[4096], payload[4096];
129+
const char *step_payload, *out;
130+
unsigned int step_payload_len, payload_len, base_payload_len, outlen;
131+
int result;
132+
sasl_conn_t *conn = mongo_sasl_context(self);
133+
134+
StringValue(rb_payload);
135+
step_payload = RSTRING_PTR(rb_payload);
136+
step_payload_len = RSTRING_LEN(rb_payload);
137+
138+
result = sasl_decode64(step_payload, step_payload_len, base_payload, sizeof(base_payload), &base_payload_len);
139+
if (is_sasl_failure(result)) {
140+
return Qfalse;
141+
}
142+
143+
result = sasl_client_step(conn, base_payload, base_payload_len, NULL, &out, &outlen);
144+
if (is_sasl_failure(result)) {
145+
return Qfalse;
146+
}
147+
148+
result = sasl_encode64(out, outlen, payload, sizeof(payload), &payload_len);
149+
if (is_sasl_failure(result)) {
150+
return Qfalse;
151+
}
152+
153+
return rb_str_new(payload, payload_len);
154+
}
155+
156+
VALUE c_GSSAPI_authenticator;
157+
158+
void Init_csasl() {
159+
VALUE mongo, sasl;
160+
mongo = rb_const_get(rb_cObject, rb_intern("Mongo"));
161+
sasl = rb_const_get(mongo, rb_intern("Sasl"));
162+
c_GSSAPI_authenticator = rb_define_class_under(sasl, "GSSAPIAuthenticator", rb_cObject);
163+
rb_define_method(c_GSSAPI_authenticator, "initialize", a_init, 4);
164+
rb_define_method(c_GSSAPI_authenticator, "initialize_challenge", initialize_challenge, 0);
165+
rb_define_method(c_GSSAPI_authenticator, "evaluate_challenge", evaluate_challenge, 1);
166+
rb_define_method(rb_cObject, "valid?", valid, 0);
167+
}

ext/csasl/extconf.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
require 'mkmf'
2+
find_header('sasl/sasl.h')
3+
have_library('sasl2', 'sasl_version')
4+
5+
create_makefile('csasl/csasl')

lib/mongo/functional.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,14 @@
1818
require 'mongo/functional/write_concern'
1919
require 'mongo/functional/uri_parser'
2020

21-
require 'mongo/functional/sasl_java' if RUBY_PLATFORM =~ /java/
21+
begin
22+
if RUBY_PLATFORM =~ /java/
23+
require 'mongo/functional/sasl_java'
24+
else
25+
require 'mongo/functional/sasl_c'
26+
require "csasl/csasl"
27+
end
28+
Mongo::HAS_SASL = true
29+
rescue LoadError
30+
Mongo::HAS_SASL = false
31+
end

lib/mongo/functional/authentication.rb

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,6 @@ def issue_plain(auth, opts={})
284284
#
285285
# @private
286286
def issue_gssapi(auth, opts={})
287-
raise NotImplementedError,
288-
"The #{auth[:mechanism]} authentication mechanism is only supported " +
289-
"for JRuby." unless RUBY_PLATFORM =~ /java/
290287
Mongo::Sasl::GSSAPI.authenticate(auth[:username], self, opts[:socket], auth[:extra] || {})
291288
end
292289

lib/mongo/functional/sasl_c.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Copyright (C) 2009-2014 MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
module Mongo
16+
module Sasl
17+
18+
module GSSAPI
19+
20+
def self.authenticate(username, client, socket, opts={})
21+
raise LoadError,
22+
"The Sasl GSSAPI authentication mechanism cannot be used because " +
23+
"its extension did not load properly" unless Mongo::HAS_SASL
24+
25+
db = client.db('$external')
26+
hostname = socket.pool.host
27+
servicename = opts[:gssapi_service_name] || 'mongodb'
28+
canonicalize = opts[:canonicalize_host_name] ? opts[:canonicalize_host_name] : false
29+
authenticator = Mongo::Sasl::GSSAPIAuthenticator.new(username, hostname, servicename, canonicalize)
30+
31+
return { } unless authenticator.valid?
32+
33+
token = authenticator.initialize_challenge
34+
cmd = BSON::OrderedHash['saslStart', 1, 'mechanism', 'GSSAPI', 'payload', token, 'autoAuthorize', 1]
35+
response = db.command(cmd, :check_response => false, :socket => socket)
36+
37+
until response['done'] do
38+
break unless Support.ok?(response)
39+
token = authenticator.evaluate_challenge(response['payload'])
40+
cmd = BSON::OrderedHash['saslContinue', 1, 'conversationId', response['conversationId'], 'payload', token]
41+
response = db.command(cmd, :check_response => false, :socket => socket)
42+
end
43+
response
44+
end
45+
end
46+
end
47+
end

lib/mongo/functional/sasl_java.rb

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) 2009-2013 MongoDB, Inc.
1+
# Copyright (C) 2009-2014 MongoDB, Inc.
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
@@ -25,22 +25,30 @@ module Sasl
2525
module GSSAPI
2626

2727
def self.authenticate(username, client, socket, opts={})
28+
raise LoadError,
29+
"The Sasl GSSAPI authentication mechanism cannot be used because " +
30+
"its extension did not load properly" unless Mongo::HAS_SASL
2831
db = client.db('$external')
2932
hostname = socket.pool.host
3033
servicename = opts[:gssapi_service_name] || 'mongodb'
3134
canonicalize = opts[:canonicalize_host_name] ? opts[:canonicalize_host_name] : false
3235

33-
authenticator = org.mongodb.sasl.GSSAPIAuthenticator.new(JRuby.runtime, username, hostname, servicename, canonicalize)
34-
token = BSON::Binary.new(authenticator.initialize_challenge)
35-
cmd = BSON::OrderedHash['saslStart', 1, 'mechanism', 'GSSAPI', 'payload', token, 'autoAuthorize', 1]
36-
response = db.command(cmd, :check_response => false, :socket => socket)
37-
38-
until response['done'] do
39-
token = BSON::Binary.new(authenticator.evaluate_challenge(response['payload'].to_s))
40-
cmd = BSON::OrderedHash['saslContinue', 1, 'conversationId', response['conversationId'], 'payload', token]
41-
response = db.command(cmd, :check_response => false, :socket => socket)
36+
begin
37+
authenticator = org.mongodb.sasl.GSSAPIAuthenticator.new(JRuby.runtime, username, hostname, servicename, canonicalize)
38+
token = BSON::Binary.new(authenticator.initialize_challenge)
39+
cmd = BSON::OrderedHash['saslStart', 1, 'mechanism', 'GSSAPI', 'payload', token, 'autoAuthorize', 1]
40+
response = db.command(cmd, :check_response => false, :socket => socket)
41+
42+
until response['done'] do
43+
break unless Support.ok?(response)
44+
token = BSON::Binary.new(authenticator.evaluate_challenge(response['payload'].to_s))
45+
cmd = BSON::OrderedHash['saslContinue', 1, 'conversationId', response['conversationId'], 'payload', token]
46+
response = db.command(cmd, :check_response => false, :socket => socket)
47+
end
48+
response
49+
rescue Java::OrgMongodbSasl::MongoSecurityException
50+
return { }
4251
end
43-
response
4452
end
4553
end
4654

mongo.gemspec

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ Gem::Specification.new do |s|
2525
if RUBY_PLATFORM =~ /java/
2626
s.platform = 'java'
2727
s.files << 'ext/jsasl/target/jsasl.jar'
28+
else
29+
s.files += Dir.glob('ext/**/*.{c,h,rb}')
30+
s.extensions = ['ext/csasl/extconf.rb']
2831
end
2932

3033
s.test_files = Dir['test/**/*.rb'] - Dir['test/bson/*']

tasks/compile.rake

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ else
3131
ext.lib_dir = "lib/bson_ext"
3232
Rake::Task['clean'].invoke
3333
end
34+
Rake::ExtensionTask.new('csasl') do |ext|
35+
ext.name = "csasl"
36+
ext.ext_dir = "ext/csasl"
37+
ext.lib_dir = "lib/csasl"
38+
end
3439
end
3540

3641
desc "Run the default compile task"
37-
task :compile => RUBY_PLATFORM =~ /java/ ? ['compile:jbson', 'compile:jsasl'] : 'compile:cbson'
42+
task :compile => RUBY_PLATFORM =~ /java/ ? ['compile:jbson', 'compile:jsasl'] : ['compile:cbson', 'compile:csasl']

tasks/testing.rake

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ namespace :test do
6767
end
6868
task :commit => :default
6969

70+
# Both the functional and replica_set tests will use the kerberos C ext
71+
# when testing GSSAPI. So we must compile when on MRI.
72+
task :default => 'compile:csasl' unless RUBY_PLATFORM =~ /java/
73+
task :functional => 'compile:csasl' unless RUBY_PLATFORM =~ /java/
74+
task :replica_set => 'compile:csasl' unless RUBY_PLATFORM =~ /java/
75+
7076
desc 'Outputs diagnostic information for troubleshooting test failures.'
7177
task :diagnostic do
7278
puts <<-MSG
@@ -107,4 +113,15 @@ namespace :test do
107113
t.libs << 'test'
108114
end
109115
end
116+
117+
task :cleanup do |t|
118+
%w(data tmp coverage lib/bson_ext lib/csasl).each do |dir|
119+
if File.directory?(dir)
120+
puts "[CLEAN-UP] Removing '#{dir}'..."
121+
FileUtils.rm_rf(dir)
122+
end
123+
end
124+
t.reenable
125+
end
126+
Rake.application.top_level_tasks << 'test:cleanup'
110127
end

0 commit comments

Comments
 (0)