package org.apereo.cas.web.report;
import org.apereo.cas.CentralAuthenticationService;
import org.apereo.cas.authentication.Authentication;
import org.apereo.cas.authentication.principal.Principal;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.ticket.Ticket;
import org.apereo.cas.ticket.TicketGrantingTicket;
import org.apereo.cas.util.DateTimeUtils;
import org.apereo.cas.util.ISOStandardDateFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.async.WebAsyncTask;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
/**
* SSO Report web controller that produces JSON data for the view.
*
* @author Misagh Moayyed
* @author Dmitriy Kopylenko
* @since 4.1
*/
public class SingleSignOnSessionsReportController extends BaseCasMvcEndpoint {
private static final String VIEW_SSO_SESSIONS = "monitoring/viewSsoSessions";
private static final String STATUS = "status";
private static final String TICKET_GRANTING_TICKET = "ticketGrantingTicket";
private static final Logger LOGGER = LoggerFactory.getLogger(SingleSignOnSessionsReportController.class);
private final CasConfigurationProperties casProperties;
private enum SsoSessionReportOptions {
ALL("all"),
PROXIED("proxied"),
DIRECT("direct");
private final String type;
/**
* Instantiates a new Sso session report options.
*
* @param type the type
*/
SsoSessionReportOptions(final String type) {
this.type = type;
}
public String getType() {
return this.type;
}
@Override
public String toString() {
return this.type;
}
}
/**
* The enum Sso session attribute keys.
*/
private enum SsoSessionAttributeKeys {
AUTHENTICATED_PRINCIPAL("authenticated_principal"),
PRINCIPAL_ATTRIBUTES("principal_attributes"),
AUTHENTICATION_DATE("authentication_date"),
AUTHENTICATION_DATE_FORMATTED("authentication_date_formatted"),
TICKET_GRANTING_TICKET("ticket_granting_ticket"),
AUTHENTICATION_ATTRIBUTES("authentication_attributes"),
PROXIED_BY("proxied_by"),
AUTHENTICATED_SERVICES("authenticated_services"),
IS_PROXIED("is_proxied"),
NUMBER_OF_USES("number_of_uses");
private final String attributeKey;
/**
* Instantiates a new Sso session attribute keys.
*
* @param attributeKey the attribute key
*/
SsoSessionAttributeKeys(final String attributeKey) {
this.attributeKey = attributeKey;
}
@Override
public String toString() {
return this.attributeKey;
}
}
private final CentralAuthenticationService centralAuthenticationService;
public SingleSignOnSessionsReportController(final CentralAuthenticationService centralAuthenticationService,
final CasConfigurationProperties casProperties) {
super("ssosessions", "/ssosessions", casProperties.getMonitor().getEndpoints().getSingleSignOnReport(), casProperties);
this.centralAuthenticationService = centralAuthenticationService;
this.casProperties = casProperties;
}
/**
* Gets sso sessions.
*
* @param option the option
* @return the sso sessions
*/
private Collection<Map<String, Object>> getActiveSsoSessions(final SsoSessionReportOptions option) {
final Collection<Map<String, Object>> activeSessions = new ArrayList<>();
final ISOStandardDateFormat dateFormat = new ISOStandardDateFormat();
getNonExpiredTicketGrantingTickets().stream().map(TicketGrantingTicket.class::cast)
.filter(tgt -> !(option == SsoSessionReportOptions.DIRECT && tgt.getProxiedBy() != null)).forEach(tgt -> {
final Authentication authentication = tgt.getAuthentication();
final Principal principal = authentication.getPrincipal();
final Map<String, Object> sso = new HashMap<>(SsoSessionAttributeKeys.values().length);
sso.put(SsoSessionAttributeKeys.AUTHENTICATED_PRINCIPAL.toString(), principal.getId());
sso.put(SsoSessionAttributeKeys.AUTHENTICATION_DATE.toString(), authentication.getAuthenticationDate());
sso.put(SsoSessionAttributeKeys.AUTHENTICATION_DATE_FORMATTED.toString(),
dateFormat.format(DateTimeUtils.dateOf(authentication.getAuthenticationDate())));
sso.put(SsoSessionAttributeKeys.NUMBER_OF_USES.toString(), tgt.getCountOfUses());
sso.put(SsoSessionAttributeKeys.TICKET_GRANTING_TICKET.toString(), tgt.getId());
sso.put(SsoSessionAttributeKeys.PRINCIPAL_ATTRIBUTES.toString(), principal.getAttributes());
sso.put(SsoSessionAttributeKeys.AUTHENTICATION_ATTRIBUTES.toString(), authentication.getAttributes());
if (option != SsoSessionReportOptions.DIRECT) {
if (tgt.getProxiedBy() != null) {
sso.put(SsoSessionAttributeKeys.IS_PROXIED.toString(), Boolean.TRUE);
sso.put(SsoSessionAttributeKeys.PROXIED_BY.toString(), tgt.getProxiedBy().getId());
} else {
sso.put(SsoSessionAttributeKeys.IS_PROXIED.toString(), Boolean.FALSE);
}
}
sso.put(SsoSessionAttributeKeys.AUTHENTICATED_SERVICES.toString(), tgt.getServices());
activeSessions.add(sso);
});
return activeSessions;
}
/**
* Gets non expired ticket granting tickets.
*
* @return the non expired ticket granting tickets
*/
private Collection<Ticket> getNonExpiredTicketGrantingTickets() {
return this.centralAuthenticationService.getTickets(ticket -> ticket instanceof TicketGrantingTicket && !ticket.isExpired());
}
/**
* Endpoint for getting SSO Sessions in JSON format.
*
* @param type the type
* @param request the request
* @param response the response
* @return the sso sessions
*/
@GetMapping(value = "/getSsoSessions")
@ResponseBody
public WebAsyncTask<Map<String, Object>> getSsoSessions(@RequestParam(defaultValue = "ALL") final String type,
final HttpServletRequest request,
final HttpServletResponse response) {
ensureEndpointAccessIsAuthorized(request, response);
final Callable<Map<String, Object>> asyncTask = () -> {
final Map<String, Object> sessionsMap = new HashMap<>(1);
final SsoSessionReportOptions option = SsoSessionReportOptions.valueOf(type);
final Collection<Map<String, Object>> activeSsoSessions = getActiveSsoSessions(option);
sessionsMap.put("activeSsoSessions", activeSsoSessions);
long totalTicketGrantingTickets = 0;
long totalProxyGrantingTickets = 0;
long totalUsageCount = 0;
final Set<String> uniquePrincipals = new HashSet<>();
for (final Map<String, Object> activeSsoSession : activeSsoSessions) {
if (activeSsoSession.containsKey(SsoSessionAttributeKeys.IS_PROXIED.toString())) {
final Boolean isProxied = Boolean.valueOf(activeSsoSession.get(SsoSessionAttributeKeys.IS_PROXIED.toString()).toString());
if (isProxied) {
totalProxyGrantingTickets++;
} else {
totalTicketGrantingTickets++;
final String principal = activeSsoSession.get(SsoSessionAttributeKeys.AUTHENTICATED_PRINCIPAL.toString()).toString();
uniquePrincipals.add(principal);
}
} else {
totalTicketGrantingTickets++;
final String principal = activeSsoSession.get(SsoSessionAttributeKeys.AUTHENTICATED_PRINCIPAL.toString()).toString();
uniquePrincipals.add(principal);
}
totalUsageCount += Long.parseLong(activeSsoSession.get(SsoSessionAttributeKeys.NUMBER_OF_USES.toString()).toString());
}
sessionsMap.put("totalProxyGrantingTickets", totalProxyGrantingTickets);
sessionsMap.put("totalTicketGrantingTickets", totalTicketGrantingTickets);
sessionsMap.put("totalTickets", totalTicketGrantingTickets + totalProxyGrantingTickets);
sessionsMap.put("totalPrincipals", uniquePrincipals.size());
sessionsMap.put("totalUsageCount", totalUsageCount);
return sessionsMap;
};
return new WebAsyncTask<>(casProperties.getHttpClient().getAsyncTimeout(), asyncTask);
}
/**
* Endpoint for destroying a single SSO Session.
*
* @param ticketGrantingTicket the ticket granting ticket
* @param request the request
* @param response the response
* @return result map
*/
@PostMapping(value = "/destroySsoSession")
@ResponseBody
public Map<String, Object> destroySsoSession(@RequestParam final String ticketGrantingTicket,
final HttpServletRequest request,
final HttpServletResponse response) {
ensureEndpointAccessIsAuthorized(request, response);
final Map<String, Object> sessionsMap = new HashMap<>(1);
try {
this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicket);
sessionsMap.put(STATUS, HttpServletResponse.SC_OK);
sessionsMap.put(TICKET_GRANTING_TICKET, ticketGrantingTicket);
} catch (final Exception e) {
LOGGER.error(e.getMessage(), e);
sessionsMap.put(STATUS, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
sessionsMap.put(TICKET_GRANTING_TICKET, ticketGrantingTicket);
sessionsMap.put("message", e.getMessage());
}
return sessionsMap;
}
/**
* Endpoint for destroying SSO Sessions.
*
* @param type the type
* @param request the request
* @param response the response
* @return result map
*/
@PostMapping(value = "/destroySsoSessions")
@ResponseBody
public Map<String, Object> destroySsoSessions(@RequestParam(defaultValue = "ALL") final String type,
final HttpServletRequest request,
final HttpServletResponse response) {
ensureEndpointAccessIsAuthorized(request, response);
final Map<String, Object> sessionsMap = new HashMap<>();
final Map<String, String> failedTickets = new HashMap<>();
final SsoSessionReportOptions option = SsoSessionReportOptions.valueOf(type);
final Collection<Map<String, Object>> collection = getActiveSsoSessions(option);
collection.stream().map(sso -> sso.get(SsoSessionAttributeKeys.TICKET_GRANTING_TICKET.toString()).toString()).forEach(ticketGrantingTicket -> {
try {
this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicket);
} catch (final Exception e) {
LOGGER.error(e.getMessage(), e);
failedTickets.put(ticketGrantingTicket, e.getMessage());
}
});
if (failedTickets.isEmpty()) {
sessionsMap.put(STATUS, HttpServletResponse.SC_OK);
} else {
sessionsMap.put(STATUS, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
sessionsMap.put("failedTicketGrantingTickets", failedTickets);
}
return sessionsMap;
}
/**
* Show sso sessions.
*
* @param request the request
* @param response the response
* @return the model and view where json data will be rendered
* @throws Exception thrown during json processing
*/
@GetMapping
public ModelAndView showSsoSessions(final HttpServletRequest request,
final HttpServletResponse response) throws Exception {
ensureEndpointAccessIsAuthorized(request, response);
return new ModelAndView(VIEW_SSO_SESSIONS);
}
}