1   package com.atlassian.seraph.filter;
2   
3   import com.atlassian.seraph.SecurityService;
4   import com.atlassian.seraph.auth.AuthenticationContext;
5   import com.atlassian.seraph.config.SecurityConfig;
6   import com.atlassian.seraph.config.SecurityConfigFactory;
7   import com.atlassian.seraph.util.RedirectUtils;
8   
9   import org.apache.log4j.Category;
10  
11  import java.io.IOException;
12  import java.security.Principal;
13  import java.util.HashSet;
14  import java.util.Set;
15  
16  import javax.servlet.Filter;
17  import javax.servlet.FilterChain;
18  import javax.servlet.FilterConfig;
19  import javax.servlet.ServletException;
20  import javax.servlet.ServletRequest;
21  import javax.servlet.ServletResponse;
22  import javax.servlet.http.HttpServletRequest;
23  import javax.servlet.http.HttpServletResponse;
24  
25  /**
26   * The SecurityFilter determines which roles are required for a given request by querying all of the Services.
27   *
28   * @see SecurityService
29   */
30  public class SecurityFilter implements Filter
31  {
32      private FilterConfig config = null;
33      private SecurityConfig securityConfig = null;
34  
35      private static final Category log = Category.getInstance(SecurityFilter.class);
36      private static final String ALREADY_FILTERED = "os_securityfilter_already_filtered";
37      public static final String ORIGINAL_URL = "atlassian.core.seraph.original.url";
38  
39      public void init(final FilterConfig config)
40      {
41          log.debug("SecurityFilter.init");
42          this.config = config;
43  
44          String configFileLocation = null;
45  
46          if (config.getInitParameter("config.file") != null)
47          {
48              configFileLocation = config.getInitParameter("config.file");
49              log.debug("Security config file location: " + configFileLocation);
50          }
51  
52          securityConfig = SecurityConfigFactory.getInstance(configFileLocation);
53          config.getServletContext().setAttribute(SecurityConfig.STORAGE_KEY, securityConfig);
54          log.debug("SecurityFilter.init completed successfully.");
55      }
56  
57      public void destroy()
58      {
59          log.debug("SecurityFilter.destroy");
60          // SER-129: securityConfig was seen in the wild to sometimes throw NPE.
61          if (securityConfig == null)
62          {
63              log.warn("Trying to destroy a SecurityFilter with null securityConfig.");
64          }
65          else
66          {
67              // TODO: Why do we set these variables to null? Note that the securityConfig still lives in the ServletContext attribute.
68              securityConfig.destroy();
69              securityConfig = null;
70          }
71          config = null;
72      }
73  
74      /**
75       * @deprecated Not needed in latest version of Servlet 2.3 API
76       */
77      // NOTE: Filter doesn't work with Orion 1.5.2 without this method
78      @Deprecated
79      public FilterConfig getFilterConfig()
80      {
81          return config;
82      }
83  
84      /**
85       * @deprecated Not needed in latest version of Servlet 2.3 API - replaced by init().
86       */
87      // NOTE: Filter doesn't work with Orion 1.5.2 without this method
88      @Deprecated
89      public void setFilterConfig(final FilterConfig filterConfig)
90      {
91          if (filterConfig != null)
92          {
93              init(filterConfig);
94          }
95      }
96  
97      public void doFilter(final ServletRequest req, final ServletResponse res, final FilterChain chain) throws IOException, ServletException
98      {
99          if ((req.getAttribute(ALREADY_FILTERED) != null) || !getSecurityConfig().getController().isSecurityEnabled())
100         {
101             chain.doFilter(req, res);
102             return;
103         }
104         else
105         {
106             req.setAttribute(ALREADY_FILTERED, Boolean.TRUE);
107         }
108 
109         final String METHOD = "doFilter : ";
110         final boolean dbg = log.isDebugEnabled();
111 
112         // Try and get around Orion's bug when redeploying
113         // it seems that filters are not called in the correct order
114         if (req.getAttribute(BaseLoginFilter.ALREADY_FILTERED) == null)
115         {
116             log.warn(METHOD + "LoginFilter not yet applied to this request - terminating filter chain");
117             return;
118         }
119 
120         final HttpServletRequest request = (HttpServletRequest) req;
121         final HttpServletResponse response = (HttpServletResponse) res;
122 
123         final String originalURL = request.getServletPath() + (request.getPathInfo() == null ? "" : request.getPathInfo()) + (request.getQueryString() == null ? "" : "?" + request.getQueryString());
124 
125         // store the original URL as a request attribute anyway - often useful for pages to access it (ie login links)
126         request.setAttribute(SecurityFilter.ORIGINAL_URL, originalURL);
127         if (dbg)
128         {
129             log.debug(METHOD + "Storing the originally requested URL (" + SecurityFilter.ORIGINAL_URL + "=" + originalURL + ")");
130         }
131 
132         final Set<String> requiredRoles = new HashSet<String>();
133 
134         // loop through loaded services and get required roles
135         for (final SecurityService service : getSecurityConfig().getServices())
136         {
137             final Set<String> serviceRoles = service.getRequiredRoles(request);
138             requiredRoles.addAll(serviceRoles);
139         }
140 
141         if (dbg)
142         {
143             log.debug(METHOD + "requiredRoles = " + requiredRoles);
144         }
145 
146         // whether this URL needs authorisation
147         boolean needAuth = false;
148 
149         // try to get the user (for cookie logins)
150         final Principal user = getSecurityConfig().getAuthenticator().getUser(request, response);
151 
152         // only redirect if we don't use basic HTTP authentication
153         final boolean basicAuthentication = RedirectUtils.isBasicAuthentication(request, getSecurityConfig().getAuthType());
154         // if we are doing basic authentication, then we don't want to continue the filter chain - we need to commit
155         // the response to allow for the authentication challenge mechanism
156         if (basicAuthentication && (user == null))
157         {
158             if (dbg)
159             {
160                 log.debug(METHOD + "Basic authentication requested.");
161             }
162             return;
163         }
164 
165         // set the user in the context
166         if (dbg)
167         {
168             log.debug(METHOD + "Setting Auth Context to be '" + (user == null ? "anonymous " : user.getName()) + "'");
169         }
170         getAuthenticationContext().setUser(user);
171 
172         // check if the current user has all required permissions
173         // if there is no current user, request.isUserInRole() always returns false so this works
174         for (final Object element : requiredRoles)
175         {
176             final String role = (String) element;
177 
178             // 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
179             // was : if (!securityConfig.getAuthenticator().isUserInRole(request, role))
180             if (!getSecurityConfig().getRoleMapper().hasRole(user, request, role))
181             {
182                 log.info(METHOD + "'" + user + "' needs (and lacks) role '" + role + "' to access " + originalURL);
183                 needAuth = true;
184             }
185         }
186 
187         // check if we're at the signon page, in which case do not auth
188         if ((request.getServletPath() != null) && request.getServletPath().equals(getSecurityConfig().getLoginURL()))
189         {
190             if (dbg)
191             {
192                 log.debug(METHOD + "Login page requested so no additional authorization required.");
193             }
194             needAuth = false;
195         }
196 
197         // if we need to authenticate, store current URL and forward
198         if (needAuth)
199         {
200             if (dbg)
201             {
202                 log.debug(METHOD + "Need Authentication: Redirecting to: " + getSecurityConfig().getLoginURL() + " from: " + originalURL);
203             }
204 
205             request.getSession().setAttribute(getSecurityConfig().getOriginalURLKey(), originalURL);
206             // only redirect if we can. if isCommited==true, there might have been a redirection requested by a LoginInterceptor, for instance.
207             if (!response.isCommitted())
208             {
209                 response.sendRedirect(RedirectUtils.getLoginUrl(request));
210             }
211             // Suppress the IDEA compile warning. This return is unnecessary, but good for safety against future code changes.
212             //noinspection UnnecessaryReturnStatement
213             return;
214         }
215         else
216         {
217             try
218             {
219                 chain.doFilter(req, res);
220             }
221             finally
222             {
223                 // clear the user from the context
224                 getAuthenticationContext().clearUser();
225             }
226         }
227     }
228 
229     protected SecurityConfig getSecurityConfig()
230     {
231         // ?? is this any useful, since we already initalize this in the init method ??
232         if (securityConfig == null)
233         {
234             securityConfig = (SecurityConfig) config.getServletContext().getAttribute(SecurityConfig.STORAGE_KEY);
235         }
236         return securityConfig;
237     }
238 
239     protected AuthenticationContext getAuthenticationContext()
240     {
241         return getSecurityConfig().getAuthenticationContext();
242     }
243 
244 }