/**
* Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright ownership. Apereo
* 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 the
* following location:
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apereo.portal.jgroups.auth;
import com.google.common.collect.ImmutableMap;
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.apereo.portal.jgroups.protocols.PingDao;
import org.apereo.portal.utils.JdbcUtils;
import org.apereo.portal.utils.RandomTokenGenerator;
import org.hibernate.exception.ConstraintViolationException;
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;
/**
* {@link PingDao} that uses the Spring JDBC APIs to do its work.
*
*/
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.apereo.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;
}
}