Completed
Push — master ( 039122...d1b12d )
by Misagh
48:00 queued 22:11
created

handle(Exception,RequestContext)   A

Complexity

Conditions 3

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 3
c 1
b 1
f 0
dl 0
loc 15
rs 9.4285
1
package org.apereo.cas.web.flow.actions;
2
3
import org.apereo.cas.authentication.AuthenticationException;
4
import org.apereo.cas.services.UnauthorizedServiceForPrincipalException;
5
import org.apereo.cas.ticket.AbstractTicketException;
6
import org.apereo.cas.web.flow.CasWebflowConstants;
7
import org.apereo.cas.web.support.WebUtils;
8
import org.slf4j.Logger;
9
import org.slf4j.LoggerFactory;
10
import org.springframework.binding.message.MessageBuilder;
11
import org.springframework.binding.message.MessageContext;
12
import org.springframework.webflow.action.AbstractAction;
13
import org.springframework.webflow.action.EventFactorySupport;
14
import org.springframework.webflow.execution.Event;
15
import org.springframework.webflow.execution.RequestContext;
16
17
import java.net.URI;
18
import java.util.LinkedHashSet;
19
import java.util.Optional;
20
import java.util.Set;
21
22
/**
23
 * Performs two important error handling functions on an
24
 * {@link org.apereo.cas.authentication.AuthenticationException} raised from the authentication
25
 * layer:
26
 * <ol>
27
 * <li>Maps handler errors onto message bundle strings for display to user.</li>
28
 * <li>Determines the next webflow state by comparing handler errors against {@link #errors}
29
 * in list order. The first entry that matches determines the outcome state, which
30
 * is the simple class name of the exception.</li>
31
 * </ol>
32
 *
33
 * @author Marvin S. Addison
34
 * @since 4.0.0
35
 */
36
public class AuthenticationExceptionHandlerAction extends AbstractAction {
37
38
    private static final String DEFAULT_MESSAGE_BUNDLE_PREFIX = "authenticationFailure.";
39
    private static final String UNKNOWN = "UNKNOWN";
40
41
    private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticationExceptionHandlerAction.class);
42
43
    /**
44
     * Ordered list of error classes that this class knows how to handle.
45
     */
46
    private final Set<Class<? extends Exception>> errors;
47
48
    /**
49
     * String appended to exception class name to create a message bundle key for that particular error.
50
     */
51
    private String messageBundlePrefix = DEFAULT_MESSAGE_BUNDLE_PREFIX;
52
53
    public AuthenticationExceptionHandlerAction() {
54
        this(new LinkedHashSet<>());
55
    }
56
57
    public AuthenticationExceptionHandlerAction(final Set<Class<? extends Exception>> errors) {
58
        this.errors = errors;
59
    }
60
61
    public Set<Class<? extends Exception>> getErrors() {
62
        return new LinkedHashSet<>(this.errors);
63
    }
64
65
    /**
66
     * Maps an authentication exception onto a state name.
67
     * Also sets an ERROR severity message in the message context.
68
     *
69
     * @param e              Authentication error to handle.
70
     * @param requestContext the spring  context
71
     * @return Name of next flow state to transition to or {@value #UNKNOWN}
72
     */
73
    public String handle(final Exception e, final RequestContext requestContext) {
74
        final MessageContext messageContext = requestContext.getMessageContext();
75
76
        if (e instanceof AuthenticationException) {
77
            return handleAuthenticationException((AuthenticationException) e, requestContext);
78
        }
79
80
        if (e instanceof AbstractTicketException) {
81
            return handleAbstractTicketException((AbstractTicketException) e, requestContext);
82
        }
83
84
        LOGGER.trace("Unable to translate errors of the authentication exception [{}]. Returning [{}]", e, UNKNOWN);
85
        final String messageCode = this.messageBundlePrefix + UNKNOWN;
86
        messageContext.addMessage(new MessageBuilder().error().code(messageCode).build());
87
        return UNKNOWN;
88
    }
89
90
    /**
91
     * Maps an authentication exception onto a state name equal to the simple class name of the {@link
92
     * AuthenticationException#getHandlerErrors()}
93
     * with highest precedence. Also sets an ERROR severity message in the
94
     * message context of the form {@code [messageBundlePrefix][exceptionClassSimpleName]}
95
     * for for the first handler
96
     * error that is configured. If no match is found, {@value #UNKNOWN} is returned.
97
     *
98
     * @param e              Authentication error to handle.
99
     * @param requestContext the spring context
100
     * @return Name of next flow state to transition to or {@value #UNKNOWN}
101
     */
102
    protected String handleAuthenticationException(final AuthenticationException e,
103
                                                   final RequestContext requestContext) {
104
105
        if (e.getHandlerErrors().containsKey(UnauthorizedServiceForPrincipalException.class.getSimpleName())) {
106
            final URI url = WebUtils.getUnauthorizedRedirectUrlIntoFlowScope(requestContext);
107
            if (url != null) {
108
                LOGGER.warn("Unauthorized service access for principal; CAS will be redirecting to [{}]", url);
109
                return CasWebflowConstants.STATE_ID_SERVICE_UNAUTHZ_CHECK;
110
            }
111
        }
112
113
        final String handlerErrorName = this.errors
114
                .stream()
115
                .filter(e.getHandlerErrors().values()::contains)
116
                .map(Class::getSimpleName)
117
                .findFirst()
118
                .orElseGet(() -> {
119
                    LOGGER.debug("Unable to translate handler errors of the authentication exception [{}]. Returning [{}]", e, UNKNOWN);
120
                    return UNKNOWN;
121
                });
122
123
        final MessageContext messageContext = requestContext.getMessageContext();
124
        final String messageCode = this.messageBundlePrefix + handlerErrorName;
125
        messageContext.addMessage(new MessageBuilder().error().code(messageCode).build());
126
        return handlerErrorName;
127
    }
128
129
    /**
130
     * Maps an {@link AbstractTicketException} onto a state name equal to the simple class name of the exception with
131
     * highest precedence. Also sets an ERROR severity message in the message context with the error code found in
132
     * {@link AbstractTicketException#getCode()}. If no match is found,
133
     * {@value #UNKNOWN} is returned.
134
     *
135
     * @param e              Ticket exception to handle.
136
     * @param requestContext the spring context
137
     * @return Name of next flow state to transition to or {@value #UNKNOWN}
138
     */
139
    protected String handleAbstractTicketException(final AbstractTicketException e, final RequestContext requestContext) {
140
        final MessageContext messageContext = requestContext.getMessageContext();
141
        final Optional<String> match = this.errors.stream()
142
                .filter(c -> c.isInstance(e)).map(Class::getSimpleName)
143
                .findFirst();
144
145
        match.ifPresent(s -> messageContext.addMessage(new MessageBuilder().error().code(e.getCode()).build()));
146
        return match.orElse(UNKNOWN);
147
    }
148
149
    @Override
150
    protected Event doExecute(final RequestContext requestContext) {
151
        final Event currentEvent = requestContext.getCurrentEvent();
152
        LOGGER.debug("Located current event [{}]", currentEvent);
153
154
        final Exception error = currentEvent.getAttributes().get("error", Exception.class);
155
        LOGGER.debug("Located error attribute [{}] with message [{}] from the current event", error.getClass(), error.getMessage());
156
157
        final String event = handle(error, requestContext);
158
        LOGGER.debug("Final event id resolved from the error is [{}]", event);
159
160
        return new EventFactorySupport().event(this, event);
161
    }
162
}
163