10a1277d33f34916d3167ebb66306bfa6f6d0961
[moon.git] /
1 /*
2  * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8
9 package org.opendaylight.aaa.federation;
10
11 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
12 import static org.opendaylight.aaa.federation.FederationEndpoint.AUTH_CLAIM;
13
14 import java.io.IOException;
15 import java.io.UnsupportedEncodingException;
16 import java.util.Enumeration;
17 import java.util.HashMap;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Set;
21 import javax.servlet.Filter;
22 import javax.servlet.FilterChain;
23 import javax.servlet.FilterConfig;
24 import javax.servlet.ServletException;
25 import javax.servlet.ServletRequest;
26 import javax.servlet.ServletResponse;
27 import javax.servlet.http.HttpServletRequest;
28 import javax.servlet.http.HttpServletResponse;
29 import org.opendaylight.aaa.api.Claim;
30 import org.opendaylight.aaa.api.ClaimAuth;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33
34 /**
35  * A generic {@link Filter} for {@link ClaimAuth} implementations.
36  * <p>
37  * This filter trusts any authentication metadata bound to a request. A request
38  * with fake authentication claims could be forged by an attacker and submitted
39  * to one of the Connector ports the engine is listening on and we would blindly
40  * accept the forged information in this filter. Therefore it is vital we only
41  * accept authentication claims from a trusted proxy. It is incumbent upon the
42  * site administrator to dedicate specific connector ports on which previously
43  * authenticated requests from a trusted proxy will be sent to and to assure
44  * only a trusted proxy can connect to that port. The site administrator must
45  * enumerate those ports in the configuration. We reject any request which did
46  * not originate on one of the configured secure proxy ports.
47  *
48  * @author liemmn
49  *
50  */
51 public class ClaimAuthFilter implements Filter {
52     private static final Logger LOG = LoggerFactory.getLogger(ClaimAuthFilter.class);
53
54     private static final String CGI_AUTH_TYPE = "AUTH_TYPE";
55     private static final String CGI_PATH_INFO = "PATH_INFO";
56     private static final String CGI_PATH_TRANSLATED = "PATH_TRANSLATED";
57     private static final String CGI_QUERY_STRING = "QUERY_STRING";
58     private static final String CGI_REMOTE_ADDR = "REMOTE_ADDR";
59     private static final String CGI_REMOTE_HOST = "REMOTE_HOST";
60     private static final String CGI_REMOTE_PORT = "REMOTE_PORT";
61     private static final String CGI_REMOTE_USER = "REMOTE_USER";
62     private static final String CGI_REMOTE_USER_GROUPS = "REMOTE_USER_GROUPS";
63     private static final String CGI_REQUEST_METHOD = "REQUEST_METHOD";
64     private static final String CGI_SCRIPT_NAME = "SCRIPT_NAME";
65     private static final String CGI_SERVER_PROTOCOL = "SERVER_PROTOCOL";
66
67     static final String UNAUTHORIZED_PORT_ERR = "Unauthorized proxy port";
68
69     @Override
70     public void init(FilterConfig fc) throws ServletException {
71     }
72
73     @Override
74     public void destroy() {
75     }
76
77     @Override
78     public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
79             throws IOException, ServletException {
80         Set<Integer> secureProxyPorts;
81         int localPort;
82
83         // Check to see if we are communicated over an authorized port or not
84         secureProxyPorts = FederationConfiguration.instance().secureProxyPorts();
85         localPort = req.getLocalPort();
86         if (!secureProxyPorts.contains(localPort)) {
87             ((HttpServletResponse) resp).sendError(SC_UNAUTHORIZED, UNAUTHORIZED_PORT_ERR);
88             return;
89         }
90
91         // Let's do some transformation!
92         List<ClaimAuth> claimAuthCollection = ServiceLocator.getInstance().getClaimAuthCollection();
93         for (ClaimAuth ca : claimAuthCollection) {
94             Claim claim = ca.transform(claims((HttpServletRequest) req));
95             if (claim != null) {
96                 req.setAttribute(AUTH_CLAIM, claim);
97                 // No need to do further transformation since it has been done
98                 break;
99             }
100         }
101         chain.doFilter(req, resp);
102     }
103
104     // Extract attributes and headers out of the request
105     private Map<String, Object> claims(HttpServletRequest req) {
106         String name;
107         Object objectValue;
108         String stringValue;
109         Map<String, Object> claims = new HashMap<>();
110
111         /*
112          * Tomcat has a bug/feature, not all attributes are enumerated by
113          * getAttributeNames() therefore getAttributeNames() cannot be used to
114          * obtain the full set of attributes. However if you know the name of
115          * the attribute a priori you can call getAttribute() and obtain the
116          * value. Therefore we maintain a list of attribute names
117          * (httpAttributes) which will be used to call getAttribute() with so we
118          * don't miss essential attributes.
119          *
120          * This is the Tomcat bug, note it is marked WONTFIX. Bug 25363 -
121          * request.getAttributeNames() not working properly Status: RESOLVED
122          * WONTFIX https://issues.apache.org/bugzilla/show_bug.cgi?id=25363
123          *
124          * The solution adopted by Tomcat is to document the behavior in the
125          * "The Apache Tomcat Connector - Reference Guide" under the JkEnvVar
126          * property where is says:
127          *
128          * You can retrieve the variables on Tomcat as request attributes via
129          * request.getAttribute(attributeName). Note that the variables send via
130          * JkEnvVar will not be listed in request.getAttributeNames().
131          */
132
133         // Capture attributes which can be enumerated ...
134         @SuppressWarnings("unchecked")
135         Enumeration<String> attrs = req.getAttributeNames();
136         while (attrs.hasMoreElements()) {
137             name = attrs.nextElement();
138             objectValue = req.getAttribute(name);
139             if (objectValue instanceof String) {
140                 // metadata might be i18n, assume UTF8 and decode
141                 stringValue = decodeUTF8((String) objectValue);
142                 objectValue = stringValue;
143             }
144             claims.put(name, objectValue);
145         }
146
147         // Capture specific attributes which cannot be enumerated ...
148         for (String attr : FederationConfiguration.instance().httpAttributes()) {
149             name = attr;
150             objectValue = req.getAttribute(name);
151             if (objectValue instanceof String) {
152                 // metadata might be i18n, assume UTF8 and decode
153                 stringValue = decodeUTF8((String) objectValue);
154                 objectValue = stringValue;
155             }
156             claims.put(name, objectValue);
157         }
158
159         /*
160          * In general we should not utilize HTTP headers as validated security
161          * assertions because they are too easy to forge. Therefore in general
162          * we don't include HTTP headers, however in certain circumstances
163          * specific headers may be acceptable, thus we permit an admin to
164          * configure the capture of specific headers.
165          */
166         for (String header : FederationConfiguration.instance().httpHeaders()) {
167             claims.put(header, req.getHeader(header));
168         }
169
170         // Capture standard CGI variables...
171         claims.put(CGI_AUTH_TYPE, req.getAuthType());
172         claims.put(CGI_PATH_INFO, req.getPathInfo());
173         claims.put(CGI_PATH_TRANSLATED, req.getPathTranslated());
174         claims.put(CGI_QUERY_STRING, req.getQueryString());
175         claims.put(CGI_REMOTE_ADDR, req.getRemoteAddr());
176         claims.put(CGI_REMOTE_HOST, req.getRemoteHost());
177         claims.put(CGI_REMOTE_PORT, req.getRemotePort());
178         // remote user might be i18n, assume UTF8 and decode
179         claims.put(CGI_REMOTE_USER, decodeUTF8(req.getRemoteUser()));
180         claims.put(CGI_REMOTE_USER_GROUPS, req.getAttribute(CGI_REMOTE_USER_GROUPS));
181         claims.put(CGI_REQUEST_METHOD, req.getMethod());
182         claims.put(CGI_SCRIPT_NAME, req.getServletPath());
183         claims.put(CGI_SERVER_PROTOCOL, req.getProtocol());
184
185         if (LOG.isDebugEnabled()) {
186             LOG.debug("ClaimAuthFilter claims = {}", claims.toString());
187         }
188
189         return claims;
190     }
191
192     /**
193      * Decode from UTF-8, return Unicode.
194      *
195      * If we're unable to UTF-8 decode the string the fallback is to return the
196      * string unmodified and log a warning.
197      *
198      * Some data, especially metadata attached to a user principal may be
199      * internationalized (i18n). The classic examples are the user's name,
200      * location, organization, etc. We need to be able to read this metadata and
201      * decode it into unicode characters so that we properly handle i18n string
202      * values.
203      *
204      * One of the the prolems is we often don't know the encoding (i.e. charset)
205      * of the string. RFC-5987 is supposed to define how non-ASCII values are
206      * transmitted in HTTP headers, this is a follow on from the work in
207      * RFC-2231. However at the time of this writing these RFC's are not
208      * implemented in the Servlet Request classes. Not only are these RFC's
209      * unimplemented but they are specific to HTTP headers, much of our metadata
210      * arrives via attributes as opposed to being in a header.
211      *
212      * Note: ASCII encoding is a subset of UTF-8 encoding therefore any strings
213      * which are pure ASCII will decode from UTF-8 just fine. However on the
214      * other hand Latin-1 (ISO-8859-1) encoding is not compatible with UTF-8 for
215      * code points in the range 128-255 (i.e. beyond 7-bit ascii). ISO-8859-1 is
216      * the default encoding for HTTP and HTML 4, however the consensus is the
217      * use of ISO-8859-1 was a mistake and Unicode with UTF-8 encoding is now
218      * the norm. If a string value is transmitted encoded in ISO-8859-1
219      * contaiing code points in the range 128-255 and we try to UTF-8 decode it
220      * it will either not be the correct decoded string or it will throw a
221      * decoding exception.
222      *
223      * Conventional practice at the moment is for the sending side to encode
224      * internationalized values in UTF-8 with the receving end decoding the
225      * value back from UTF-8. We do not expect the use of ISO-8859-1 on these
226      * attributes. However due to peculiarities of the Java String
227      * implementation we have to specify the raw bytes are encoded in ISO-8859-1
228      * just to get back the raw bytes to be able to feed into the UTF-8 decoder.
229      * This doesn't seem right but it is because we need the full 8-bit byte and
230      * the only way to say "unmodified 8-bit bytes" in Java is to call it
231      * ISO-8859-1. Ugh!
232      *
233      * @param string
234      *            The input string in UTF-8 to be decoded.
235      * @return Unicode string
236      */
237     private String decodeUTF8(String string) {
238         if (string == null) {
239             return null;
240         }
241         try {
242             return new String(string.getBytes("ISO8859-1"), "UTF-8");
243         } catch (UnsupportedEncodingException e) {
244             LOG.warn("Unable to UTF-8 decode: ", string, e);
245             return string;
246         }
247     }
248
249 }