001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    package org.tynamo.security.shiro;
020    
021    import static org.apache.shiro.util.StringUtils.split;
022    
023    import java.io.IOException;
024    
025    import javax.servlet.ServletRequest;
026    import javax.servlet.ServletResponse;
027    
028    import org.apache.shiro.SecurityUtils;
029    import org.apache.shiro.subject.Subject;
030    import org.apache.shiro.util.AntPathMatcher;
031    import org.apache.shiro.util.PatternMatcher;
032    import org.apache.shiro.web.servlet.AdviceFilter;
033    import org.apache.shiro.web.util.WebUtils;
034    
035    /**
036     * Superclass for any filter that controls access to a resource and may redirect the user to the login page
037     * if they are not authenticated.  This superclass provides the method
038     * {@link #saveRequestAndRedirectToLogin(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}
039     * which is used by many subclasses as the behavior when a user is unauthenticated.
040     * 
041     * This class and the subclasses that are used as Shiro's built-in filters were copied from Shiro 1.1.0
042     * and modified locally to implement same behavior as specified in https://issues.apache.org/jira/browse/SHIRO-256
043     * We'll revert to using Shiro's filters if the feature gets implemented in Shiro 2.x
044     *
045     * @since 0.4.0
046     */
047    public abstract class AccessControlFilter extends AdviceFilter {
048            // default values - populated by the ChainFactory from symbols 
049            public static String LOGIN_URL, SUCCESS_URL, UNAUTHORIZED_URL;
050            
051        protected PatternMatcher pathMatcher = new AntPathMatcher() {
052                            @Override
053                            public boolean match(String pattern, String string) {
054                                    return super.match(pattern, string.toLowerCase());
055                            }
056                    };
057                    
058                    private String[] configElements;
059    
060                    private String config;
061                    
062                    /*
063                     * Same as {@link #setConfig()} except throws an {@link IllegalArgumentException} if config is already set.
064                     * Mostly used in the initialization logic  
065                     */
066                    public void addConfig(String config) {
067                            if (config == null && this.config == null && configElements != null) return;
068                            if (config != null && config.equals(this.config)) return;
069                            if (configElements != null) throw new IllegalArgumentException("Configuration is already add for this filter, existing config is " + this.configElements + ". Use setConfig if you want to override the existing configuration");
070                            setConfig(config);
071                    }
072    
073                    
074                    public void setConfig(String config) {
075          if (config != null) configElements = split(config);
076          else configElements = new String[0];
077          this.config = config;
078                    }
079    
080        /**
081         * Constant representing the HTTP 'GET' request method, equal to <code>GET</code>.
082         */
083        public static final String GET_METHOD = "GET";
084    
085        /**
086         * Constant representing the HTTP 'POST' request method, equal to <code>POST</code>.
087         */
088        public static final String POST_METHOD = "POST";
089    
090        /**
091         * The login url to used to authenticate a user, used when redirecting users if authentication is required.
092         */
093        private String loginUrl = LOGIN_URL;
094    
095        private String successUrl = SUCCESS_URL;
096        
097        private String unauthorizedUrl = UNAUTHORIZED_URL;
098    
099        /**
100         * Returns the success url to use as the default location a user is sent after logging in.  Typically a redirect
101         * after login will redirect to the originally request URL; this property is provided mainly as a fallback in case
102         * the original request URL is not available or not specified.
103         * <p/>
104         * The default value is {@link #DEFAULT_SUCCESS_URL}.
105         *
106         * @return the success url to use as the default location a user is sent after logging in.
107         */
108        public String getSuccessUrl() {
109            return successUrl;
110        }
111    
112        /**
113         * Sets the default/fallback success url to use as the default location a user is sent after logging in.  Typically
114         * a redirect after login will redirect to the originally request URL; this property is provided mainly as a
115         * fallback in case the original request URL is not available or not specified.
116         * <p/>
117         * The default value is {@link #DEFAULT_SUCCESS_URL}.
118         *
119         * @param successUrl the success URL to redirect the user to after a successful login.
120         */
121        public void setSuccessUrl(String successUrl) {
122            this.successUrl = successUrl;
123        }
124        
125        
126        /**
127         * Returns the login URL used to authenticate a user.
128         * <p/>
129         * Most Shiro filters use this url
130         * as the location to redirect a user when the filter requires authentication.  Unless overridden, the
131         * {@link #DEFAULT_LOGIN_URL DEFAULT_LOGIN_URL} is assumed, which can be overridden via
132         * {@link #setLoginUrl(String) setLoginUrl}.
133         *
134         * @return the login URL used to authenticate a user, used when redirecting users if authentication is required.
135         */
136        public String getLoginUrl() {
137            return loginUrl;
138        }
139    
140        /**
141         * Sets the login URL used to authenticate a user.
142         * <p/>
143         * Most Shiro filters use this url as the location to redirect a user when the filter requires
144         * authentication.  Unless overridden, the {@link #DEFAULT_LOGIN_URL DEFAULT_LOGIN_URL} is assumed.
145         *
146         * @param loginUrl the login URL used to authenticate a user, used when redirecting users if authentication is required.
147         */
148        public void setLoginUrl(String loginUrl) {
149            this.loginUrl = loginUrl;
150        }
151    
152        public String getUnauthorizedUrl() {
153                            return unauthorizedUrl;
154                    }
155    
156                    public void setUnauthorizedUrl(String unauthorizedUrl) {
157                            this.unauthorizedUrl = unauthorizedUrl;
158                    }
159    
160                    /**
161         * Convenience method that acquires the Subject associated with the request.
162         * <p/>
163         * The default implementation simply returns
164         * {@link org.apache.shiro.SecurityUtils#getSubject() SecurityUtils.getSubject()}.
165         *
166         * @param request  the incoming <code>ServletRequest</code>
167         * @param response the outgoing <code>ServletResponse</code>
168         * @return the Subject associated with the request.
169         */
170        protected Subject getSubject(ServletRequest request, ServletResponse response) {
171            return SecurityUtils.getSubject();
172        }
173    
174        /**
175         * Returns <code>true</code> if the request is allowed to proceed through the filter normally, or <code>false</code>
176         * if the request should be handled by the
177         * {@link #onAccessDenied(ServletRequest,ServletResponse,Object) onAccessDenied(request,response,mappedValue)}
178         * method instead.
179         *
180         * @param request     the incoming <code>ServletRequest</code>
181         * @param response    the outgoing <code>ServletResponse</code>
182         * @param mappedValue the filter-specific config value mapped to this filter in the URL rules mappings.
183         * @return <code>true</code> if the request should proceed through the filter normally, <code>false</code> if the
184         *         request should be processed by this filter's
185         *         {@link #onAccessDenied(ServletRequest,ServletResponse,Object)} method instead.
186         * @throws Exception if an error occurs during processing.
187         */
188        protected abstract boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;
189    
190        /**
191         * Processes requests where the subject was denied access as determined by the
192         * {@link #isAccessAllowed(javax.servlet.ServletRequest, javax.servlet.ServletResponse, Object) isAccessAllowed}
193         * method, retaining the {@code mappedValue} that was used during configuration.
194         * <p/>
195         * This method immediately delegates to {@link #onAccessDenied(ServletRequest,ServletResponse)} as a
196         * convenience in that most post-denial behavior does not need the mapped config again.
197         *
198         * @param request     the incoming <code>ServletRequest</code>
199         * @param response    the outgoing <code>ServletResponse</code>
200         * @param mappedValue the config specified for the filter in the matching request's filter chain.
201         * @return <code>true</code> if the request should continue to be processed; false if the subclass will
202         *         handle/render the response directly.
203         * @throws Exception if there is an error processing the request.
204         * @since 1.0
205         */
206        protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
207            return onAccessDenied(request, response);
208        }
209    
210        /**
211         * Processes requests where the subject was denied access as determined by the
212         * {@link #isAccessAllowed(javax.servlet.ServletRequest, javax.servlet.ServletResponse, Object) isAccessAllowed}
213         * method.
214         *
215         * @param request  the incoming <code>ServletRequest</code>
216         * @param response the outgoing <code>ServletResponse</code>
217         * @return <code>true</code> if the request should continue to be processed; false if the subclass will
218         *         handle/render the response directly.
219         * @throws Exception if there is an error processing the request.
220         */
221        protected abstract boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception;
222    
223        /**
224         * Returns <code>true</code> if
225         * {@link #isAccessAllowed(ServletRequest,ServletResponse,Object) isAccessAllowed(Request,Response,Object)},
226         * otherwise returns the result of
227         * {@link #onAccessDenied(ServletRequest,ServletResponse,Object) onAccessDenied(Request,Response,Object)}.
228         *
229         * @return <code>true</code> if
230         *         {@link #isAccessAllowed(javax.servlet.ServletRequest, javax.servlet.ServletResponse, Object) isAccessAllowed},
231         *         otherwise returns the result of
232         *         {@link #onAccessDenied(javax.servlet.ServletRequest, javax.servlet.ServletResponse) onAccessDenied}.
233         * @throws Exception if an error occurs.
234         */
235        public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
236            return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
237        }
238        
239        protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
240              return onPreHandle(request, response, configElements);
241        }
242    
243        /**
244         * Returns <code>true</code> if the incoming request is a login request, <code>false</code> otherwise.
245         * <p/>
246         * The default implementation merely returns <code>true</code> if the incoming request matches the configured
247         * {@link #getLoginUrl() loginUrl} by calling
248         * <code>{@link #pathsMatch(String, String) pathsMatch(loginUrl, request)}</code>.
249         *
250         * @param request  the incoming <code>ServletRequest</code>
251         * @param response the outgoing <code>ServletResponse</code>
252         * @return <code>true</code> if the incoming request is a login request, <code>false</code> otherwise.
253         */
254        protected boolean isLoginRequest(ServletRequest request, ServletResponse response) {
255          return pathMatcher.matches(getLoginUrl(), WebUtils.getPathWithinApplication(WebUtils.toHttp(request)));
256        }
257    
258        /**
259         * Convenience method for subclasses to use when a login redirect is required.
260         * <p/>
261         * This implementation simply calls {@link #saveRequest(javax.servlet.ServletRequest) saveRequest(request)}
262         * and then {@link #redirectToLogin(javax.servlet.ServletRequest, javax.servlet.ServletResponse) redirectToLogin(request,response)}.
263         *
264         * @param request  the incoming <code>ServletRequest</code>
265         * @param response the outgoing <code>ServletResponse</code>
266         * @throws IOException if an error occurs.
267         */
268        protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
269            saveRequest(request);
270            redirectToLogin(request, response);
271        }
272    
273        /**
274         * Convenience method merely delegates to
275         * {@link WebUtils#saveRequest(javax.servlet.ServletRequest) WebUtils.saveRequest(request)} to save the request
276         * state for reuse later.  This is mostly used to retain user request state when a redirect is issued to
277         * return the user to their originally requested url/resource.
278         * <p/>
279         * If you need to save and then immediately redirect the user to login, consider using
280         * {@link #saveRequestAndRedirectToLogin(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
281         * saveRequestAndRedirectToLogin(request,response)} directly.
282         *
283         * @param request the incoming ServletRequest to save for re-use later (for example, after a redirect).
284         */
285        protected void saveRequest(ServletRequest request) {
286            WebUtils.saveRequest(request);
287        }
288    
289        /**
290         * Convenience method for subclasses that merely acquires the {@link #getLoginUrl() getLoginUrl} and redirects
291         * the request to that url.
292         * <p/>
293         * <b>N.B.</b>  If you want to issue a redirect with the intention of allowing the user to then return to their
294         * originally requested URL, don't use this method directly.  Instead you should call
295         * {@link #saveRequestAndRedirectToLogin(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
296         * saveRequestAndRedirectToLogin(request,response)}, which will save the current request state so that it can
297         * be reconstructed and re-used after a successful login.
298         *
299         * @param request  the incoming <code>ServletRequest</code>
300         * @param response the outgoing <code>ServletResponse</code>
301         * @throws IOException if an error occurs.
302         */
303        protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
304            String loginUrl = getLoginUrl();
305            WebUtils.issueRedirect(request, response, loginUrl);
306        }
307    
308    }