Skip to content

Commit

Permalink
disable pass when ticket is released or reassigned
Browse files Browse the repository at this point in the history
  • Loading branch information
cbellone committed Apr 16, 2023
1 parent ab5fb82 commit 69fb19c
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 17 deletions.
10 changes: 9 additions & 1 deletion src/main/java/alfio/manager/TicketReservationManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import alfio.model.result.WarningMessage;
import alfio.model.subscription.*;
import alfio.model.system.command.FinalizeReservation;
import alfio.model.system.command.InvalidateAccess;
import alfio.model.transaction.*;
import alfio.model.transaction.capabilities.OfflineProcessor;
import alfio.model.transaction.capabilities.ServerInitiatedTransaction;
Expand Down Expand Up @@ -1375,7 +1376,9 @@ public void updateTicketOwner(Ticket ticket,
}
extensionManager.handleTicketAssignment(newTicket, ticketCategoryRepository.getById(ticket.getCategoryId()), updateTicketOwner.getAdditional());


if (isTicketBeingReassigned(ticket, updateTicketOwner, event)) {
invalidateAccess(event, ticket);
}

Ticket postUpdateTicket = ticketRepository.findByUUID(ticket.getUuid());
Map<String, String> postUpdateTicketFields = ticketFieldRepository.findAllByTicketId(ticket.getId()).stream().collect(Collectors.toMap(TicketFieldValue::getName, TicketFieldValue::getValue));
Expand Down Expand Up @@ -1585,6 +1588,7 @@ public void releaseTicket(Event event, TicketReservation ticketReservation, fina
throw new IllegalStateException("Cannot release reserved tickets");
}
//
invalidateAccess(event, ticket);

String reservationId = ticketReservation.getId();
//#365 - reset UUID when releasing a ticket
Expand Down Expand Up @@ -1621,6 +1625,10 @@ public void releaseTicket(Event event, TicketReservation ticketReservation, fina
extensionManager.handleTicketCancelledForEvent(event, Collections.singletonList(ticket.getUuid()));
}

private void invalidateAccess(Event event, Ticket ticket) {
applicationEventPublisher.publishEvent(new InvalidateAccess(ticket, ticketRepository.getTicketMetadata(ticket.getId()), event));
}

int getReservationTimeout(Configurable configurable) {
return configurationManager.getFor(RESERVATION_TIMEOUT, configurable.getConfigurationLevel()).getValueAsIntOrDefault(25);
}
Expand Down
94 changes: 78 additions & 16 deletions src/main/java/alfio/manager/wallet/GoogleWalletManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,27 @@

import alfio.manager.system.ConfigurationManager;
import alfio.model.*;
import alfio.model.metadata.TicketMetadata;
import alfio.model.metadata.TicketMetadataContainer;
import alfio.model.system.ConfigurationKeys;
import alfio.model.user.Organization;
import alfio.repository.EventDescriptionRepository;
import alfio.repository.EventRepository;
import alfio.repository.TicketCategoryRepository;
import alfio.repository.TicketRepository;
import alfio.repository.user.OrganizationRepository;
import alfio.model.system.command.InvalidateAccess;
import alfio.repository.*;
import alfio.util.HttpUtils;
import alfio.util.MustacheCustomTag;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.io.ByteArrayInputStream;
import java.io.IOException;
Expand All @@ -55,9 +57,11 @@

@Component
@AllArgsConstructor
@Log4j2
@Transactional
public class GoogleWalletManager {

private static final Logger log = LoggerFactory.getLogger(GoogleWalletManager.class);
private static final String WALLET_OBJECT_ID = "gWalletObjectId";
private final EventRepository eventRepository;
private final ConfigurationManager configurationManager;
private final EventDescriptionRepository eventDescriptionRepository;
Expand All @@ -66,6 +70,7 @@ public class GoogleWalletManager {
private final HttpClient httpClient;
private final ObjectMapper objectMapper;
private final Environment environment;
private final AuditingRepository auditingRepository;

public Optional<Pair<EventAndOrganizationId, Ticket>> validateTicket(String eventName, String ticketUuid) {
var eventOptional = eventRepository.findOptionalEventAndOrganizationIdByShortName(eventName);
Expand All @@ -89,6 +94,41 @@ public String createAddToWalletUrl(Ticket ticket, EventAndOrganizationId event)
}
}

@EventListener
public void invalidateAccessForTicket(InvalidateAccess invalidateAccess) {
try {
Map<ConfigurationKeys, String> passConf = getConfigurationKeys(invalidateAccess.getEvent());
if (!passConf.isEmpty()) {
var objectIdOptional = invalidateAccess.getTicketMetadataContainer()
.getMetadataForKey(TicketMetadataContainer.GENERAL)
.map(m -> m.getAttributes().get(WALLET_OBJECT_ID));
if (objectIdOptional.isPresent()) {
invalidateObject(invalidateAccess.getTicket().getUuid(), objectIdOptional.get(), passConf);
}
}
} catch (Exception e) {
log.warn("Error while invalidating access for ticket " + invalidateAccess.getTicket().getUuid(), e);
}
}

private void invalidateObject(String ticketId, String objectId, Map<ConfigurationKeys, String> passConf) throws IOException, InterruptedException {
log.trace("Invalidating access to object ID: {}", objectId);
var credentials = retrieveCredentials(passConf.get(WALLET_SERVICE_ACCOUNT_KEY));
URI uriWithId = URI.create(String.format("%s/%s", EventTicketObject.WALLET_URL, objectId));
HttpRequest expireRequest = HttpRequest.newBuilder()
.uri(uriWithId)
.header("Authorization", String.format("Bearer %s", credentials.refreshAccessToken().getTokenValue()))
.method("PATCH", HttpRequest.BodyPublishers.ofString("{\"state\":\"INACTIVE\"}"))
//.DELETE()
.build();
var response = httpClient.send(expireRequest, HttpResponse.BodyHandlers.ofString());
if (HttpUtils.callSuccessful(response)) {
log.debug("Access invalidated for ticket {}", ticketId);
} else {
log.warn("Cannot invalidate access for ticket {}, response: {}", ticketId, response.body());
}
}

private Map<ConfigurationKeys, String> getConfigurationKeys(EventAndOrganizationId event) {
var conf = configurationManager.getFor(Set.of(
ENABLE_WALLET,
Expand Down Expand Up @@ -150,31 +190,53 @@ private String buildWalletPassUrl(Ticket ticket,
.end(ticketValidityEnd)
.build();

String walletTicketId = formatEventTicketObjectId(ticket, event, issuerId, host);
var eventTicketObject = EventTicketObject.builder()
.id(formatEventTicketObjectId(ticket, event, issuerId, host))
.id(walletTicketId)
.classId(eventTicketClass.getId())
.ticketHolderName(ticket.getFullName())
.ticketNumber(ticket.getUuid())
.barcode(ticket.ticketCode(event.getPrivateKey()))
.build();

GoogleCredentials credentials = null;
GoogleCredentials credentials = retrieveCredentials(serviceAccountKey);

createEventClass(credentials, eventTicketClass, overwritePreviousClassesAndEvents);
String eventObjectId = createEventObject(credentials, eventTicketObject, overwritePreviousClassesAndEvents);
String walletPassUrl = generateWalletPassUrl(credentials, eventObjectId, baseUrl);
persistPassId(ticket, eventObjectId);
return walletPassUrl;
}

private static GoogleCredentials retrieveCredentials(String serviceAccountKey) {
try {
credentials = GoogleCredentials
return GoogleCredentials
.fromStream(new ByteArrayInputStream(serviceAccountKey.getBytes(StandardCharsets.UTF_8)))
.createScoped(Collections.singleton("https://www.googleapis.com/auth/wallet_object.issuer"));
} catch (IOException e) {
throw new GoogleWalletException("Unable to retrieve Service Account Credentials from configuration", e);
}
}

createEventClass(credentials, eventTicketClass, overwritePreviousClassesAndEvents);
String eventObjectId = createEventObject(credentials, eventTicketObject, overwritePreviousClassesAndEvents);

return generateWalletPassUrl(credentials, eventObjectId, baseUrl);
private void persistPassId(Ticket ticket, String eventObjectId) {
var metadataContainer = ticketRepository.getTicketMetadata(ticket.getId());
var existingMetadata = metadataContainer.getMetadataForKey(TicketMetadataContainer.GENERAL);
var attributesMap = existingMetadata.map(ticketMetadata -> new HashMap<>(ticketMetadata.getAttributes()))
.orElseGet(HashMap::new);
attributesMap.put(WALLET_OBJECT_ID, eventObjectId);
metadataContainer.putMetadata(TicketMetadataContainer.GENERAL, existingMetadata.map(tm -> tm.withAttributes(attributesMap)).orElseGet(() -> new TicketMetadata(null, null, attributesMap)));
ticketRepository.updateTicketMetadata(ticket.getId(), metadataContainer);
}

private String formatEventTicketObjectId(Ticket ticket, Event event, String issuerId, String host) {
return String.format("%s.%s-%s-object.%s-%s", issuerId, walletIdPrefix(), host, event.getShortName().replaceAll("[^\\w.-]", "_"), ticket.getUuid());
return String.format("%s.%s-%s-object.%s-%s",
issuerId,
walletIdPrefix(),
host,
event.getShortName().replaceAll("[^\\w.-]", "_"),
// if the attendee gives their ticket to somebody else, the new ticket holder must be able to add their ticket to the wallet
// therefore we add count(audit(UPDATE_TICKET)) as suffix for the ticket UUID
ticket.getUuid()+ "_" + auditingRepository.countAuditsOfTypeForTicket(ticket.getTicketsReservationId(), ticket.getId(), Audit.EventType.UPDATE_TICKET));
}

private String formatEventTicketClassId(Event event, String issuerId, TicketCategory category, String host) {
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/alfio/model/metadata/TicketMetadata.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ public int hashCode() {
public static TicketMetadata empty() {
return new TicketMetadata(null, null, Map.of());
}
public TicketMetadata withAttributes(Map<String, String> attributes) {
return new TicketMetadata(joinLink, Map.copyOf(linkDescription), Map.copyOf(attributes));
}

public static TicketMetadata copyOf(TicketMetadata src) {
if (src != null) {
Expand Down
48 changes: 48 additions & 0 deletions src/main/java/alfio/model/system/command/InvalidateAccess.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* This file is part of alf.io.
*
* alf.io is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* alf.io is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with alf.io. If not, see <http://www.gnu.org/licenses/>.
*/
package alfio.model.system.command;

import alfio.model.Event;
import alfio.model.Ticket;
import alfio.model.metadata.TicketMetadataContainer;

/**
* Signals that access for the ticket must be invalidated on external systems
*/
public class InvalidateAccess {
private final Ticket ticket;
private final TicketMetadataContainer ticketMetadataContainer;
private final Event event;

public InvalidateAccess(Ticket ticket, TicketMetadataContainer ticketMetadataContainer, Event event) {
this.ticket = ticket;
this.ticketMetadataContainer = ticketMetadataContainer;
this.event = event;
}

public Ticket getTicket() {
return ticket;
}

public TicketMetadataContainer getTicketMetadataContainer() {
return ticketMetadataContainer;
}

public Event getEvent() {
return event;
}
}
5 changes: 5 additions & 0 deletions src/main/java/alfio/repository/AuditingRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ default int insert(String reservationId, Integer userId, PurchaseContext p, Audi
@Query("select count(*) from auditing_user where reservation_id = :reservationId and event_type = :eventType")
Integer countAuditsOfTypeForReservation(@Bind("reservationId") String reservationId, @Bind("eventType") Audit.EventType eventType);

@Query("select count(*) from auditing_user where reservation_id = :reservationId and entity_id = :ticketId::text and event_type = :eventType")
Integer countAuditsOfTypeForTicket(@Bind("reservationId") String reservationId,
@Bind("ticketId") int ticketId,
@Bind("eventType") Audit.EventType eventType);

@Query("select count(*) from auditing_user where reservation_id = :reservationId and event_type in (:eventTypes) and date_trunc('day', :referenceDate::timestamp) = date_trunc('day', event_time)")
Integer countAuditsOfTypesInTheSameDay(@Bind("reservationId") String reservationId, @Bind("eventTypes") Collection<String> eventTypes, @Bind("referenceDate") ZonedDateTime date);

Expand Down

0 comments on commit 69fb19c

Please sign in to comment.