package org.apereo.cas.ticket.registry;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.ignite.Ignite;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.IgniteState;
import org.apache.ignite.Ignition;
import org.apache.ignite.cache.query.QueryCursor;
import org.apache.ignite.cache.query.ScanQuery;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.ssl.SslContextFactory;
import org.apereo.cas.configuration.model.support.ignite.IgniteProperties;
import org.apereo.cas.ticket.Ticket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.cache.Cache;
import javax.cache.expiry.Duration;
import javax.cache.expiry.ExpiryPolicy;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
import static java.util.stream.Collectors.toList;
/**
* <p>
* <a href="https://ignite.apache.org">Ignite</a> based distributed ticket registry.
* </p>
* <p>
* Use distinct caches for ticket granting tickets (TGT) and service tickets (ST) for:
* </p>
* <ul>
* <li>Tuning: use cache level time to live with different values for TGT an ST.</li>
* <li>Monitoring: follow separately the number of TGT and ST.</li>
* </ul>
*
* @author Timur Duehr timur.duehr@nccgroup.trust
* @since 5.0.0`
*/
public class IgniteTicketRegistry extends AbstractTicketRegistry {
private static final Logger LOGGER = LoggerFactory.getLogger(IgniteTicketRegistry.class);
private final IgniteConfiguration igniteConfiguration;
private final IgniteProperties properties;
private IgniteCache<String, Ticket> ticketIgniteCache;
private Ignite ignite;
private boolean supportRegistryState = true;
/**
* Instantiates a new Ignite ticket registry.
*
* @param igniteConfiguration the ignite configuration
* @param properties the properties
*/
public IgniteTicketRegistry(final IgniteConfiguration igniteConfiguration, final IgniteProperties properties) {
this.igniteConfiguration = igniteConfiguration;
this.properties = properties;
}
@Override
public void addTicket(final Ticket ticketToAdd) {
final Ticket ticket = encodeTicket(ticketToAdd);
LOGGER.debug("Adding ticket [{}] to the cache [{}]", ticket.getId(), this.ticketIgniteCache.getName());
this.ticketIgniteCache.withExpiryPolicy(new ExpiryPolicy() {
@Override
public Duration getExpiryForCreation() {
return new Duration(TimeUnit.SECONDS, ticket.getExpirationPolicy().getTimeToLive());
}
@Override
public Duration getExpiryForAccess() {
final long idleTime = ticket.getExpirationPolicy().getTimeToIdle() <= 0
? ticket.getExpirationPolicy().getTimeToLive()
: ticket.getExpirationPolicy().getTimeToIdle();
return new Duration(TimeUnit.SECONDS, idleTime);
}
@Override
public Duration getExpiryForUpdate() {
return new Duration(TimeUnit.SECONDS, ticket.getExpirationPolicy().getTimeToLive());
}
}).put(ticket.getId(), ticket);
}
@Override
public long deleteAll() {
final int size = this.ticketIgniteCache.size();
this.ticketIgniteCache.removeAll();
return size;
}
@Override
public boolean deleteSingleTicket(final String ticketId) {
final Ticket ticket = getTicket(ticketId);
if (ticket != null) {
return this.ticketIgniteCache.remove(ticket.getId());
}
return true;
}
@Override
public Ticket getTicket(final String ticketIdToGet) {
final String ticketId = encodeTicketId(ticketIdToGet);
if (ticketId == null) {
return null;
}
final Ticket ticket = this.ticketIgniteCache.get(ticketId);
if (ticket == null) {
LOGGER.debug("No ticket by id [{}] is found in the registry", ticketId);
return null;
}
return decodeTicket(ticket);
}
@Override
public Collection<Ticket> getTickets() {
final QueryCursor<Cache.Entry<String, Ticket>> cursor = this.ticketIgniteCache.query(new ScanQuery<>((key, t) -> !t.isExpired()));
return decodeTickets(cursor.getAll().stream().map(Cache.Entry::getValue).collect(toList()));
}
public void setTicketIgniteCache(final IgniteCache<String, Ticket> ticketIgniteCache) {
this.ticketIgniteCache = ticketIgniteCache;
}
@Override
public Ticket updateTicket(final Ticket ticket) {
addTicket(ticket);
return ticket;
}
/**
* Flag to indicate whether this registry instance should participate in reporting its state with
* default value set to {@code true}.
* <p>Therefore, the flag provides a level of flexibility such that depending on the cache and environment
* settings, reporting statistics
* can be set to false and disabled.</p>
*
* @param supportRegistryState true, if the registry is to support registry state
* @see #sessionCount()
* @see #serviceTicketCount()
*/
public void setSupportRegistryState(final boolean supportRegistryState) {
this.supportRegistryState = supportRegistryState;
}
private void configureSecureTransport() {
final String nullKey = "NULL";
if (StringUtils.isNotBlank(properties.getKeyStoreFilePath())
&& StringUtils.isNotBlank(properties.getKeyStorePassword())
&& StringUtils.isNotBlank(properties.getTrustStoreFilePath())
&& StringUtils.isNotBlank(properties.getTrustStorePassword())) {
final SslContextFactory sslContextFactory = new SslContextFactory();
sslContextFactory.setKeyStoreFilePath(properties.getKeyStoreFilePath());
sslContextFactory.setKeyStorePassword(properties.getKeyStorePassword().toCharArray());
if (nullKey.equals(properties.getTrustStoreFilePath()) && nullKey.equals(properties.getTrustStorePassword())) {
sslContextFactory.setTrustManagers(SslContextFactory.getDisabledTrustManager());
} else {
sslContextFactory.setTrustStoreFilePath(properties.getTrustStoreFilePath());
sslContextFactory.setTrustStorePassword(properties.getKeyStorePassword().toCharArray());
}
if (StringUtils.isNotBlank(properties.getKeyAlgorithm())) {
sslContextFactory.setKeyAlgorithm(properties.getKeyAlgorithm());
}
if (StringUtils.isNotBlank(properties.getProtocol())) {
sslContextFactory.setProtocol(properties.getProtocol());
}
if (StringUtils.isNotBlank(properties.getTrustStoreType())) {
sslContextFactory.setTrustStoreType(properties.getTrustStoreType());
}
if (StringUtils.isNotBlank(properties.getKeyStoreType())) {
sslContextFactory.setKeyStoreType(properties.getKeyStoreType());
}
this.igniteConfiguration.setSslContextFactory(sslContextFactory);
}
}
/**
* Init.
*/
@PostConstruct
public void init() {
LOGGER.info("Setting up Ignite Ticket Registry...");
configureSecureTransport();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("igniteConfiguration.cacheConfiguration=[{}]", (Object[]) this.igniteConfiguration.getCacheConfiguration());
LOGGER.debug("igniteConfiguration.getDiscoverySpi=[{}]", this.igniteConfiguration.getDiscoverySpi());
LOGGER.debug("igniteConfiguration.getSslContextFactory=[{}]", this.igniteConfiguration.getSslContextFactory());
}
if (Ignition.state() == IgniteState.STOPPED) {
this.ignite = Ignition.start(this.igniteConfiguration);
} else if (Ignition.state() == IgniteState.STARTED) {
this.ignite = Ignition.ignite();
}
this.ticketIgniteCache = this.ignite.getOrCreateCache(properties.getTicketsCache().getCacheName());
}
/**
* Make sure we shutdown Ignite when the context is destroyed.
*/
@PreDestroy
public void shutdown() {
Ignition.stopAll(true);
}
@Override
public String toString() {
return new ToStringBuilder(this)
.appendSuper(super.toString())
.append("igniteConfiguration", properties)
.append("supportRegistryState", this.supportRegistryState)
.toString();
}
}