/**
* Licensed to Jasig under one or more contributor license
* agreements. See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* Jasig licenses this file to you 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 org.jasig.portal.jgroups.auth;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collections;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.sql.DataSource;
import org.hibernate.exception.ConstraintViolationException;
import org.jasig.portal.jgroups.protocols.PingDao;
import org.jasig.portal.utils.JdbcUtils;
import org.jasig.portal.utils.RandomTokenGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.support.DataAccessUtils;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionOperations;
import org.springframework.transaction.support.TransactionTemplate;
import com.google.common.collect.ImmutableMap;
/**
* {@link PingDao} that uses the Spring JDBC APIs to do its work.
*
* @author Eric Dalquist
*/
public class JdbcAuthDao implements AuthDao, InitializingBean {
/**
* This class is ONLY used to provide for creation of the table/index required by the {@link JdbcAuthDao}.
* Due to the JPA -> Hibernate -> Ehcache -> JGroups -> DAO_PING -> JdbcAuthDao reference chain this class
* CANNOT directly reference the JPA entity manager or transaction manager
*/
@Entity(name = Table.NAME)
public static class Table implements Serializable {
private static final long serialVersionUID = 1L;
static final String NAME = "UP_JGROUPS_AUTH";
static final String COL_SERVICE_NAME = "SERVICE_NAME";
static final String COL_RANDOM_TOKEN = "RANDOM_TOKEN";
@Id
@Column(name=COL_SERVICE_NAME, length=100)
private final String serviceName = null;
@Column(name=COL_RANDOM_TOKEN, length=4000)
private final String randomToken = null;
}
private static final String PRM_SERVICE_NAME = "serviceName";
private static final String PRM_RANDOM_TOKEN = "randomToken";
private static final String INSERT_SQL =
"INSERT INTO " + Table.NAME + " " +
"(" + Table.COL_SERVICE_NAME + ", " + Table.COL_RANDOM_TOKEN + ") " +
"values (:" + PRM_SERVICE_NAME + ", :" + PRM_RANDOM_TOKEN + ")";
private static final String SELECT_SQL =
"SELECT " + Table.COL_RANDOM_TOKEN + " " +
"FROM " + Table.NAME + " " +
"WHERE " + Table.COL_SERVICE_NAME + "=:" + PRM_SERVICE_NAME;
protected final Logger logger = LoggerFactory.getLogger(getClass());
private int authTokenLength = 1000;
private JdbcOperations jdbcOperations;
private NamedParameterJdbcOperations namedParameterJdbcOperations;
private volatile boolean ready = false;
public void setJdbcOperations(JdbcOperations jdbcOperations) {
this.jdbcOperations = jdbcOperations;
this.namedParameterJdbcOperations = new NamedParameterJdbcTemplate(this.jdbcOperations);
}
@Value("${org.jasig.portal.jgroups.auth.token_length:1000}")
public void setAuthTokenLength(int authTokenLength) {
this.authTokenLength = authTokenLength;
}
@Override
public void afterPropertiesSet() throws Exception {
HashedDaoAuthToken.setAuthDao(this);
}
@Override
public String getAuthToken(String serviceName) {
if (!isReady()) {
return null;
}
for (int count = 0; count < 10; count++) {
final String token = DataAccessUtils.singleResult(this.namedParameterJdbcOperations.queryForList(SELECT_SQL, Collections.singletonMap(PRM_SERVICE_NAME, serviceName), String.class));
if (token != null) {
return token;
}
//No token found, try creating it
createToken(serviceName);
}
logger.warn("Failed to get/create auth token for {} after 10 tries", serviceName);
return null;
}
protected void createToken(final String serviceName) {
try {
this.jdbcOperations.execute(new ConnectionCallback<Object>() {
@Override
public Object doInConnection(Connection con) throws SQLException, DataAccessException {
//This is horribly hacky but we can't rely on the main uPortal TM directly or we get
//into a circular dependency loop from JPA to Ehcache to jGroups and back to JPA
final DataSource ds = new SingleConnectionDataSource(con, true);
final PlatformTransactionManager ptm = new DataSourceTransactionManager(ds);
final TransactionOperations to = new TransactionTemplate(ptm);
to.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
logger.info("Creating jGroups auth token");
final String authToken = RandomTokenGenerator.INSTANCE.generateRandomToken(authTokenLength);
final ImmutableMap<String, String> params = ImmutableMap.of(
PRM_SERVICE_NAME, serviceName,
PRM_RANDOM_TOKEN, authToken);
namedParameterJdbcOperations.update(INSERT_SQL, params);
}
});
return null;
}
});
}
catch (ConstraintViolationException e) {
//Ignore, just means a concurrent token creation
}
catch (DataIntegrityViolationException e) {
//Ignore, just means a concurrent token creation
}
}
protected boolean isReady() {
boolean r = this.ready;
if (!r) {
r = JdbcUtils.doesTableExist(this.jdbcOperations, Table.NAME);
if (r) {
this.ready = r;
}
}
return r;
}
}