View Javadoc

1   package com.atlassian.seraph.filter;
2   
3   import com.atlassian.security.auth.trustedapps.ApplicationCertificate;
4   import com.atlassian.security.auth.trustedapps.CurrentApplication;
5   import com.atlassian.security.auth.trustedapps.DefaultEncryptedCertificate;
6   import com.atlassian.security.auth.trustedapps.InvalidCertificateException;
7   import com.atlassian.security.auth.trustedapps.TransportErrorMessage;
8   import com.atlassian.security.auth.trustedapps.TrustedApplication;
9   import com.atlassian.security.auth.trustedapps.TrustedApplicationUtils;
10  import com.atlassian.security.auth.trustedapps.TrustedApplicationsManager;
11  import com.atlassian.security.auth.trustedapps.UserResolver;
12  import com.atlassian.seraph.auth.DefaultAuthenticator;
13  import com.atlassian.seraph.auth.RoleMapper;
14  import com.atlassian.seraph.config.SecurityConfigFactory;
15  
16  import org.apache.log4j.Logger;
17  import org.bouncycastle.util.encoders.Base64;
18  
19  import java.io.IOException;
20  import java.io.OutputStreamWriter;
21  import java.io.UnsupportedEncodingException;
22  import java.io.Writer;
23  import java.security.Principal;
24  import java.security.PublicKey;
25  
26  import javax.servlet.Filter;
27  import javax.servlet.FilterChain;
28  import javax.servlet.FilterConfig;
29  import javax.servlet.ServletException;
30  import javax.servlet.ServletRequest;
31  import javax.servlet.ServletResponse;
32  import javax.servlet.http.HttpServletRequest;
33  import javax.servlet.http.HttpServletResponse;
34  
35  /**
36   * This filter serves two purposes: 1. Authenticates requests from trusted applications if the right certificate is
37   * present in the request 2. Returns the UID and public key of this application upon request so other servers can
38   * establish trusted relationship with this application as a client
39   * <p>
40   * For the first purpose, the filter will intercept any calls to a page '/admin/appTrustCertificate'. Directory
41   * structure of the request will be ignored. The returned page will contain 2 lines:
42   * <ul>
43   * <li> ID </li>
44   * <li> public key BASE64 encoded </li>
45   * </ul>
46   * <p>
47   * For the second purpose the following header parameters must be present and valid:
48   * {@link CurrentApplication#HEADER_TRUSTED_APP_CERT} {@link CurrentApplication#HEADER_TRUSTED_APP_ID}
49   * <p>
50   * If the authentication should fail a message will be set in the response header:
51   * {@link CurrentApplication#HEADER_TRUSTED_APP_ERROR}
52   */
53  public class TrustedApplicationsFilter implements Filter
54  {
55      private static final Logger log = Logger.getLogger(TrustedApplicationsFilter.class);
56  
57      private static final class Status
58      {
59          static final String ERROR = "ERROR";
60          static final String OK = "OK";
61      }
62  
63      private final CertificateServer certificateServer;
64      private final Authenticator authenticator;
65  
66      private FilterConfig filterConfig = null;
67  
68      /*
69       * used to get the static seraph-config.xml driven RoleMapper
70       */
71      // /CLOVER:OFF
72      public TrustedApplicationsFilter(TrustedApplicationsManager appManager, UserResolver resolver)
73      {
74          this(appManager, resolver, SecurityConfigFactory.getInstance().getRoleMapper());
75      }
76  
77      // /CLOVER:ON
78  
79      public TrustedApplicationsFilter(TrustedApplicationsManager appManager, UserResolver resolver, RoleMapper roleMapper)
80      {
81          this(new CertificateServerImpl(appManager), new AuthenticatorImpl(appManager, resolver, roleMapper));
82      }
83  
84      TrustedApplicationsFilter(CertificateServer certificateServer, Authenticator authenticator)
85      {
86          notNull("certificateServer", certificateServer);
87          notNull("authenticator", authenticator);
88          this.certificateServer = certificateServer;
89          this.authenticator = authenticator;
90      }
91  
92      public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException
93      {
94          // if this is a certificate request
95          // serve the certificate back and return
96          HttpServletRequest request = (HttpServletRequest) req;
97          HttpServletResponse response = (HttpServletResponse) res;
98  
99          if (getPathInfo(request).endsWith("/admin/appTrustCertificate"))
100         {
101             response.setContentType("text/plain");
102             certificateServer.writeCertificate(new OutputStreamWriter(response.getOutputStream()));
103             return;
104         }
105 
106         boolean isTrustedAppCall = false;
107         // for all other requests - let the parent class do its job
108         if (request.getAttribute(BaseLoginFilter.OS_AUTHSTATUS_KEY) == null)
109         {
110             String status = authenticate((HttpServletRequest) req, (HttpServletResponse) res);
111             if (BaseLoginFilter.LOGIN_SUCCESS.equals(status))
112             {
113                 request.setAttribute(BaseLoginFilter.OS_AUTHSTATUS_KEY, status);
114                 response.setHeader(TrustedApplicationUtils.Header.Response.STATUS, Status.OK);
115                 isTrustedAppCall = true;
116             }
117         }
118         try
119         {
120             chain.doFilter(request, res);
121         }
122         finally
123         {
124             if (isTrustedAppCall && request.getSession(false) != null)
125             {
126                 request.getSession().invalidate();
127             }
128         }
129     }
130 
131     public String authenticate(HttpServletRequest request, HttpServletResponse response)
132     {
133         final Authenticator.Result result = authenticator.authenticate(request);
134 
135         switch (result.getStatus().getOrdinal())
136         {
137             case Authenticator.Result.Status.Constants.NO_ATTEMPT:
138                 return BaseLoginFilter.LOGIN_NOATTEMPT;
139 
140             case Authenticator.Result.Status.Constants.FAILED:
141                 setFailureHeader(response, result.getMessage());
142                 return BaseLoginFilter.LOGIN_FAILED;
143 
144             case Authenticator.Result.Status.Constants.ERROR:
145                 setFailureHeader(response, result.getMessage());
146                 return BaseLoginFilter.LOGIN_ERROR;
147 
148             case Authenticator.Result.Status.Constants.SUCCESS:
149                 request.getSession().setAttribute(DefaultAuthenticator.LOGGED_IN_KEY, result.getUser());
150                 request.getSession().setAttribute(DefaultAuthenticator.LOGGED_OUT_KEY, null);
151                 return BaseLoginFilter.LOGIN_SUCCESS;
152 
153                 // /CLOVER:OFF
154             default:
155                 throw new IllegalStateException("Unknown result: " + result.getStatus().getOrdinal());
156                 // /CLOVER:ON
157         }
158     }
159 
160     protected String getPathInfo(HttpServletRequest request)
161     {
162         String context = request.getContextPath();
163         String uri = request.getRequestURI();
164         if (context != null && context.length() > 0)
165         {
166             return uri.substring(context.length());
167         }
168         else
169         {
170             return uri;
171         }
172     }
173 
174     private void setFailureHeader(HttpServletResponse response, String message)
175     {
176         response.setHeader(TrustedApplicationUtils.Header.Response.STATUS, Status.ERROR);
177         response.addHeader(TrustedApplicationUtils.Header.Response.ERROR, message);
178         if (log.isInfoEnabled())
179         {
180             log.info(message, new RuntimeException(message));
181         }
182     }
183 
184     public void init(FilterConfig config)
185     {
186         this.filterConfig = config;
187     }
188 
189     public void destroy()
190     {
191         filterConfig = null;
192     }
193 
194     /** @deprecated Not needed in latest version of Servlet 2.3 API */
195     public FilterConfig getFilterConfig()
196     {
197         return filterConfig;
198     }
199 
200     /** @deprecated Not needed in latest version of Servlet 2.3 API - replaced by init(). */
201     public void setFilterConfig(FilterConfig filterConfig)
202     {
203         if (filterConfig != null) // it seems that Orion 1.5.2 calls this with a null config.
204         {
205             init(filterConfig);
206         }
207     }
208 
209     /**
210      * serve the CurrentApplication's certificate
211      */
212     interface CertificateServer
213     {
214         void writeCertificate(Writer writer) throws IOException;
215     }
216 
217     static class CertificateServerImpl implements CertificateServer
218     {
219         final TrustedApplicationsManager appManager;
220 
221         CertificateServerImpl(TrustedApplicationsManager appManager)
222         {
223             this.appManager = appManager;
224         }
225 
226         public void writeCertificate(Writer writer) throws IOException
227         {
228             CurrentApplication currentApplication = appManager.getCurrentApplication();
229             PublicKey publicKey = currentApplication.getPublicKey();
230 
231             try
232             {
233                 writer.write(currentApplication.getID());
234                 writer.write("\n");
235 
236                 byte[] key = publicKey.getEncoded();
237                 writer.write(new String(Base64.encode(key), TrustedApplicationUtils.Constant.CHARSET_NAME));
238                 writer.write("\n");
239                 writer.write(TrustedApplicationUtils.Constant.VERSION.toString());
240                 writer.write("\n");
241                 writer.write(TrustedApplicationUtils.Constant.MAGIC);
242                 writer.flush();
243             }
244             catch (UnsupportedEncodingException ex)
245             {
246                 throw new AssertionError(ex);
247             }
248             catch (IOException e)
249             {
250                 throw new RuntimeException(e);
251             }
252         }
253     }
254 
255     /**
256      * Authenticate a TrustedApplication request
257      */
258     interface Authenticator
259     {
260         Result authenticate(HttpServletRequest request);
261 
262         static class Result
263         {
264             private final Status status;
265             private final TransportErrorMessage message;
266             private final Principal user;
267 
268             Result(Status status)
269             {
270                 this(status, null, null);
271             }
272 
273             Result(Status status, TransportErrorMessage message)
274             {
275                 this(status, message, null);
276 
277                 notNull("message", message);
278             }
279 
280             Result(Status status, Principal principal)
281             {
282                 this(status, null, principal);
283 
284                 notNull("principal", principal);
285             }
286 
287             Result(Status status, TransportErrorMessage message, Principal user)
288             {
289                 if (status == null)
290                 {
291                     throw new IllegalArgumentException("status");
292                 }
293                 this.status = status;
294                 this.message = message;
295                 this.user = user;
296             }
297 
298             public Status getStatus()
299             {
300                 return status;
301             }
302 
303             public String getMessage()
304             {
305                 return message.toString();
306             }
307 
308             public Principal getUser()
309             {
310                 return user;
311             }
312 
313             static final class Status
314             {
315                 /**
316                  * Necessary to declare these as int constants as javac is too dumb to realise that a final member of a
317                  * final class's static constants is a constant
318                  */
319                 static final class Constants
320                 {
321                     static final int SUCCESS = 0;
322                     static final int FAILED = 1;
323                     static final int ERROR = 2;
324                     static final int NO_ATTEMPT = 3;
325                 }
326 
327                 static final Status SUCCESS = new Status(Constants.SUCCESS, "success");
328                 static final Status FAILED = new Status(Constants.FAILED, "failed");
329                 static final Status ERROR = new Status(Constants.ERROR, "error");
330                 static final Status NO_ATTEMPT = new Status(Constants.NO_ATTEMPT, "no attempt");
331 
332                 private final int ordinal;
333                 private final String name;
334 
335                 private Status(int ordinal, String name)
336                 {
337                     this.ordinal = ordinal;
338                     this.name = name;
339                 }
340 
341                 int getOrdinal()
342                 {
343                     return ordinal;
344                 }
345 
346                 public String toString()
347                 {
348                     return name;
349                 }
350             }
351 
352             static class NoAttempt extends Result
353             {
354                 NoAttempt()
355                 {
356                     super(Status.NO_ATTEMPT);
357                 }
358             }
359 
360             static class Error extends Result
361             {
362                 Error(TransportErrorMessage message)
363                 {
364                     super(Status.ERROR, message);
365                 }
366             }
367 
368             static class Failure extends Result
369             {
370                 Failure(TransportErrorMessage message)
371                 {
372                     super(Status.FAILED, message);
373                 }
374             }
375 
376             static class Success extends Result
377             {
378                 public Success(Principal principal)
379                 {
380                     super(Status.SUCCESS, principal);
381                 }
382             }
383         }
384     }
385 
386     static class AuthenticatorImpl implements Authenticator
387     {
388         final TrustedApplicationsManager appManager;
389         final UserResolver resolver;
390         final RoleMapper roleMapper;
391 
392         AuthenticatorImpl(TrustedApplicationsManager appManager, UserResolver resolver, RoleMapper roleMapper)
393         {
394             this.appManager = appManager;
395             this.resolver = resolver;
396             this.roleMapper = roleMapper;
397         }
398 
399         public Result authenticate(HttpServletRequest request)
400         {
401             final String certStr = request.getHeader(TrustedApplicationUtils.Header.Request.CERTIFICATE);
402             if (isBlank(certStr))
403             {
404                 return new Result.NoAttempt();
405             }
406 
407             final String id = request.getHeader(TrustedApplicationUtils.Header.Request.ID);
408             if (isBlank(id))
409             {
410                 return new Result.Error(new TransportErrorMessage.ApplicationIdNotFoundInRequest());
411             }
412 
413             final String key = request.getHeader(TrustedApplicationUtils.Header.Request.SECRET_KEY);
414             if (isBlank(key))
415             {
416                 return new Result.Error(new TransportErrorMessage.SecretKeyNotFoundInRequest());
417             }
418 
419             final String magicNumber = request.getHeader(TrustedApplicationUtils.Header.Request.MAGIC);
420             // magic number validation is only done from protocol version 2, version 1 had no version header
421             final String version = request.getHeader(TrustedApplicationUtils.Header.Request.VERSION);
422             final Integer protocolVersion;
423             try
424             {
425                 protocolVersion = (!isBlank(version)) ? new Integer(version) : null;
426             }
427             catch (NumberFormatException e)
428             {
429                 return new Result.Error(new TransportErrorMessage.BadProtocolVersion(version));
430             }
431 
432             // note, this code only handles non-null version - doesn't differentiate between 1 & 2
433             if (protocolVersion != null)
434             {
435                 if (isBlank(magicNumber))
436                 {
437                     return new Result.Error(new TransportErrorMessage.MagicNumberNotFoundInRequest());
438                 }
439             }
440 
441             TrustedApplication app = appManager.getTrustedApplication(id);
442             if (app == null)
443             {
444                 return new Result.Failure(new TransportErrorMessage.ApplicationUnknown(id));
445             }
446 
447             final ApplicationCertificate certificate;
448             try
449             {
450                 certificate = app.decode(new DefaultEncryptedCertificate(id, key, certStr, protocolVersion, magicNumber), request);
451             }
452             catch (InvalidCertificateException ex)
453             {
454                 log.warn("Failed to login trusted application: " + app.getID() + " due to: " + ex);
455                 // debug for stacktrace, no need for isDebugEnabled check as there is no string concatenation
456                 log.debug("Failed to login trusted application cause", ex);
457                 return new Result.Error(ex.getTransportErrorMessage());
458             }
459             final Principal user = resolver.resolve(certificate);
460             if (user == null)
461             {
462                 log.warn("User '" + certificate.getUserName() + "' referenced by trusted application: '" + app.getID() + "' is not found.");
463                 return new Result.Failure(new TransportErrorMessage.UserUnknown(certificate.getUserName()));
464             }
465             else if (!roleMapper.canLogin(user, request))
466             {
467                 // user exists but is not allowed to login
468                 log.warn("User '" + certificate.getUserName() + "' referenced by trusted application: '" + app.getID() + "' cannot login.");
469                 return new Result.Failure(new TransportErrorMessage.PermissionDenied());
470             }
471 
472             return new Result.Success(user);
473         }
474     }
475 
476     private static boolean isBlank(String input)
477     {
478         return (input == null) || input.trim().length() == 0;
479     }
480 
481     private static void notNull(String name, Object notNull)
482     {
483         if (notNull == null)
484         {
485             throw new IllegalArgumentException(name + " should not be null");
486         }
487     }
488 }