Skip to content

Commit 4874df2

Browse files
committed
JAVA-436: When a database and credentials are specified in the Mongo URI, automatically authenticate the database. This is done lazily on first use of the database by just setting the credentials on the DB instance. Care is taken to make sure that DB.authenticate and DB.authenticateCommand still work correctly even if credentials are on the URI, so long as the same credentials are used in all cases. Otherwise clients who were working around this by calling those methods explicitly would break.
1 parent 59eacbf commit 4874df2

File tree

6 files changed

+241
-59
lines changed

6 files changed

+241
-59
lines changed

src/main/com/mongodb/DB.java

Lines changed: 81 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.io.ByteArrayOutputStream;
2626
import java.io.IOException;
2727
import java.util.ArrayList;
28+
import java.util.Arrays;
2829
import java.util.Collections;
2930
import java.util.HashSet;
3031
import java.util.Iterator;
@@ -40,7 +41,7 @@
4041
public abstract class DB {
4142

4243
private static final Set<String> _obedientCommands = new HashSet<String>();
43-
44+
4445
static {
4546
_obedientCommands.add("group");
4647
_obedientCommands.add("aggregate");
@@ -63,6 +64,17 @@ public DB( Mongo mongo , String name ){
6364
_options = new Bytes.OptionHolder( _mongo._netOptions );
6465
}
6566

67+
/**
68+
* @param mongo the mongo instance
69+
* @param name the database name
70+
*/
71+
DB( Mongo mongo , String name, String username, char[] password ) {
72+
_mongo = mongo;
73+
_name = name;
74+
_options = new Bytes.OptionHolder( _mongo._netOptions );
75+
authenticationCredentialsReference.set(new AuthenticationCredentials(username, password));
76+
}
77+
6678
/**
6779
* Determines the read preference that should be used for the given command.
6880
* @param command the <code>DBObject</code> representing the command
@@ -558,53 +570,70 @@ public boolean isAuthenticated() {
558570
}
559571

560572
/**
561-
* Authenticates to db with the given name and password
573+
* Authenticates to db with the given credentials. If this method (or {@code authenticateCommand} has already been
574+
* called with the same credentials and the authentication test succeeded, this method will return true. If this method
575+
* has already been called with different credentials and the authentication test succeeded,
576+
* this method will throw an {@code IllegalStateException}. If this method has already been called with any credentials
577+
* and the authentication test failed, this method will re-try the authentication test with the
578+
* given credentials.
562579
*
563580
* @param username name of user for this database
564581
* @param password password of user for this database
565582
* @return true if authenticated, false otherwise
566-
* @throws MongoException
583+
* @throws MongoException if authentication failed due to invalid user/pass, or other exceptions like I/O
584+
* @throws IllegalStateException if authentiation test has already succeeded with different credentials
585+
* @see #authenticateCommand(String, char[])
567586
* @dochub authenticate
568587
*/
569588
public boolean authenticate(String username, char[] password ){
570-
571-
if (authenticationCredentialsReference.get() != null) {
572-
throw new IllegalStateException("can't authenticate twice on the same database");
573-
}
574-
575-
AuthenticationCredentials newCredentials = new AuthenticationCredentials(username, password);
576-
CommandResult res = newCredentials.authenticate();
577-
if (!res.ok())
578-
return false;
579-
580-
boolean wasNull = authenticationCredentialsReference.compareAndSet(null, newCredentials);
581-
if (!wasNull) {
582-
throw new IllegalStateException("can't authenticate twice on the same database");
583-
}
584-
return true;
589+
return authenticateCommandHelper(username, password).ok();
585590
}
586591

587592
/**
588-
* Authenticates to db with the given name and password
593+
* Authenticates to db with the given credentials. If this method (or {@code authenticate} has already been
594+
* called with the same credentials and the authentication test succeeded, this method will return true. If this method
595+
* has already been called with different credentials and the authentication test succeeded,
596+
* this method will throw an {@code IllegalStateException}. If this method has already been called with any credentials
597+
* and the authentication test failed, this method will re-try the authentication test with the
598+
* given credentials.
599+
*
589600
*
590601
* @param username name of user for this database
591602
* @param password password of user for this database
592603
* @return the CommandResult from authenticate command
593604
* @throws MongoException if authentication failed due to invalid user/pass, or other exceptions like I/O
605+
* @throws IllegalStateException if authentiation test has already succeeded with different credentials
606+
* @see #authenticate(String, char[])
594607
* @dochub authenticate
595608
*/
596609
public synchronized CommandResult authenticateCommand(String username, char[] password ){
610+
CommandResult res = authenticateCommandHelper(username, password);
611+
res.throwOnError();
612+
return res;
613+
}
597614

598-
if (authenticationCredentialsReference.get() != null) {
599-
throw new IllegalStateException( "can't authenticate twice on the same database" );
615+
private CommandResult authenticateCommandHelper(String username, char[] password) {
616+
AuthenticationCredentials currentCredentials = authenticationCredentialsReference.get();
617+
AuthenticationCredentials newCredentials = new AuthenticationCredentials(username, password);
618+
619+
if (currentCredentials != null) {
620+
if (currentCredentials.equals(newCredentials)) {
621+
if (credentialsAlreadySuccessfullyTested) {
622+
return authenticationTestCommandResult;
623+
}
624+
} else {
625+
throw new IllegalStateException("can't authenticate twice on the same database");
626+
}
600627
}
601628

602-
AuthenticationCredentials newCredentials = new AuthenticationCredentials(username, password);
603629
CommandResult res = newCredentials.authenticate();
604-
res.throwOnError();
605-
boolean wasNull = authenticationCredentialsReference.compareAndSet(null, newCredentials);
606-
if (!wasNull) {
607-
throw new IllegalStateException("can't authenticate twice on the same database");
630+
if (res.ok()) {
631+
boolean wasNull = authenticationCredentialsReference.compareAndSet(null, newCredentials);
632+
if (!wasNull && credentialsAlreadySuccessfullyTested) {
633+
throw new IllegalStateException("can't authenticate twice on the same database");
634+
}
635+
credentialsAlreadySuccessfullyTested = true;
636+
authenticationTestCommandResult = res;
608637
}
609638
return res;
610639
}
@@ -776,8 +805,14 @@ AuthenticationCredentials getAuthenticationCredentials() {
776805
private com.mongodb.ReadPreference _readPref;
777806
final Bytes.OptionHolder _options;
778807

808+
// the credentials, possibly set in the constructor, in which case they have not been tested yet.
779809
private AtomicReference<AuthenticationCredentials> authenticationCredentialsReference =
780810
new AtomicReference<AuthenticationCredentials>();
811+
// this can be false with credentials set if the credentials were passed in to the constructor
812+
private volatile boolean credentialsAlreadySuccessfullyTested = false;
813+
// cached authentication command result, to return in case of multiple calls to authenticateCommand with the
814+
// same credentials
815+
private volatile CommandResult authenticationTestCommandResult;
781816

782817
/**
783818
* Encapsulate everything relating to authorization of a user on a database
@@ -797,6 +832,26 @@ private AuthenticationCredentials(final String userName, final char[] password)
797832
this.authHash = createHash(userName, password);
798833
}
799834

835+
@Override
836+
public boolean equals(final Object o) {
837+
if (this == o) return true;
838+
if (o == null || getClass() != o.getClass()) return false;
839+
840+
final AuthenticationCredentials that = (AuthenticationCredentials) o;
841+
842+
if (!Arrays.equals(authHash, that.authHash)) return false;
843+
if (!userName.equals(that.userName)) return false;
844+
845+
return true;
846+
}
847+
848+
@Override
849+
public int hashCode() {
850+
int result = userName.hashCode();
851+
result = 31 * result + Arrays.hashCode(authHash);
852+
return result;
853+
}
854+
800855
CommandResult authenticate() {
801856
requestStart();
802857
try {

src/main/com/mongodb/DBApiLayer.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,26 @@ protected DBApiLayer( Mongo mongo, String name , DBConnector connector ){
9999
_connector = connector;
100100
}
101101

102+
/**
103+
*
104+
* @param mongo the Mongo instance
105+
* @param name the database name
106+
* @param connector the connector
107+
* @param username username to authenticate database against
108+
* @param password password to authenticate database against
109+
*/
110+
protected DBApiLayer(final Mongo mongo, final String name, final DBTCPConnector connector, final String username, final char[] password) {
111+
super(mongo, name, username, password);
112+
113+
if ( connector == null )
114+
throw new IllegalArgumentException( "need a connector: " + name );
115+
116+
_root = name;
117+
_rootPlusDot = _root + ".";
118+
119+
_connector = connector;
120+
}
121+
102122
public void requestStart(){
103123
_connector.requestStart();
104124
}

src/main/com/mongodb/Mongo.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,8 @@ public Mongo( List<ServerAddress> seeds , MongoOptions options ) {
295295
/**
296296
* Creates a Mongo described by a URI.
297297
* If only one address is used it will only connect to that node, otherwise it will discover all nodes.
298+
* If the URI contains database credentials, the database will be authenticated lazily on first use
299+
* with those credentials.
298300
* @param uri
299301
* @see MongoURI
300302
* <p>examples:
@@ -326,6 +328,11 @@ public Mongo( MongoURI uri )
326328
_connector = new DBTCPConnector( this , replicaSetSeeds );
327329
}
328330

331+
if (uri.getDatabase() != null && uri.getUsername() != null) {
332+
DB db = new DBApiLayer(this, uri.getDatabase() , _connector, uri.getUsername(), uri.getPassword());
333+
_dbs.put(db.getName(), db);
334+
}
335+
329336
_connector.start();
330337
if (_options.cursorFinalizerEnabled) {
331338
_cleaner = new CursorCleanerThread();

src/main/com/mongodb/MongoURI.java

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,8 @@ public MongoOptions getOptions(){
192192

193193
/**
194194
* creates a Mongo instance based on the URI
195-
* @return
195+
* @return a new Mongo instance. There is no caching, so each call will create a new instance, each of which
196+
* must be closed manually.
196197
* @throws MongoException
197198
* @throws UnknownHostException
198199
*/
@@ -205,29 +206,27 @@ public Mongo connect()
205206

206207
/**
207208
* returns the DB object from a newly created Mongo instance based on this URI
208-
* @return
209+
* @return the database specified in the URI. This will implicitly create a new Mongo instance,
210+
* which must be closed manually.
209211
* @throws MongoException
210212
* @throws UnknownHostException
211213
*/
212-
public DB connectDB()
213-
throws UnknownHostException {
214-
// TODO auth
215-
return connect().getDB( getDatabase() );
214+
public DB connectDB() throws UnknownHostException {
215+
return connect().getDB(getDatabase());
216216
}
217217

218218
/**
219219
* returns the URI's DB object from a given Mongo instance
220-
* @param m
221-
* @return
220+
* @param mongo the Mongo instance to get the database from.
221+
* @return the database specified in this URI
222222
*/
223-
public DB connectDB( Mongo m ){
224-
// TODO auth
225-
return m.getDB( getDatabase() );
223+
public DB connectDB( Mongo mongo ){
224+
return mongo.getDB( getDatabase() );
226225
}
227226

228227
/**
229228
* returns the URI's Collection from a given DB object
230-
* @param db
229+
* @param db the database to get the collection from
231230
* @return
232231
*/
233232
public DBCollection connectCollection( DB db ){
@@ -236,11 +235,11 @@ public DBCollection connectCollection( DB db ){
236235

237236
/**
238237
* returns the URI's Collection from a given Mongo instance
239-
* @param m
240-
* @return
238+
* @param mongo the mongo instance to get the collection from
239+
* @return the collection specified in this URI
241240
*/
242-
public DBCollection connectCollection( Mongo m ){
243-
return connectDB( m ).getCollection( getCollection() );
241+
public DBCollection connectCollection( Mongo mongo ){
242+
return connectDB( mongo ).getCollection( getCollection() );
244243
}
245244

246245
// ---------------------------------

0 commit comments

Comments
 (0)