package org.apereo.cas.ticket.registry; import com.couchbase.client.java.document.SerializableDocument; import com.couchbase.client.java.view.DefaultView; import com.couchbase.client.java.view.View; import com.couchbase.client.java.view.ViewQuery; import com.couchbase.client.java.view.ViewResult; import com.couchbase.client.java.view.ViewRow; import com.google.common.base.Throwables; import org.apereo.cas.couchbase.core.CouchbaseClientFactory; import org.apereo.cas.ticket.ServiceTicket; import org.apereo.cas.ticket.Ticket; import org.apereo.cas.ticket.TicketGrantingTicket; import org.apereo.cas.ticket.accesstoken.AccessToken; import org.apereo.cas.ticket.code.OAuthCode; import org.apereo.cas.ticket.proxy.ProxyGrantingTicket; import org.apereo.cas.ticket.proxy.ProxyTicket; import org.apereo.cas.ticket.refreshtoken.RefreshToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.PreDestroy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Spliterator; import java.util.Spliterators; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import java.util.stream.StreamSupport; /** * A Ticket Registry storage backend which uses the memcached protocol. * CouchBase is a multi host NoSQL database with a memcached interface * to persistent storage which also is quite usable as a replicated * ticket storage engine for multiple front end CAS servers. * * @author Fredrik Jönsson "fjo@kth.se" * @author Misagh Moayyed * @since 4.2.0 */ public class CouchbaseTicketRegistry extends AbstractTicketRegistry { private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseTicketRegistry.class); private static final long MAX_EXP_TIME_IN_DAYS = 30; private static final String END_TOKEN = "\u02ad"; private static final String VIEW_NAME_ALL_TICKETS = "all_tickets"; private static final View ALL_TICKETS_VIEW = DefaultView.create( VIEW_NAME_ALL_TICKETS, "function(d,m) {emit(m.id);}", "_count"); private static final List<View> ALL_VIEWS = Arrays.asList(new View[]{ALL_TICKETS_VIEW}); private static final String UTIL_DOCUMENT = "statistics"; private final CouchbaseClientFactory couchbase; public CouchbaseTicketRegistry(final CouchbaseClientFactory couchbase, final boolean isQueryEnabled) { this.couchbase = couchbase; LOGGER.info("Setting up Couchbase Ticket Registry instance"); System.setProperty("com.couchbase.queryEnabled", Boolean.toString(isQueryEnabled)); LOGGER.debug("Setting up indexes on document [{}] and views [{}]", UTIL_DOCUMENT, ALL_VIEWS); this.couchbase.ensureIndexes(UTIL_DOCUMENT, ALL_VIEWS); LOGGER.info("Initializing Couchbase..."); this.couchbase.initialize(); LOGGER.info("Initialized Couchbase bucket [{}]", this.couchbase.bucket().name()); } @Override public Ticket updateTicket(final Ticket ticket) { LOGGER.debug("Updating ticket [{}]", ticket); try { final SerializableDocument document = SerializableDocument.create(ticket.getId(), getTimeToLive(ticket), ticket); LOGGER.debug("Upserting document [{}] into couchbase bucket [{}]", document.id(), this.couchbase.bucket().name()); this.couchbase.bucket().upsert(document); } catch (final Exception e) { LOGGER.error("Failed updating [{}]: [{}]", ticket, e); } return ticket; } @Override public void addTicket(final Ticket ticketToAdd) { LOGGER.debug("Adding ticket [{}]", ticketToAdd); try { final Ticket ticket = encodeTicket(ticketToAdd); final SerializableDocument document = SerializableDocument.create(ticket.getId(), getTimeToLive(ticketToAdd), ticket); LOGGER.debug("Created document for ticket [{}]. Upserting into bucket [{}]", ticketToAdd, this.couchbase.bucket().name()); this.couchbase.bucket().upsert(document); } catch (final Exception e) { LOGGER.error("Failed adding [{}]: [{}]", ticketToAdd, e); } } @Override public Ticket getTicket(final String ticketId) { try { LOGGER.debug("Locating ticket id [{}]", ticketId); final String encTicketId = encodeTicketId(ticketId); if (encTicketId == null) { LOGGER.debug("Ticket id [{}] could not be found", ticketId); return null; } final SerializableDocument document = this.couchbase.bucket().get(encTicketId, SerializableDocument.class); if (document != null) { final Ticket t = (Ticket) document.content(); LOGGER.debug("Got ticket [{}] from the registry.", t); return t; } LOGGER.debug("Ticket [{}] not found in the registry.", encTicketId); return null; } catch (final Exception e) { LOGGER.error("Failed fetching [{}]: [{}]", ticketId, e); return null; } } /** * Stops the couchbase client. */ @PreDestroy public void destroy() { try { LOGGER.debug("Shutting down Couchbase"); this.couchbase.shutdown(); } catch (final Exception e) { throw Throwables.propagate(e); } } @Override public Collection<Ticket> getTickets() { LOGGER.debug("getTickets() isn't supported. Returning empty list"); return new ArrayList<>(); } @Override public long sessionCount() { return runQuery(TicketGrantingTicket.PREFIX + '-'); } @Override public long serviceTicketCount() { return runQuery(ServiceTicket.PREFIX + '-'); } @Override public boolean deleteSingleTicket(final String ticketId) { LOGGER.debug("Deleting ticket [{}]", ticketId); try { return this.couchbase.bucket().remove(ticketId) != null; } catch (final Exception e) { LOGGER.error("Failed deleting [{}]: [{}]", ticketId, e); return false; } } @Override public long deleteAll() { final Iterator<ViewRow> grantingTicketsIt = getViewResultIteratorForPrefixedTickets(TicketGrantingTicket.PREFIX + '-').iterator(); final Iterator<ViewRow> serviceTicketsIt = getViewResultIteratorForPrefixedTickets(ServiceTicket.PREFIX + '-').iterator(); final Iterator<ViewRow> proxyTicketsIt = getViewResultIteratorForPrefixedTickets(ProxyTicket.PREFIX + '-').iterator(); final Iterator<ViewRow> proxyGrantingTicketsIt = getViewResultIteratorForPrefixedTickets(ProxyGrantingTicket.PREFIX + '-').iterator(); final Iterator<ViewRow> accessTokenIt = getViewResultIteratorForPrefixedTickets(AccessToken.PREFIX + '-').iterator(); final Iterator<ViewRow> oauthcodeIt = getViewResultIteratorForPrefixedTickets(OAuthCode.PREFIX + '-').iterator(); final Iterator<ViewRow> refreshTokenIt = getViewResultIteratorForPrefixedTickets(RefreshToken.PREFIX + '-').iterator(); final int count = getViewRowCountFromViewResultIterator(grantingTicketsIt) + getViewRowCountFromViewResultIterator(serviceTicketsIt) + getViewRowCountFromViewResultIterator(proxyTicketsIt) + getViewRowCountFromViewResultIterator(proxyGrantingTicketsIt) + getViewRowCountFromViewResultIterator(accessTokenIt) + getViewRowCountFromViewResultIterator(oauthcodeIt) + getViewRowCountFromViewResultIterator(refreshTokenIt); Stream<ViewRow> tickets = StreamSupport.stream(Spliterators.spliteratorUnknownSize(grantingTicketsIt, Spliterator.ORDERED), true); tickets.forEach(t -> this.couchbase.bucket().remove(t.document())); tickets = StreamSupport.stream(Spliterators.spliteratorUnknownSize(serviceTicketsIt, Spliterator.ORDERED), true); tickets.forEach(t -> this.couchbase.bucket().remove(t.document())); tickets = StreamSupport.stream(Spliterators.spliteratorUnknownSize(proxyTicketsIt, Spliterator.ORDERED), true); tickets.forEach(t -> this.couchbase.bucket().remove(t.document())); tickets = StreamSupport.stream(Spliterators.spliteratorUnknownSize(proxyGrantingTicketsIt, Spliterator.ORDERED), true); tickets.forEach(t -> this.couchbase.bucket().remove(t.document())); tickets = StreamSupport.stream(Spliterators.spliteratorUnknownSize(accessTokenIt, Spliterator.ORDERED), true); tickets.forEach(t -> this.couchbase.bucket().remove(t.document())); tickets = StreamSupport.stream(Spliterators.spliteratorUnknownSize(oauthcodeIt, Spliterator.ORDERED), true); tickets.forEach(t -> this.couchbase.bucket().remove(t.document())); tickets = StreamSupport.stream(Spliterators.spliteratorUnknownSize(refreshTokenIt, Spliterator.ORDERED), true); tickets.forEach(t -> this.couchbase.bucket().remove(t.document())); return count; } private int runQuery(final String prefix) { final Iterator<ViewRow> iterator = getViewResultIteratorForPrefixedTickets(prefix).iterator(); return getViewRowCountFromViewResultIterator(iterator); } private static int getViewRowCountFromViewResultIterator(final Iterator<ViewRow> iterator) { if (iterator.hasNext()) { final ViewRow res = iterator.next(); final Integer count = (Integer) res.value(); LOGGER.debug("Found [{}] rows", count); return count; } LOGGER.debug("No rows could be found by the query iterator."); return 0; } private ViewResult getViewResultIteratorForPrefixedTickets(final String prefix) { LOGGER.debug("Running query on document [{}] and view [{}] with prefix [{}]", UTIL_DOCUMENT, VIEW_NAME_ALL_TICKETS, prefix); return this.couchbase.bucket().query( ViewQuery.from(UTIL_DOCUMENT, VIEW_NAME_ALL_TICKETS) .startKey(prefix) .endKey(prefix + END_TOKEN) .reduce()); } /** * Get the expiration policy value of the ticket in seconds. * * @param ticket the ticket * @return the exp value * @see <a href="http://docs.couchbase.com/developer/java-2.0/documents-basics.html">Couchbase Docs</a> */ private static int getTimeToLive(final Ticket ticket) { final int expTime = ticket.getExpirationPolicy().getTimeToLive().intValue(); if (TimeUnit.SECONDS.toDays(expTime) >= MAX_EXP_TIME_IN_DAYS) { LOGGER.warn("Any expiration time larger than [{}] days in seconds is considered absolute (as in a Unix time stamp) " + "anything smaller is considered relative in seconds.", MAX_EXP_TIME_IN_DAYS); } return expTime; } }