package org.apereo.cas;
import com.codahale.metrics.annotation.Counted;
import com.codahale.metrics.annotation.Metered;
import com.codahale.metrics.annotation.Timed;
import org.apereo.cas.authentication.Authentication;
import org.apereo.cas.authentication.AuthenticationServiceSelectionPlan;
import org.apereo.cas.authentication.ContextualAuthenticationPolicy;
import org.apereo.cas.authentication.ContextualAuthenticationPolicyFactory;
import org.apereo.cas.authentication.policy.AcceptAnyAuthenticationPolicyFactory;
import org.apereo.cas.authentication.principal.DefaultPrincipalFactory;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.authentication.principal.Service;
import org.apereo.cas.logout.LogoutManager;
import org.apereo.cas.services.RegisteredService;
import org.apereo.cas.services.ServiceContext;
import org.apereo.cas.services.ServicesManager;
import org.apereo.cas.services.UnauthorizedProxyingException;
import org.apereo.cas.ticket.AbstractTicketException;
import org.apereo.cas.ticket.InvalidTicketException;
import org.apereo.cas.ticket.Ticket;
import org.apereo.cas.ticket.TicketFactory;
import org.apereo.cas.ticket.TicketGrantingTicket;
import org.apereo.cas.ticket.UnsatisfiedAuthenticationPolicyException;
import org.apereo.cas.ticket.registry.TicketRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import java.io.Serializable;
import java.util.Collection;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* An abstract implementation of the {@link CentralAuthenticationService} that provides access to
* the needed scaffolding and services that are necessary to CAS, such as ticket registry, service registry, etc.
* The intention here is to allow extensions to easily benefit these already-configured components
* without having to to duplicate them again.
*
* @author Misagh Moayyed
* @since 4.2.0
*/
public abstract class AbstractCentralAuthenticationService implements CentralAuthenticationService, Serializable,
ApplicationEventPublisherAware {
private static final long serialVersionUID = -7572316677901391166L;
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractCentralAuthenticationService.class);
/**
* Application event publisher.
*/
@Autowired
protected ApplicationEventPublisher applicationEventPublisher;
/**
* {@link TicketRegistry} for storing and retrieving tickets as needed.
*/
protected TicketRegistry ticketRegistry;
/**
* Implementation of Service Manager.
*/
protected ServicesManager servicesManager;
/**
* The logout manager.
**/
protected LogoutManager logoutManager;
/**
* The ticket factory.
**/
protected TicketFactory ticketFactory;
/**
* The service selection strategy during validation events.
**/
protected final AuthenticationServiceSelectionPlan authenticationRequestServiceSelectionStrategies;
/**
* Authentication policy that uses a service context to produce stateful security policies to apply when
* authenticating credentials.
*/
protected ContextualAuthenticationPolicyFactory<ServiceContext> serviceContextAuthenticationPolicyFactory
= new AcceptAnyAuthenticationPolicyFactory();
/**
* Factory to create the principal type.
**/
protected PrincipalFactory principalFactory = new DefaultPrincipalFactory();
/**
* Cipher executor to handle ticket validation.
*/
protected CipherExecutor<String, String> cipherExecutor;
/**
* Build the central authentication service implementation.
*
* @param ticketRegistry the tickets registry.
* @param ticketFactory the ticket factory
* @param servicesManager the services manager.
* @param logoutManager the logout manager.
* @param selectionStrategies the selection strategies
* @param policy Authentication policy uses a service context to create security policies to apply when
* authenticating credentials.
* @param principalFactory principal factory to create principal objects
* @param cipherExecutor Cipher executor to handle ticket validation.
*/
public AbstractCentralAuthenticationService(final TicketRegistry ticketRegistry,
final TicketFactory ticketFactory, final ServicesManager servicesManager,
final LogoutManager logoutManager,
final AuthenticationServiceSelectionPlan selectionStrategies,
final ContextualAuthenticationPolicyFactory<ServiceContext> policy,
final PrincipalFactory principalFactory,
final CipherExecutor<String, String> cipherExecutor) {
this.ticketRegistry = ticketRegistry;
this.servicesManager = servicesManager;
this.logoutManager = logoutManager;
this.ticketFactory = ticketFactory;
this.authenticationRequestServiceSelectionStrategies = selectionStrategies;
this.principalFactory = principalFactory;
this.serviceContextAuthenticationPolicyFactory = policy;
this.cipherExecutor = cipherExecutor;
}
/**
* Publish CAS events.
*
* @param e the event
*/
protected void doPublishEvent(final ApplicationEvent e) {
if (applicationEventPublisher != null) {
LOGGER.debug("Publishing [{}]", e);
this.applicationEventPublisher.publishEvent(e);
}
}
@Transactional(transactionManager = "ticketTransactionManager",
noRollbackFor = InvalidTicketException.class)
@Timed(name = "GET_TICKET_TIMER")
@Metered(name = "GET_TICKET_METER")
@Counted(name = "GET_TICKET_COUNTER", monotonic = true)
@Override
public <T extends Ticket> T getTicket(final String ticketId) throws InvalidTicketException {
Assert.notNull(ticketId, "ticketId cannot be null");
final Ticket ticket = this.ticketRegistry.getTicket(ticketId);
verifyTicketState(ticket, ticketId, null);
return (T) ticket;
}
/**
* {@inheritDoc}
* <p>
* Note:
* Synchronization on ticket object in case of cache based registry doesn't serialize
* access to critical section. The reason is that cache pulls serialized data and
* builds new object, most likely for each pull. Is this synchronization needed here?
*/
@Transactional(transactionManager = "ticketTransactionManager", noRollbackFor = InvalidTicketException.class)
@Timed(name = "GET_TICKET_TIMER")
@Metered(name = "GET_TICKET_METER")
@Counted(name = "GET_TICKET_COUNTER", monotonic = true)
@Override
public <T extends Ticket> T getTicket(final String ticketId, final Class<T> clazz) throws InvalidTicketException {
Assert.notNull(ticketId, "ticketId cannot be null");
final Ticket ticket = this.ticketRegistry.getTicket(ticketId, clazz);
verifyTicketState(ticket, ticketId, clazz);
return (T) ticket;
}
@Transactional(transactionManager = "ticketTransactionManager")
@Timed(name = "GET_TICKETS_TIMER")
@Metered(name = "GET_TICKETS_METER")
@Counted(name = "GET_TICKETS_COUNTER", monotonic = true)
@Override
public Collection<Ticket> getTickets(final Predicate<Ticket> predicate) {
return this.ticketRegistry.getTickets().stream()
.filter(predicate)
.collect(Collectors.toSet());
}
@Transactional(transactionManager = "ticketTransactionManager", readOnly = false)
@Timed(name = "DELETE_TICKET_TIMER")
@Metered(name = "DELETE_TICKET_METER")
@Counted(name = "DELETE_TICKET_COUNTER", monotonic = true)
@Override
public void deleteTicket(final String ticketId) {
this.ticketRegistry.deleteTicket(ticketId);
}
/**
* Gets the authentication satisfied by policy.
*
* @param authentication the authentication
* @param context the context
* @return the authentication satisfied by policy
* @throws AbstractTicketException the ticket exception
*/
protected Authentication getAuthenticationSatisfiedByPolicy(final Authentication authentication, final ServiceContext context)
throws AbstractTicketException {
final ContextualAuthenticationPolicy<ServiceContext> policy = this.serviceContextAuthenticationPolicyFactory.createPolicy(context);
if (policy.isSatisfiedBy(authentication)) {
return authentication;
}
throw new UnsatisfiedAuthenticationPolicyException(policy);
}
/**
* Evaluate proxied service if needed.
*
* @param service the service
* @param ticketGrantingTicket the ticket granting ticket
* @param registeredService the registered service
*/
protected void evaluateProxiedServiceIfNeeded(final Service service, final TicketGrantingTicket ticketGrantingTicket,
final RegisteredService registeredService) {
final Service proxiedBy = ticketGrantingTicket.getProxiedBy();
if (proxiedBy != null) {
LOGGER.debug("TGT is proxied by [{}]. Locating proxy service in registry...", proxiedBy.getId());
final RegisteredService proxyingService = this.servicesManager.findServiceBy(proxiedBy);
if (proxyingService != null) {
LOGGER.debug("Located proxying service [{}] in the service registry", proxyingService);
if (!proxyingService.getProxyPolicy().isAllowedToProxy()) {
LOGGER.warn("Found proxying service [{}], but it is not authorized to fulfill the proxy attempt made by [{}]",
proxyingService.getId(), service.getId());
throw new UnauthorizedProxyingException(UnauthorizedProxyingException.MESSAGE
+ registeredService.getId());
}
} else {
LOGGER.warn("No proxying service found. Proxy attempt by service [{}] (registered service [{}]) is not allowed.",
service.getId(), registeredService.getId());
throw new UnauthorizedProxyingException(UnauthorizedProxyingException.MESSAGE
+ registeredService.getId());
}
} else {
LOGGER.trace("TGT is not proxied by another service");
}
}
/**
* Validate ticket expiration policy and throws exception if ticket is no longer valid.
* Expired tickets are also deleted from the registry immediately on demand.
*
* @param ticket the ticket
* @param id the original id
* @param clazz the clazz
*/
protected void verifyTicketState(final Ticket ticket, final String id, final Class clazz) {
if (ticket == null) {
LOGGER.debug("Ticket [{}] by type [{}] cannot be found in the ticket registry.", id,
clazz != null ? clazz.getSimpleName() : "unspecified");
throw new InvalidTicketException(id);
}
synchronized (ticket) {
if (ticket.isExpired()) {
deleteTicket(id);
LOGGER.debug("Ticket [{}] has expired and is now deleted from the ticket registry.", ticket);
throw new InvalidTicketException(id);
}
}
}
@Override
public Ticket updateTicket(final Ticket ticket) {
this.ticketRegistry.updateTicket(ticket);
return ticket;
}
/**
* Resolve service from authentication request.
*
* @param service the service
* @return the service
*/
protected Service resolveServiceFromAuthenticationRequest(final Service service) {
return authenticationRequestServiceSelectionStrategies.resolveService(service);
}
/**
* Verify the ticket id received is actually legitimate
* before contacting downstream systems to find and process it.
*
* @param ticketId the ticket id
* @return true/false
*/
protected boolean isTicketAuthenticityVerified(final String ticketId) {
if (this.cipherExecutor != null) {
LOGGER.debug("Attempting to decode service ticket [{}] to verify authenticity", ticketId);
return !StringUtils.isEmpty(this.cipherExecutor.decode(ticketId));
}
return !StringUtils.isEmpty(ticketId);
}
@Override
public void setApplicationEventPublisher(final ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
}