View Javadoc

1   package com.atlassian.seraph.filter;
2   
3   import com.atlassian.seraph.SecurityService;
4   import com.atlassian.seraph.auth.AuthType;
5   import com.atlassian.seraph.auth.AuthenticationContext;
6   import com.atlassian.seraph.auth.Authenticator;
7   import com.atlassian.seraph.config.SecurityConfig;
8   import com.atlassian.seraph.config.SecurityConfigFactory;
9   import com.atlassian.seraph.util.RedirectUtils;
10  import com.atlassian.seraph.util.SecurityUtils;
11  import org.apache.log4j.Category;
12  
13  import javax.servlet.Filter;
14  import javax.servlet.FilterChain;
15  import javax.servlet.FilterConfig;
16  import javax.servlet.ServletException;
17  import javax.servlet.ServletRequest;
18  import javax.servlet.ServletResponse;
19  import javax.servlet.http.Cookie;
20  import javax.servlet.http.HttpServletRequest;
21  import javax.servlet.http.HttpServletResponse;
22  import java.io.IOException;
23  import java.security.Principal;
24  import java.util.HashSet;
25  import java.util.Set;
26  
27  import static org.apache.commons.lang.StringUtils.isNotBlank;
28  
29  /**
30   * The SecurityFilter determines which roles are required for a given request by querying all of the Services.
31   *
32   * @see SecurityService
33   */
34  public class SecurityFilter implements Filter
35  {
36      private FilterConfig config = null;
37      private SecurityConfig securityConfig = null;
38  
39      private static final Category log = Category.getInstance(SecurityFilter.class);
40      static final String ALREADY_FILTERED = "os_securityfilter_already_filtered";
41      public static final String ORIGINAL_URL = "atlassian.core.seraph.original.url";
42  
43      public void init(final FilterConfig config)
44      {
45          log.debug("SecurityFilter.init");
46          this.config = config;
47  
48          String configFileLocation = null;
49  
50          if (config.getInitParameter("config.file") != null)
51          {
52              configFileLocation = config.getInitParameter("config.file");
53              log.debug("Security config file location: " + configFileLocation);
54          }
55  
56          securityConfig = SecurityConfigFactory.getInstance(configFileLocation);
57          config.getServletContext().setAttribute(SecurityConfig.STORAGE_KEY, securityConfig);
58          log.debug("SecurityFilter.init completed successfully.");
59      }
60  
61      public void destroy()
62      {
63          log.debug("SecurityFilter.destroy");
64          // SER-129: securityConfig was seen in the wild to sometimes throw NPE.
65          if (securityConfig == null)
66          {
67              log.warn("Trying to destroy a SecurityFilter with null securityConfig.");
68          }
69          else
70          {
71              // TODO: Why do we set these variables to null? Note that the securityConfig still lives in the ServletContext attribute.
72              securityConfig.destroy();
73              securityConfig = null;
74          }
75          config = null;
76      }
77  
78      public void doFilter(final ServletRequest req, final ServletResponse res, final FilterChain chain) throws IOException, ServletException
79      {
80          if ((req.getAttribute(ALREADY_FILTERED) != null) || !getSecurityConfig().getController().isSecurityEnabled())
81          {
82              chain.doFilter(req, res);
83              return;
84          }
85          else
86          {
87              req.setAttribute(ALREADY_FILTERED, Boolean.TRUE);
88          }
89  
90          final String METHOD = "doFilter : ";
91          final boolean dbg = log.isDebugEnabled();
92  
93          // Try and get around Orion's bug when redeploying
94          // it seems that filters are not called in the correct order
95          if (!SecurityUtils.isSeraphFilteringDisabled(req))
96          {
97              log.warn(METHOD + "LoginFilter not yet applied to this request - terminating filter chain");
98              return;
99          }
100 
101         final HttpServletRequest httpServletRequest = (HttpServletRequest) req;
102         final HttpServletResponse httpServletResponse = (HttpServletResponse) res;
103 
104         final String originalURL = httpServletRequest.getServletPath() + (httpServletRequest.getPathInfo() == null ? "" : httpServletRequest.getPathInfo()) + (httpServletRequest.getQueryString() == null ? "" : "?" + httpServletRequest.getQueryString());
105 
106         // store the original URL as a request attribute anyway - often useful for pages to access it (ie login links)
107         httpServletRequest.setAttribute(SecurityFilter.ORIGINAL_URL, originalURL);
108         if (dbg)
109         {
110             log.debug(METHOD + "Storing the originally requested URL (" + SecurityFilter.ORIGINAL_URL + "=" + originalURL + ")");
111         }
112 
113         final Set<String> requiredRoles = new HashSet<String>();
114 
115         // loop through loaded services and get required roles
116         for (final SecurityService service : getSecurityConfig().getServices())
117         {
118             final Set<String> serviceRoles = service.getRequiredRoles(httpServletRequest);
119             requiredRoles.addAll(serviceRoles);
120         }
121 
122         if (dbg)
123         {
124             log.debug(METHOD + "requiredRoles = " + requiredRoles);
125         }
126 
127         // whether this URL needs authorisation
128         boolean needAuth = false;
129 
130         // try to get the user (for cookie logins and basic auth as well as already established sessions)
131         final Authenticator authenticator = getSecurityConfig().getAuthenticator();
132         final Principal user = authenticator.getUser(httpServletRequest, httpServletResponse);
133 
134         if (user == null)
135         {
136             AuthType authType = AuthType.getAuthTypeInformation(httpServletRequest, getSecurityConfig());
137 
138             // This include AuthType.BASIC *as well as* when the client pre-emptively sends BASIC credentials
139             // This will also catch AuthType.ANY when they send BASIC credentials.
140             if (RedirectUtils.isBasicAuthentication(httpServletRequest, getSecurityConfig().getAuthType()))
141             {
142                 // The DefaultAuthenticator already did a response.sendError(401)
143                 return;
144             }
145             else if (authType == AuthType.COOKIE && httpServletRequest.getSession(false) == null)
146             {
147                 httpServletResponse.sendError(401, "os_authType was 'cookie' but no valid cookie was sent.");
148                 return;
149             }
150             else if (authType == AuthType.ANY)
151             {
152                 // If they specified AuthType.ANY and sent BASIC credentials it was handled by a previous case.
153                 // For this we only have to worry about cookies.
154                 if (hasJSessionCookie(httpServletRequest.getCookies()) && httpServletRequest.getSession(false) == null)
155                 {
156                     httpServletResponse.sendError(401, "os_authType was 'any' and an invalid cookie was sent.");
157                     return;
158                 }
159             }
160         }
161 
162         // set the user in the context
163         if (dbg)
164         {
165             log.debug(METHOD + "Setting Auth Context to be '" + (user == null ? "anonymous " : user.getName()) + "'");
166         }
167 
168         final AuthenticationContext authenticationContext = getAuthenticationContext();
169         authenticationContext.setUser(user);
170 
171         // check if the current user has all required permissions
172         // if there is no current user, request.isUserInRole() always returns false so this works
173         for (final Object element : requiredRoles)
174         {
175             final String role = (String) element;
176 
177             // this isUserInRole method is only used here and 'd be better off replaced by getRoleMapper().hasRole(user, request, role)) since we have the user already
178             // was : if (!securityConfig.getAuthenticator().isUserInRole(request, role))
179             if (!getSecurityConfig().getRoleMapper().hasRole(user, httpServletRequest, role))
180             {
181                 log.info(METHOD + "'" + user + "' needs (and lacks) role '" + role + "' to access " + originalURL);
182                 needAuth = true;
183             }
184         }
185 
186         // check if we're at the signon page, in which case do not auth
187         if ((httpServletRequest.getServletPath() != null) && httpServletRequest.getServletPath().equals(getSecurityConfig().getLoginURL()))
188         {
189             if (dbg)
190             {
191                 log.debug(METHOD + "Login page requested so no additional authorization required.");
192             }
193             needAuth = false;
194         }
195 
196         // if we need to authenticate, store current URL and forward
197         if (needAuth)
198         {
199             // POST requests can't be properly handled by 30x redirects, since all the POST parameters are lost in
200             // the redirect process. to get around this Seraph supports a "login forward path". if this is defined
201             // Seraph uses the servlet request dispatcher to FORWARD the request, giving apps a chance to save  or
202             // otherwise handle the POST params before redirecting to the login page.
203             String loginForwardPath = getSecurityConfig().getLoginForwardPath();
204             if (isPOST(httpServletRequest) && isNotBlank(loginForwardPath))
205             {
206                 if (dbg)
207                 {
208                     log.debug(METHOD + "Need Authentication for POST: Forwarding to: " + loginForwardPath + " from: " + originalURL);
209                 }
210 
211                 httpServletRequest.getRequestDispatcher(loginForwardPath).forward(httpServletRequest, httpServletResponse);
212                 return;
213             }
214 
215             if (dbg)
216             {
217                 log.debug(METHOD + "Need Authentication: Redirecting to: " + getSecurityConfig().getLoginURL() + " from: " + originalURL);
218             }
219 
220             httpServletRequest.getSession().setAttribute(getSecurityConfig().getOriginalURLKey(), originalURL);
221             // only redirect if we can. if isCommited==true, there might have been a redirection requested by a LoginInterceptor, for instance.
222             if (!httpServletResponse.isCommitted())
223             {
224                 httpServletResponse.sendRedirect(RedirectUtils.getLoginUrl(httpServletRequest));
225             }
226             // Suppress the IDEA compile warning. This return is unnecessary, but good for safety against future code changes.
227             //noinspection UnnecessaryReturnStatement
228             return;
229         }
230         else
231         {
232             try
233             {
234                 chain.doFilter(req, res);
235             }
236             finally
237             {
238                 // clear the user from the context
239                 authenticationContext.clearUser();
240             }
241         }
242     }
243 
244     private boolean isPOST(HttpServletRequest httpServletRequest)
245     {
246         return "POST".equals(httpServletRequest.getMethod());
247     }
248 
249     private boolean hasJSessionCookie(final Cookie[] cookies)
250     {
251         if (cookies == null)
252         {
253             return false;
254         }
255 
256         for (Cookie cookie : cookies)
257         {
258             if (cookie.getName().equals("JSESSIONID"))
259             {
260                 return true;
261             }
262         }
263 
264         return false;
265     }
266 
267     protected SecurityConfig getSecurityConfig()
268     {
269         // ?? is this any useful, since we already initalize this in the init method ??
270         if (securityConfig == null)
271         {
272             securityConfig = (SecurityConfig) config.getServletContext().getAttribute(SecurityConfig.STORAGE_KEY);
273         }
274         return securityConfig;
275     }
276 
277     protected AuthenticationContext getAuthenticationContext()
278     {
279         return getSecurityConfig().getAuthenticationContext();
280     }
281 
282 }