package org.apereo.cas.ticket; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apereo.cas.authentication.Authentication; import org.apereo.cas.authentication.principal.Service; import org.apereo.cas.ticket.proxy.ProxyGrantingTicket; import org.springframework.util.Assert; import javax.persistence.Column; import javax.persistence.DiscriminatorColumn; import javax.persistence.DiscriminatorValue; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.Lob; import javax.persistence.ManyToOne; import javax.persistence.OneToMany; import javax.persistence.Table; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Concrete implementation of a TicketGrantingTicket. A TicketGrantingTicket is * the global identifier of a principal into the system. It grants the Principal * single-sign on access to any service that opts into single-sign on. * Expiration of a TicketGrantingTicket is controlled by the ExpirationPolicy * specified as object creation. * * @author Scott Battaglia * @since 3.0.0 */ @Entity @Table(name = "TICKETGRANTINGTICKET") @DiscriminatorColumn(name = "TYPE") @DiscriminatorValue(TicketGrantingTicket.PREFIX) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) public class TicketGrantingTicketImpl extends AbstractTicket implements TicketGrantingTicket { /** * Unique Id for serialization. */ private static final long serialVersionUID = -8608149809180911599L; /** * The authenticated object for which this ticket was generated for. */ @Lob @Column(name = "AUTHENTICATION", nullable = false, length = Integer.MAX_VALUE) private Authentication authentication; /** * Flag to enforce manual expiration. */ @Column(name = "EXPIRED", nullable = false) private Boolean expired = Boolean.FALSE; /** * Service that produced a proxy-granting ticket. */ @Lob @Column(name = "PROXIED_BY", nullable = true, length = Integer.MAX_VALUE) private Service proxiedBy; /** * The services associated to this ticket. */ @Lob @Column(name = "SERVICES_GRANTED_ACCESS_TO", nullable = false, length = Integer.MAX_VALUE) private HashMap<String, Service> services = new HashMap<>(); /** * The {@link TicketGrantingTicket} this is associated with. */ @ManyToOne(targetEntity = TicketGrantingTicketImpl.class) private TicketGrantingTicket ticketGrantingTicket; /** * The PGTs associated to this ticket. */ @OneToMany(targetEntity = TicketGrantingTicketImpl.class, mappedBy = "ticketGrantingTicket", fetch = FetchType.EAGER) @JsonIgnore private Set<ProxyGrantingTicket> proxyGrantingTickets = new HashSet<>(); /** * The ticket ids which are tied to this ticket. */ @Lob @Column(name = "DESCENDANT_TICKETS", nullable = false, length = Integer.MAX_VALUE) private HashSet<String> descendantTickets = new HashSet<>(); /** * Instantiates a new ticket granting ticket impl. */ public TicketGrantingTicketImpl() { } /** * Constructs a new TicketGrantingTicket. * May throw an {@link IllegalArgumentException} if the Authentication object is null. * * @param id the id of the Ticket * @param proxiedBy Service that produced this proxy ticket. * @param parentTicketGrantingTicket the parent ticket * @param authentication the Authentication request for this ticket * @param policy the expiration policy for this ticket. */ @JsonCreator public TicketGrantingTicketImpl(@JsonProperty("id") final String id, @JsonProperty("proxiedBy") final Service proxiedBy, @JsonProperty("grantingTicket") final TicketGrantingTicket parentTicketGrantingTicket, @JsonProperty("authentication") final Authentication authentication, @JsonProperty("expirationPolicy") final ExpirationPolicy policy) { super(id, policy); if (parentTicketGrantingTicket != null && proxiedBy == null) { throw new IllegalArgumentException("Must specify proxiedBy when providing parent TGT"); } Assert.notNull(authentication, "authentication cannot be null"); this.ticketGrantingTicket = parentTicketGrantingTicket; this.authentication = authentication; this.proxiedBy = proxiedBy; } /** * Constructs a new TicketGrantingTicket without a parent * TicketGrantingTicket. * * @param id the id of the Ticket * @param authentication the Authentication request for this ticket * @param policy the expiration policy for this ticket. */ public TicketGrantingTicketImpl(final String id, final Authentication authentication, final ExpirationPolicy policy) { this(id, null, null, authentication, policy); } @Override public TicketGrantingTicket getGrantingTicket() { return this.ticketGrantingTicket; } @Override public Authentication getAuthentication() { return this.authentication; } /** * {@inheritDoc} * <p>The state of the ticket is affected by this operation and the * ticket will be considered used. The state update subsequently may * impact the ticket expiration policy in that, depending on the policy * configuration, the ticket may be considered expired. */ @Override public synchronized ServiceTicket grantServiceTicket(final String id, final Service service, final ExpirationPolicy expirationPolicy, final boolean credentialProvided, final boolean onlyTrackMostRecentSession) { final ServiceTicket serviceTicket = new ServiceTicketImpl(id, this, service, credentialProvided, expirationPolicy); trackServiceSession(serviceTicket.getId(), service, onlyTrackMostRecentSession); return serviceTicket; } /** * Update service and track session. * * @param id the id * @param service the service * @param onlyTrackMostRecentSession the only track most recent session */ protected void trackServiceSession(final String id, final Service service, final boolean onlyTrackMostRecentSession) { update(); service.setPrincipal(getRoot().getAuthentication().getPrincipal()); if (onlyTrackMostRecentSession) { final String path = normalizePath(service); final Collection<Service> existingServices = this.services.values(); // loop on existing services existingServices.stream() .filter(existingService -> path.equals(normalizePath(existingService))) .findFirst().ifPresent(existingServices::remove); } this.services.put(id, service); } /** * Normalize the path of a service by removing the query string and everything after a semi-colon. * * @param service the service to normalize * @return the normalized path */ private static String normalizePath(final Service service) { String path = service.getId(); path = StringUtils.substringBefore(path, "?"); path = StringUtils.substringBefore(path, ";"); path = StringUtils.substringBefore(path, "#"); return path; } /** * Gets an new map with the service ticket and services accessed by this ticket-granting ticket. * * @return a map of service ticket and services accessed by this ticket-granting ticket. */ @Override public synchronized Map<String, Service> getServices() { return new HashMap<>(this.services); } @Override public Collection<ProxyGrantingTicket> getProxyGrantingTickets() { return this.proxyGrantingTickets; } /** * Remove all services of the TGT (at logout). */ @Override public void removeAllServices() { this.services.clear(); } /** * Return if the TGT has no parent. * * @return if the TGT has no parent. */ @Override public boolean isRoot() { return this.getGrantingTicket() == null; } @Override public void markTicketExpired() { this.expired = Boolean.TRUE; } @JsonIgnore @Override public TicketGrantingTicket getRoot() { final TicketGrantingTicket parent = getGrantingTicket(); if (parent == null) { return this; } return parent.getRoot(); } /** * Return if the TGT is expired. * * @return if the TGT is expired. */ @Override public boolean isExpiredInternal() { return this.expired; } @JsonIgnore @Override public List<Authentication> getChainedAuthentications() { final List<Authentication> list = new ArrayList<>(); list.add(getAuthentication()); if (getGrantingTicket() == null) { return Collections.unmodifiableList(list); } list.addAll(getGrantingTicket().getChainedAuthentications()); return Collections.unmodifiableList(list); } @Override public Service getProxiedBy() { return this.proxiedBy; } @Override public boolean equals(final Object object) { if (object == null) { return false; } if (object == this) { return true; } if (!(object instanceof TicketGrantingTicket)) { return false; } final Ticket ticket = (Ticket) object; return new EqualsBuilder() .append(ticket.getId(), this.getId()) .isEquals(); } @Override public String getPrefix() { return TicketGrantingTicket.PREFIX; } @Override public Collection getDescendantTickets() { return descendantTickets; } }