SQLRealm.java
/***************************************************************************
Copyright 2008 Emily Estes
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
***************************************************************************/
package net.metanotion.sqlauthident;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import net.metanotion.functor.Block;
import net.metanotion.sql.SchemaGenerator;
import net.metanotion.sql.SequenceExecutor;
import net.metanotion.util.JDBCTransaction;
import net.metanotion.util.SecureString;
import net.metanotion.authident.AuthPassword;
import net.metanotion.authident.AuthPasswordRealm;
import net.metanotion.authident.CredentialedUserToken;
import net.metanotion.authident.CredentialedUserTokenImpl;
import net.metanotion.authident.Realm;
import net.metanotion.authident.UserToken;
/** A SQL database backed implementation of the {@link net.metanotion.authident.Realm} and
{@link net.metanotion.authident.AuthPassword} interfaces. This class assumes the schema in src/main/sql/auth.schema.sql
*/
public final class SQLRealm implements AuthPassword<SQLRealm>, AuthPasswordRealm<SQLRealm> {
private static final Queries q = new Queries();
private static final String keyFunction = "bcrypt";
private static final String userCreationError = "User not created.";
private final DataSource ds;
private final SQLRealm that = this;
private static final SchemaGenerator schemaGenerator = new SchemaGenerator() {
@Override public Iterator<String> openSchema() { return SequenceExecutor.openSchema(SQLRealm.class, ";--"); }
};
/** This returns a SchemaGenerator instance that returns an iterator of the statements required to build the
expected schema for the SQL Realm.
@return an instance of SchemaGenerator that will enumerate the SQL DDL required to build the authentication
database tables.
*/
public static SchemaGenerator schemaFactory() { return schemaGenerator; }
/** Create a realm backed by a data source.
@param ds The DataSource to connect to the user database.
*/
public SQLRealm(final DataSource ds) { this.ds = ds; }
/** Provide an instance of an AuthPassword authentication provider.
@return An AuthPassword authentication provider instance backed by this realm.
*/
@Override public AuthPassword getAuthPassword() { return this; }
@Override public SQLUserToken getUser(final long uid) {
try (final Connection conn = ds.getConnection()) {
final List<Long> u = q.getUser(conn, uid);
if(u.size() > 0) {
return new SQLUserToken(this.ds, this, u.get(0));
} else {
return null;
}
} catch (final Exception ex) {
throw new RuntimeException(ex);
}
}
@Override public SQLUserToken createUser() {
return JDBCTransaction.doTX(ds, new Block<Connection,SQLUserToken>() {
public SQLUserToken eval(final Connection conn) throws Exception {
final long uid = q.reserveUID(conn);
q.addUser(conn, uid);
return new SQLUserToken(ds, that, uid);
}
});
}
@Override public CredentialedUserToken<SQLRealm, String> authenticate(final String username, final SecureString password) {
if(password == null) { return null; }
try {
if(username == null) { return null; }
return JDBCTransaction.doTX(ds, new Block<Connection, CredentialedUserToken<SQLRealm, String>>() {
public CredentialedUserToken<SQLRealm, String> eval(final Connection conn) throws Exception {
for(final AuthStruct authInfo: q.getAuthentication(conn, username)) {
if(BCrypt.checkpw(password.normalize(), authInfo.hash)) {
return new CredentialedUserTokenImpl<SQLRealm>(
new SQLUserToken(ds, that, authInfo.uid), username, that);
} else {
return null;
}
}
return null;
}
});
} finally {
password.close();
}
}
@Override public CredentialedUserToken<SQLRealm, String> getIdentity(final String username) {
if(username == null) { return null; }
return JDBCTransaction.doTX(ds, new Block<Connection, CredentialedUserToken<SQLRealm, String>>() {
public CredentialedUserToken<SQLRealm, String> eval(final Connection conn) throws Exception {
for(final Long uid: q.getByUsername(conn, username)) {
return new CredentialedUserTokenImpl<SQLRealm>(
new SQLUserToken(ds, that, uid.longValue()), username, that);
}
return null;
}
});
}
@Override public Iterable<String> listUsernames(final UserToken u) {
if(u == null) { return Collections.EMPTY_LIST; }
return JDBCTransaction.doTX(ds, new Block<Connection,Iterable<String>>() {
public Iterable<String> eval(final Connection conn) throws Exception {
return q.userList(conn, u.getID());
}
});
}
@Override public CredentialedUserToken<SQLRealm, String> createAuthentication(final String username,
final SecureString password) {
try {
final CredentialedUserToken<SQLRealm, String> ut =
new CredentialedUserTokenImpl<SQLRealm>(createUser(), username, this);
final String hashedPW = BCrypt.hashpw(password.normalize(), BCrypt.gensalt());
final CredentialedUserToken<SQLRealm, String> ut2 =
JDBCTransaction.doTX(ds, new Block<Connection,CredentialedUserToken<SQLRealm, String>>() {
public CredentialedUserToken<SQLRealm, String> eval(final Connection conn) throws Exception {
return (q.createAuthentication(conn, ut.getID(), username, hashedPW, keyFunction) > 0) ? ut : null;
}
});
if(ut2 == null) {
ut.delete();
return null;
} else {
return ut2;
}
} finally { password.close(); }
}
@Override public void addAuthentication(final UserToken u, final String username) {
JDBCTransaction.doTX(ds, new Block<Connection,Integer>() {
public Integer eval(final Connection conn) throws Exception {
return q.addAuthentication(conn, u.getID(), username);
}
});
}
@Override public void setPassword(final String username, final SecureString password) {
try {
final String hashedPW = BCrypt.hashpw(password.normalize(), BCrypt.gensalt());
JDBCTransaction.doTX(ds, new Block<Connection,Object>() {
public Object eval(final Connection conn) throws Exception {
return q.updateAuthentication(conn, username, hashedPW, keyFunction);
}
});
} finally { password.close(); }
}
@Override public void deleteAuthentication(final UserToken u, final String username) {
if(!(JDBCTransaction.doTX(ds, new Block<Connection,Boolean>() {
public Boolean eval(final Connection conn) throws Exception {
return (q.deleteAuthentication(conn, u.getID(), username) > 0);
}
}).booleanValue())) { throw new RuntimeException(userCreationError); }
}
/** The UserToken implementation used by this Realm/AuthPassword implementation. */
private static final class SQLUserToken implements UserToken {
private static final Queries q = new Queries();
private final DataSource ds;
private final SQLRealm realm;
private final long id;
/** Create a UserToken instance.
@param ds The database backing this instance.
@param realm The realm instance which is responsible for this user token.
@param id The realm unique long associated with this identity/user token.
*/
protected SQLUserToken(final DataSource ds, final SQLRealm realm, final long id) {
this.ds = ds;
this.id = id;
this.realm = realm;
}
@Override public SQLRealm realm() { return realm; }
@Override public long getID() { return id; }
@Override public void delete() {
try {
JDBCTransaction.doTX(ds.getConnection(), new Block<Connection,Integer>() {
public Integer eval(final Connection conn) throws Exception {
return q.deleteUser(conn, id);
}
});
} catch (SQLException e) { throw new RuntimeException(e); }
}
private SQLUserToken getToken(final UserToken u) {
final Realm r = u.realm();
if(r instanceof SQLRealm) {
return (SQLUserToken) r.getUser(u.getID());
} else {
return null;
}
}
@Override public boolean equals(final Object o) {
if(o instanceof UserToken) {
final SQLUserToken u = (o instanceof SQLUserToken) ? (SQLUserToken) o : getToken((UserToken) o);
if(u==null) { return false; }
return this.realm.equals(u.realm) && (u.id == this.id);
}
return false;
}
@Override public int hashCode() {
return this.ds.hashCode() + Long.valueOf(this.id).hashCode();
}
}
@Override public boolean equals(final Object obj) {
if(!(obj instanceof SQLRealm)) { return false; }
return this.ds.equals(((SQLRealm) obj).ds);
}
@Override public int hashCode() { return this.ds.hashCode(); }
}