Skip to content

Commit 96b3d2f

Browse files
committed
RUBY-530 Kerberos support on JRuby
Special thanks to Jeff Yemin (@jyemin) for his help and review. RUBY-530 Support for GSSAPI in URI parser RUBY-530 More tests - unit, replica set, threading RUBY-530 Use the socket to get hostname RUBY-530 Refer to config file as JAAS_LOGIN_CONFIG_FILE RUBY-530 Update extra options name in auth module RUBY-530 Don't require a config file in gssapi test RUBY-530 Keep realm and host as different env variables in gssapi test RUBY-530 Adding javadocs to GSSAPIAuthenticator RUBY-530 Use a byte challenge instead of a string to avoid encoding Get rid of extra whitespaces RUBY-530 Initialize extra opts to empty hash if there are none RUBY-530 Adding in tests for failure with wrong servicename Fixing indentation Fixing typo RUBY-530 Adding in tests for extra opts RUBY-530 Split kdc and host in tests, they are usually not the same RUBY-530 Test for canonicalizehostname option RUBY-530 Auth hashes now also have an extra key RUBY-530 Rename extra opts variables and provide better error msg RUBY-530 update uri test for new extra opts format
1 parent 61732c2 commit 96b3d2f

File tree

16 files changed

+522
-92
lines changed

16 files changed

+522
-92
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright (C) 2009-2014 MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.mongodb.sasl;
18+
19+
import org.ietf.jgss.GSSCredential;
20+
import org.ietf.jgss.GSSException;
21+
import org.ietf.jgss.GSSManager;
22+
import org.ietf.jgss.GSSName;
23+
import org.ietf.jgss.Oid;
24+
import org.jruby.Ruby;
25+
import org.jruby.RubyString;
26+
import org.jruby.RubyBoolean;
27+
28+
import javax.security.sasl.Sasl;
29+
import javax.security.sasl.SaslClient;
30+
import javax.security.sasl.SaslException;
31+
import java.net.UnknownHostException;
32+
import java.net.InetAddress;
33+
import java.util.HashMap;
34+
import java.util.Map;
35+
36+
/**
37+
* A helper class for SASL authentication using GSSAPI (Kerberos)
38+
*/
39+
public class GSSAPIAuthenticator {
40+
private static final String GSSAPI_MECHANISM_NAME = "GSSAPI";
41+
private static final String GSSAPI_OID = "1.2.840.113554.1.2.2";
42+
public static final String CANONICALIZE_HOST_NAME_KEY = "CANONICALIZE_HOST_NAME";
43+
44+
private final Ruby runTime;
45+
private final String userName;
46+
private final String hostName;
47+
private final String serviceName;
48+
private final boolean canonicalizeHostName;
49+
50+
private final SaslClient saslClient;
51+
52+
/**
53+
* Constructs a wrapper for a Sasl client that handles GSSAPI (Kerberos) mechanism authentication.
54+
*
55+
* @param runTime the Ruby run time
56+
* @param userName the user name
57+
* @param hostName the host name
58+
* @param serviceName the service name
59+
* @param canonicalizeHostName whether the hostname should be canonicalized
60+
*/
61+
public GSSAPIAuthenticator(final Ruby runTime, final RubyString userName, final RubyString hostName, final RubyString serviceName, final RubyBoolean canonicalizeHostName) {
62+
this.runTime = runTime;
63+
this.userName = userName.decodeString();
64+
this.hostName = hostName.decodeString();
65+
this.serviceName = serviceName.decodeString();
66+
this.canonicalizeHostName = (Boolean) canonicalizeHostName.toJava(Boolean.class);
67+
this.saslClient = createSaslClient();
68+
}
69+
70+
/**
71+
* If the mechanism has an initial response, evaluteChallenge() is called to get the challenge. Otherwise, null is returned.
72+
*
73+
* @return the initial challenge to send to the server or null if the mechanism doesn't have an initial response.
74+
*
75+
* @throws MongoSecurityException if there is no response to the challenge.
76+
*/
77+
public RubyString initializeChallenge() {
78+
try {
79+
return saslClient.hasInitialResponse() ? RubyString.newString(runTime, saslClient.evaluateChallenge(new byte[0])) : null;
80+
} catch (SaslException e) {
81+
throw new MongoSecurityException("SASL protocol error: no client response to challenge for credential", e);
82+
}
83+
}
84+
85+
/**
86+
* Evaluate the next challenge, given the response from the server.
87+
*
88+
* @param rubyPayload the non-null challenge sent from the server.
89+
*
90+
* @return the response to the challenge
91+
*/
92+
public RubyString evaluateChallenge(RubyString rubyPayload) {
93+
try {
94+
return RubyString.newString(runTime, saslClient.evaluateChallenge(rubyPayload.getBytes()));
95+
} catch (SaslException e) {
96+
throw new MongoSecurityException("SASL protocol error: no client response to challenge for credential", e);
97+
}
98+
}
99+
100+
private SaslClient createSaslClient() {
101+
try {
102+
Map<String, Object> props = new HashMap<String, Object>();
103+
props.put(Sasl.CREDENTIALS, getGSSCredential(userName));
104+
SaslClient saslClient = Sasl.createSaslClient(new String[]{GSSAPI_MECHANISM_NAME}, userName,
105+
serviceName,
106+
getHostName(), props, null);
107+
if (saslClient == null) {
108+
throw new MongoSecurityException(String.format("No platform support for %s mechanism", GSSAPI_MECHANISM_NAME));
109+
}
110+
return saslClient;
111+
} catch (SaslException e) {
112+
throw new MongoSecurityException(e);
113+
} catch (GSSException e) {
114+
throw new MongoSecurityException(e);
115+
} catch (UnknownHostException e) {
116+
throw new MongoSecurityException(e);
117+
} catch (SecurityException e) {
118+
throw new MongoSecurityException(e);
119+
}
120+
}
121+
122+
private GSSCredential getGSSCredential(final String userName) throws GSSException {
123+
Oid krb5Mechanism = new Oid(GSSAPI_OID);
124+
GSSManager manager = GSSManager.getInstance();
125+
GSSName name = manager.createName(userName, GSSName.NT_USER_NAME);
126+
return manager.createCredential(name, GSSCredential.INDEFINITE_LIFETIME, krb5Mechanism, GSSCredential.INITIATE_ONLY);
127+
}
128+
129+
private String getHostName() throws UnknownHostException {
130+
return canonicalizeHostName ? InetAddress.getByName(hostName).getCanonicalHostName() : hostName;
131+
}
132+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.mongodb.sasl;
2+
3+
public class MongoSecurityException extends RuntimeException {
4+
5+
private static final long serialVersionUID = -7531399100914218967L;
6+
7+
public MongoSecurityException(final Throwable cause) {
8+
super(cause);
9+
}
10+
11+
public MongoSecurityException(final String message) {
12+
super(message);
13+
}
14+
15+
public MongoSecurityException(final String message, final Throwable cause) {
16+
super(message, cause);
17+
}
18+
}

ext/jsasl/target/jsasl.jar

2.68 KB
Binary file not shown.

lib/mongo/db.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ def initialize(name, client, opts={})
141141
# used to authenticate against a database when the credentials exist
142142
# elsewhere.
143143
# @param mechanism [String] The authentication mechanism to be used.
144+
# @param extra [Hash] A optional hash of extra options to be stored with
145+
# the credential set.
144146
#
145147
# @note The ability to disable the save_auth option has been deprecated.
146148
# With save_auth=false specified, driver authentication behavior during
@@ -150,12 +152,12 @@ def initialize(name, client, opts={})
150152
#
151153
# @raise [AuthenticationError] Raised if authentication fails.
152154
# @return [Boolean] The result of the authentication operation.
153-
def authenticate(username, password=nil, save_auth=nil, source=nil, mechanism=nil)
155+
def authenticate(username, password=nil, save_auth=nil, source=nil, mechanism=nil, extra=nil)
154156
warn "[DEPRECATED] Disabling the 'save_auth' option no longer has " +
155157
"any effect. Please see the API documentation for more details " +
156158
"on this change." unless save_auth.nil?
157159

158-
@client.add_auth(self.name, username, password, source, mechanism)
160+
@client.add_auth(self.name, username, password, source, mechanism, extra)
159161
true
160162
end
161163

lib/mongo/functional.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@
1717
require 'mongo/functional/read_preference'
1818
require 'mongo/functional/write_concern'
1919
require 'mongo/functional/uri_parser'
20+
21+
require 'mongo/functional/sasl_java' if RUBY_PLATFORM =~ /java/

lib/mongo/functional/authentication.rb

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ module Authentication
1919

2020
DEFAULT_MECHANISM = 'MONGODB-CR'
2121
MECHANISMS = ['GSSAPI', 'MONGODB-CR', 'MONGODB-X509', 'PLAIN']
22+
EXTRA = { 'GSSAPI' => [:gssapi_service_name, :canonicalize_host_name] }
2223

2324
# authentication module methods
2425
class << self
@@ -59,6 +60,15 @@ def validate_credentials(auth)
5960
"When using the authentication mechanism #{auth[:mechanism]} " +
6061
"both username and password are required."
6162
end
63+
# if extra opts exist, validate them
64+
allowed_keys = EXTRA[auth[:mechanism]]
65+
if auth[:extra] && !auth[:extra].empty?
66+
invalid_opts = []
67+
auth[:extra].keys.each { |k| invalid_opts << k unless allowed_keys.include?(k) }
68+
raise MongoArgumentError,
69+
"Invalid extra option(s): #{invalid_opts} found. Please check the extra options" +
70+
" passed and try again." unless invalid_opts.empty?
71+
end
6272
auth
6373
end
6474

@@ -95,19 +105,22 @@ def hash_password(username, password)
95105
# (if different than the current database).
96106
# @param mechanism [String] (nil) The authentication mechanism being used
97107
# (default: 'MONGODB-CR').
108+
# @param extra [Hash] (nil) A optional hash of extra options to be stored with
109+
# the credential set.
98110
#
99111
# @raise [MongoArgumentError] Raised if the database has already been used
100112
# for authentication. A log out is required before additional auths can
101113
# be issued against a given database.
102114
# @raise [AuthenticationError] Raised if authentication fails.
103115
# @return [Hash] a hash representing the authentication just added.
104-
def add_auth(db_name, username, password=nil, source=nil, mechanism=nil)
116+
def add_auth(db_name, username, password=nil, source=nil, mechanism=nil, extra=nil)
105117
auth = Authentication.validate_credentials({
106118
:db_name => db_name,
107119
:username => username,
108120
:password => password,
109121
:source => source,
110-
:mechanism => mechanism
122+
:mechanism => mechanism,
123+
:extra => extra
111124
})
112125

113126
if @auths.any? {|a| a[:source] == auth[:source]}
@@ -276,7 +289,9 @@ def issue_plain(auth, opts={})
276289
# @private
277290
def issue_gssapi(auth, opts={})
278291
raise NotImplementedError,
279-
"The #{auth[:mechanism]} authentication mechanism is not yet supported."
292+
"The #{auth[:mechanism]} authentication mechanism is only supported " +
293+
"for JRuby." unless RUBY_PLATFORM =~ /java/
294+
Mongo::Sasl::GSSAPI.authenticate(auth[:username], self, opts[:socket], auth[:extra] || {})
280295
end
281296

282297
# Helper to fetch a nonce value from a given database instance.

lib/mongo/functional/sasl_java.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Copyright (C) 2009-2013 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+
require 'jruby'
16+
17+
include Java
18+
19+
jar_dir = File.join(File.dirname(__FILE__), '../../../ext/jsasl')
20+
require File.join(jar_dir, 'target/jsasl.jar')
21+
22+
module Mongo
23+
module Sasl
24+
25+
module GSSAPI
26+
27+
def self.authenticate(username, client, socket, opts={})
28+
db = client.db('$external')
29+
hostname = socket.pool.host
30+
servicename = opts[:gssapi_service_name] || 'mongodb'
31+
canonicalize = opts[:canonicalize_host_name] ? opts[:canonicalize_host_name] : false
32+
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)
42+
end
43+
response
44+
end
45+
end
46+
47+
end
48+
end

0 commit comments

Comments
 (0)