package org.apereo.cas.ticket.registry;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient;
import com.amazonaws.services.dynamodbv2.model.AttributeDefinition;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.CreateTableRequest;
import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest;
import com.amazonaws.services.dynamodbv2.model.DeleteItemResult;
import com.amazonaws.services.dynamodbv2.model.DeleteTableRequest;
import com.amazonaws.services.dynamodbv2.model.DescribeTableRequest;
import com.amazonaws.services.dynamodbv2.model.GetItemRequest;
import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
import com.amazonaws.services.dynamodbv2.model.KeyType;
import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;
import com.amazonaws.services.dynamodbv2.model.PutItemRequest;
import com.amazonaws.services.dynamodbv2.model.PutItemResult;
import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType;
import com.amazonaws.services.dynamodbv2.model.ScanRequest;
import com.amazonaws.services.dynamodbv2.model.ScanResult;
import com.amazonaws.services.dynamodbv2.model.TableDescription;
import com.amazonaws.services.dynamodbv2.util.TableUtils;
import org.apache.commons.lang3.SerializationUtils;
import org.apereo.cas.configuration.model.support.dynamodb.DynamoDbTicketRegistryProperties;
import org.apereo.cas.ticket.Ticket;
import org.apereo.cas.ticket.TicketCatalog;
import org.apereo.cas.ticket.TicketDefinition;
import org.jooq.lambda.Unchecked;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
* This is {@link DynamoDbTicketRegistryFacilitator}.
*
* @author Misagh Moayyed
* @since 5.1.0
*/
public class DynamoDbTicketRegistryFacilitator {
private static final Logger LOGGER = LoggerFactory.getLogger(DynamoDbTicketRegistryFacilitator.class);
private enum ColumnNames {
ID("id"),
PREFIX("prefix"),
CREATION_TIME("creationTime"),
COUNT_OF_USES("countOfUses"),
TIME_TO_LIVE("timeToLive"),
TIME_TO_IDLE("timeToIdle"),
ENCODED("encoded");
private final String name;
ColumnNames(final String name) {
this.name = name;
}
public String getName() {
return name;
}
}
private final TicketCatalog ticketCatalog;
private final DynamoDbTicketRegistryProperties dynamoDbProperties;
private final AmazonDynamoDBClient amazonDynamoDBClient;
public DynamoDbTicketRegistryFacilitator(final TicketCatalog ticketCatalog,
final DynamoDbTicketRegistryProperties dynamoDbProperties,
final AmazonDynamoDBClient amazonDynamoDBClient) {
this.ticketCatalog = ticketCatalog;
this.dynamoDbProperties = dynamoDbProperties;
this.amazonDynamoDBClient = amazonDynamoDBClient;
createTicketTables(dynamoDbProperties.isDropTablesOnStartup());
}
/**
* Delete.
*
* @param ticketId the ticket id
* @return the boolean
*/
public boolean delete(final String ticketId) {
final TicketDefinition metadata = this.ticketCatalog.find(ticketId);
if (metadata != null) {
final DeleteItemRequest del = new DeleteItemRequest()
.withTableName(metadata.getProperties().getStorageName())
.withKey(Collections.singletonMap(ColumnNames.ID.getName(), new AttributeValue(ticketId)));
LOGGER.debug("Submitting delete request [{}] for ticket [{}]", del, ticketId);
final DeleteItemResult res = amazonDynamoDBClient.deleteItem(del);
LOGGER.debug("Delete request came back with result [{}]", res);
return res != null;
}
return false;
}
/**
* Delete all.
*
* @return the int
*/
public int deleteAll() {
final AtomicInteger count = new AtomicInteger();
final Collection<TicketDefinition> metadata = this.ticketCatalog.findAll();
metadata.forEach(r -> {
final ScanRequest scan = new ScanRequest(r.getProperties().getStorageName());
LOGGER.debug("Submitting scan request [{}] to table [{}]", scan, r.getProperties().getStorageName());
count.addAndGet(this.amazonDynamoDBClient.scan(scan).getCount());
});
createTicketTables(true);
return count.get();
}
/**
* Gets all.
*
* @return the all
*/
public Collection<Ticket> getAll() {
final Collection<Ticket> tickets = new ArrayList<>();
final Collection<TicketDefinition> metadata = this.ticketCatalog.findAll();
metadata.forEach(r -> {
final ScanRequest scan = new ScanRequest(r.getProperties().getStorageName());
LOGGER.debug("Scanning table with request [{}]", scan);
final ScanResult result = this.amazonDynamoDBClient.scan(scan);
LOGGER.debug("Scanned table with result [{}]", scan);
tickets.addAll(result.getItems()
.stream()
.map(DynamoDbTicketRegistryFacilitator::deserializeTicket)
.collect(Collectors.toList()));
});
return tickets;
}
/**
* Get ticket.
*
* @param ticketId the ticket id
* @return the ticket
*/
public Ticket get(final String ticketId) {
final TicketDefinition metadata = this.ticketCatalog.find(ticketId);
if (metadata != null) {
final Map<String, AttributeValue> keys = new HashMap<>();
keys.put(ColumnNames.ID.getName(), new AttributeValue(ticketId));
final GetItemRequest request = new GetItemRequest()
.withKey(keys)
.withTableName(metadata.getProperties().getStorageName());
LOGGER.debug("Submitting request [{}] to get ticket item [{}]", request, ticketId);
final Map<String, AttributeValue> returnItem = amazonDynamoDBClient.getItem(request).getItem();
if (returnItem != null) {
final Ticket ticket = deserializeTicket(returnItem);
LOGGER.debug("Located ticket [{}]", ticket);
return ticket;
}
} else {
LOGGER.warn("No ticket definition could be found in the catalog to match [{}]", ticketId);
}
return null;
}
private static Ticket deserializeTicket(final Map<String, AttributeValue> returnItem) {
final ByteBuffer bb = returnItem.get(ColumnNames.ENCODED.getName()).getB();
LOGGER.debug("Located binary encoding of ticket item [{}]. Transforming item into ticket object", returnItem);
return SerializationUtils.deserialize(bb.array());
}
/**
* Put ticket.
*
* @param ticket the ticket
* @param encodedTicket the encoded ticket
*/
public void put(final Ticket ticket, final Ticket encodedTicket) {
final TicketDefinition metadata = this.ticketCatalog.find(ticket);
final Map<String, AttributeValue> values = buildTableAttributeValuesMapFromTicket(ticket, encodedTicket);
LOGGER.debug("Adding ticket id [{}] with attribute values [{}]", encodedTicket.getId(), values);
final PutItemRequest putItemRequest = new PutItemRequest(metadata.getProperties().getStorageName(), values);
LOGGER.debug("Submitting put request [{}] for ticket id [{}]", putItemRequest, encodedTicket.getId());
final PutItemResult putItemResult = amazonDynamoDBClient.putItem(putItemRequest);
LOGGER.debug("Ticket added with result [{}]", putItemResult);
getAll();
}
/**
* Create ticket tables.
*
* @param deleteTables the delete tables
*/
public void createTicketTables(final boolean deleteTables) {
final Collection<TicketDefinition> metadata = this.ticketCatalog.findAll();
metadata.forEach(Unchecked.consumer(r -> {
final CreateTableRequest request = new CreateTableRequest()
.withAttributeDefinitions(new AttributeDefinition(ColumnNames.ID.getName(), ScalarAttributeType.S))
.withKeySchema(new KeySchemaElement(ColumnNames.ID.getName(), KeyType.HASH))
.withProvisionedThroughput(new ProvisionedThroughput(dynamoDbProperties.getReadCapacity(),
dynamoDbProperties.getWriteCapacity()))
.withTableName(r.getProperties().getStorageName());
if (deleteTables) {
final DeleteTableRequest delete = new DeleteTableRequest(r.getProperties().getStorageName());
LOGGER.debug("Sending delete request [{}] to remove table if necessary", delete);
TableUtils.deleteTableIfExists(amazonDynamoDBClient, delete);
}
LOGGER.debug("Sending delete request [{}] to create table", request);
TableUtils.createTableIfNotExists(amazonDynamoDBClient, request);
LOGGER.debug("Waiting until table [{}] becomes active...", request.getTableName());
TableUtils.waitUntilActive(amazonDynamoDBClient, request.getTableName());
final DescribeTableRequest describeTableRequest = new DescribeTableRequest().withTableName(request.getTableName());
LOGGER.debug("Sending request [{}] to obtain table description...", describeTableRequest);
final TableDescription tableDescription = amazonDynamoDBClient.describeTable(describeTableRequest).getTable();
LOGGER.debug("Located newly created table with description: [{}]", tableDescription);
}));
}
/**
* Build table attribute values from ticket map.
*
* @param ticket the ticket
* @param encTicket the encoded ticket
* @return the map
*/
public Map<String, AttributeValue> buildTableAttributeValuesMapFromTicket(final Ticket ticket, final Ticket encTicket) {
final Map<String, AttributeValue> values = new HashMap<>();
values.put(ColumnNames.ID.getName(), new AttributeValue(encTicket.getId()));
values.put(ColumnNames.PREFIX.getName(), new AttributeValue(encTicket.getPrefix()));
values.put(ColumnNames.CREATION_TIME.getName(), new AttributeValue(ticket.getCreationTime().toString()));
values.put(ColumnNames.COUNT_OF_USES.getName(), new AttributeValue().withN(Integer.toString(ticket.getCountOfUses())));
values.put(ColumnNames.TIME_TO_LIVE.getName(), new AttributeValue().withN(Long.toString(ticket.getExpirationPolicy().getTimeToLive())));
values.put(ColumnNames.TIME_TO_IDLE.getName(), new AttributeValue().withN(Long.toString(ticket.getExpirationPolicy().getTimeToIdle())));
values.put(ColumnNames.ENCODED.getName(), new AttributeValue().withB(ByteBuffer.wrap(SerializationUtils.serialize(encTicket))));
LOGGER.debug("Created attribute values [{}] based on provided ticket [{}]", values, encTicket.getId());
return values;
}
}