/*******************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved.
*
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product includes a number of subcomponents with
* separate copyright notices and license terms. Your use of these
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
*******************************************************************************/
package org.cloudfoundry.identity.uaa.codestore;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cloudfoundry.identity.uaa.util.TimeService;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.springframework.util.Assert;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.concurrent.atomic.AtomicLong;
public class JdbcExpiringCodeStore implements ExpiringCodeStore {
public static final String tableName = "expiring_code_store";
public static final String fields = "code, expiresat, data, intent, identity_zone_id";
protected static final String insert = "insert into " + tableName + " (" + fields + ") values (?,?,?,?,?)";
protected static final String delete = "delete from " + tableName + " where code = ? and identity_zone_id = ?";
protected static final String deleteIntent = "delete from " + tableName + " where intent = ? and identity_zone_id = ?";
protected static final String deleteExpired = "delete from " + tableName + " where expiresat < ?";
private static final JdbcExpiringCodeMapper rowMapper = new JdbcExpiringCodeMapper();
protected static final String selectAllFields = "select " + fields + " from " + tableName + " where code = ? and identity_zone_id = ?";
private Log logger = LogFactory.getLog(getClass());
private RandomValueStringGenerator generator = new RandomValueStringGenerator(10);
private JdbcTemplate jdbcTemplate;
private TimeService timeService;
private AtomicLong lastExpired = new AtomicLong();
private long expirationInterval = 60 * 1000; // once a minute
public long getExpirationInterval() {
return expirationInterval;
}
public void setExpirationInterval(long expirationInterval) {
this.expirationInterval = expirationInterval;
}
protected JdbcExpiringCodeStore() {
// package protected for unit tests only
}
public JdbcExpiringCodeStore(DataSource dataSource, TimeService timeService) {
setDataSource(dataSource);
setTimeService(timeService);
}
protected void setDataSource(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
protected void setTimeService(TimeService timeService) {
this.timeService = timeService;
}
@Override
public ExpiringCode generateCode(String data, Timestamp expiresAt, String intent) {
cleanExpiredEntries();
if (data == null || expiresAt == null) {
throw new NullPointerException();
}
if (expiresAt.getTime() < timeService.getCurrentTimeMillis()) {
throw new IllegalArgumentException();
}
int count = 0;
while (count < 3) {
count++;
String code = generator.generate();
try {
int update = jdbcTemplate.update(insert, code, expiresAt.getTime(), data, intent, IdentityZoneHolder.get().getId());
if (update == 1) {
ExpiringCode expiringCode = new ExpiringCode(code, expiresAt, data, intent);
return expiringCode;
} else {
logger.warn("Unable to store expiring code:" + code);
}
} catch (DataIntegrityViolationException x) {
if (count == 3) {
throw x;
}
}
}
return null;
}
@Override
public ExpiringCode retrieveCode(String code) {
cleanExpiredEntries();
if (code == null) {
throw new NullPointerException();
}
try {
ExpiringCode expiringCode = jdbcTemplate.queryForObject(selectAllFields, rowMapper, code, IdentityZoneHolder.get().getId());
if (expiringCode != null) {
jdbcTemplate.update(delete, code, IdentityZoneHolder.get().getId());
}
if (expiringCode.getExpiresAt().getTime() < timeService.getCurrentTimeMillis()) {
expiringCode = null;
}
return expiringCode;
} catch (EmptyResultDataAccessException x) {
return null;
}
}
@Override
public void setGenerator(RandomValueStringGenerator generator) {
this.generator = generator;
}
@Override
public void expireByIntent(String intent) {
Assert.hasText(intent);
jdbcTemplate.update(deleteIntent, intent, IdentityZoneHolder.get().getId());
}
public int cleanExpiredEntries() {
long now = timeService.getCurrentTimeMillis();
long lastCheck = lastExpired.get();
if ((now - lastCheck) > expirationInterval && lastExpired.compareAndSet(lastCheck, now)) {
int count = jdbcTemplate.update(deleteExpired, now);
logger.debug("Expiring code sweeper complete, deleted " + count + " entries.");
return count;
}
return 0;
}
protected static class JdbcExpiringCodeMapper implements RowMapper<ExpiringCode> {
@Override
public ExpiringCode mapRow(ResultSet rs, int rowNum) throws SQLException {
String code = rs.getString("code");
Timestamp expiresAt = new Timestamp(rs.getLong("expiresat"));
String intent = rs.getString("intent");
String data = rs.getString("data");
return new ExpiringCode(code, expiresAt, data, intent);
}
}
}