View Javadoc

1   /*
2    * Copyright (c) 2007 Kathryn Huxtable
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *
16   * $Id: UserAttributesImpl.java 1087 2007-04-10 15:39:08Z kathryn5 $
17   */
18  package org.kathrynhuxtable.middleware.shibshim.filter;
19  
20  import java.io.ByteArrayInputStream;
21  import java.io.IOException;
22  import java.util.ArrayList;
23  import java.util.Enumeration;
24  import java.util.HashMap;
25  import java.util.Iterator;
26  import java.util.Map;
27  import java.util.Vector;
28  
29  import javax.xml.parsers.DocumentBuilderFactory;
30  import javax.xml.parsers.ParserConfigurationException;
31  
32  import org.apache.log4j.Logger;
33  import org.w3c.dom.Attr;
34  import org.w3c.dom.Document;
35  import org.w3c.dom.Element;
36  import org.w3c.dom.NamedNodeMap;
37  import org.w3c.dom.Node;
38  import org.xml.sax.SAXException;
39  
40  /**
41   * Attributes object for the Shibboleth shim filter. Hold the collection of
42   * attributes and provide parsing method.
43   */
44  public class UserAttributesImpl extends HashMap implements UserAttributes {
45      /**
46       * Logger for error/information/debugging.
47       */
48      private static Logger       log                 = Logger.getLogger(UserAttributesImpl.class.getName());
49  
50      /**
51       * Initial capacity for hashes.
52       */
53      private static final int    INITIAL_CAPACITY    = 5;
54  
55      /**
56       * Text of the DTD for the XML assertion. I'm not convinced that we need to
57       * verify this.
58       */
59      private static final String DTD_TEXT            = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE  Assertion [\n"
60                                                              + "<!ELEMENT  Assertion (AttributeStatement)>\n<!ATTLIST  Assertion\n"
61                                                              + "  IssueInstant CDATA #REQUIRED\n  AuthenticationInstant CDATA #REQUIRED\n"
62                                                              + "  Issuer CDATA #REQUIRED\n  Handle CDATA #REQUIRED>\n"
63                                                              + "<!ELEMENT  AttributeStatement (Attribute*)>\n<!ELEMENT  Attribute (AttributeValue+)>\n"
64                                                              + "<!ATTLIST  Attribute\n  AttributeName CDATA #REQUIRED>\n"
65                                                              + "<!ELEMENT  AttributeValue (#PCDATA | AttributeStatement)*>\n]>\n";
66  
67      // Constants used while parsing the user information retrieved in XML
68      // format.
69  
70      /**
71       * Name of the &lt;Attribute&gt; element.
72       */
73      private static final String ATTRIBUTE_TAG       = "Attribute";
74  
75      /**
76       * Name of the &lt;AttributeName&gt; attribute.
77       */
78      private static final String ATTRIBUTE_NAME_TAG  = "AttributeName";
79  
80      /**
81       * Name of the &lt;AttributeValue&gt; element.
82       */
83      private static final String ATTRIBUTE_VALUE_TAG = "AttributeValue";
84  
85      /**
86       * Name of the &lt;Assertion&gt; element.
87       */
88      private static final String ASSERTION_TAG       = "Assertion";
89  
90      /**
91       * Name of the &lt;AttributeStatement&gt; element.
92       */
93      private static final String ATTR_STMT_TAG       = "AttributeStatement";
94  
95      /**
96       * Indicates whether object has user attributes, as distinct from
97       * configuration attributes.
98       */
99      private boolean             hasAttributes;
100 
101     /**
102      * Map mapping incoming Shibboleth attributes to application attributes. Any
103      * attribute not in the key set will be ignored. Case is ignored for keys
104      * but not for values.
105      */
106     private Map                 attributeMap;
107 
108     /**
109      * Make new object. Sets hasAttributes false.
110      * 
111      * @param attributeMap
112      *            the map mapping incoming Shibboleth attribute names to
113      *            internal attribute names. Any attributes not listed here will
114      *            be ignored.
115      */
116     public UserAttributesImpl(Map attributeMap) {
117         super();
118         hasAttributes = false;
119         this.attributeMap = attributeMap;
120     }
121 
122     /**
123      * {@inheritDoc}
124      */
125     public boolean isMappedAttribute(String name) {
126         return attributeMap.containsValue(name.toLowerCase());
127     }
128 
129     /**
130      * {@inheritDoc}
131      */
132     public String getHeader(String name) {
133         ArrayList list = null;
134         name = name.toLowerCase();
135         if (name.startsWith("config-")) {
136             return (String) get(name);
137         }
138         list = (ArrayList) get(name);
139         log.debug("Got list = " + list + ", size = " + (list == null ? "?" : (new Integer(list.size())).toString()));
140         Object res = list == null || list.size() == 0 ? null : list.get(0);
141         if (res instanceof HashMap) {
142             // This is a kuorganizationalrole element.
143             HashMap subMap = (HashMap) res;
144             Iterator itr = (subMap.keySet()).iterator();
145             String subTable = null;
146             subTable = "<Assertion>";
147             while (itr.hasNext()) {
148                 String subName = (String) itr.next();
149                 subTable += "<Attribute name=\"" + subName + "\">" + subMap.get(subName) + "</Attribute>";
150             }
151             subTable += "</Assertion>";
152             return subTable;
153         }
154         return (String) res;
155     }
156 
157     /**
158      * {@inheritDoc}
159      */
160     public Enumeration getHeaders(String name) {
161         ArrayList list = null;
162 
163         name = name.toLowerCase();
164 
165         if (name.startsWith("config-")) {
166             Vector res = new Vector();
167             res.add(get(name));
168             return res.elements();
169         } else {
170             list = (ArrayList) get(name);
171             if (list == null) {
172                 return (new Vector()).elements();
173             }
174         }
175 
176         return (new Vector(list)).elements();
177     }
178 
179     /**
180      * {@inheritDoc}
181      */
182     public Enumeration getHeaderNames() {
183         Vector v = new Vector();
184         Iterator keys = keySet().iterator();
185         while (keys.hasNext()) {
186             String key = (String) keys.next();
187             v.add(key);
188         }
189         return v.elements();
190     }
191 
192     /**
193      * Return whether we have user attributes or just configuration attributes.
194      * 
195      * @return true if we have user attributes
196      */
197     public boolean hasAttributes() {
198         return hasAttributes;
199     }
200 
201     /**
202      * Clear the user attributes, leaving the configuration attributes alone.
203      */
204     public void clearUserInfo() {
205         Iterator keys = this.keySet().iterator();
206         Vector remMap = new Vector();
207         int track = 0;
208         while (keys.hasNext()) {
209             String key = (String) keys.next();
210             if (!key.startsWith("config-")) {
211                 remMap.add(key);
212             }
213         }
214         while (track < remMap.size()) {
215             this.remove(remMap.get(track));
216             track++;
217         }
218         hasAttributes = false;
219     }
220 
221     /**
222      * Parse the user info string from the Shibboleth Shim server into a hash
223      * map object.
224      * 
225      * @param userInfo
226      *            String containing the user info in XML format.
227      * @throws UserInfoException
228      *             if an error occurs.
229      */
230     public void parseUserInfo(String userInfo) throws UserInfoException {
231         // Parse the XML string and obtain a DOM document.
232         Document doc = parseXmlFile(userInfo);
233 
234         // Top element must be ASSERTION_TAG.
235         Element top = doc.getDocumentElement();
236         if (!top.getTagName().equals(ASSERTION_TAG)) {
237             return;
238         }
239 
240         // Retrieve the Assertion attributes.
241         NamedNodeMap assertionAttrs = top.getAttributes();
242         for (int i = 0; i < assertionAttrs.getLength(); i++) {
243             Attr attr = (Attr) assertionAttrs.item(i);
244             ArrayList assertionList = new ArrayList(INITIAL_CAPACITY);
245             assertionList.add(attr.getNodeValue());
246             this.put("config-" + attr.getNodeName().toLowerCase(), assertionList);
247         }
248 
249         // Assertion must contain one and only one ATTR_STMT_TAG element.
250         Node attributeStatement = top.getFirstChild();
251         if (attributeStatement == null || !attributeStatement.getNodeName().equals(ATTR_STMT_TAG) || attributeStatement.getNextSibling() != null) {
252             return;
253         }
254 
255         // Loop over ATTR_STMT_TAG elements.
256         for (Node attribute = attributeStatement.getFirstChild(); attribute != null; attribute = attribute.getNextSibling()) {
257             String nodeName = attribute.getNodeName();
258             // Skip non-Attribute elements. There shouldn't be any...
259             if (nodeName == null || !nodeName.equals(ATTRIBUTE_TAG)) {
260                 continue;
261             }
262             // Retrieve the attribute name. Skip if not an attributeMap key.
263             String attrName = ((Element) attribute).getAttribute(ATTRIBUTE_NAME_TAG);
264             if (!attributeMap.containsKey(attrName.toLowerCase())) {
265                 log.debug("Skipping incoming attribute \"" + attrName + "\"");
266                 continue;
267             }
268             // For every "Attribute" element get the attribute name and its
269             // value.
270             ArrayList attrList = new ArrayList(INITIAL_CAPACITY);
271             for (Node value = attribute.getFirstChild(); value != null; value = value.getNextSibling()) {
272                 String valueNodeName = value.getNodeName();
273                 // Skip anything outside a "Value" tag. There shouldn't be
274                 // anything...
275                 if (valueNodeName == null || !valueNodeName.equals(ATTRIBUTE_VALUE_TAG)) {
276                     continue;
277                 }
278                 StringBuffer attrValue = new StringBuffer();
279                 for (Node valueNode = value.getFirstChild(); valueNode != null; valueNode = valueNode.getNextSibling()) {
280                     if (valueNode.getNodeName() == null || !valueNode.getNodeName().equals(ATTR_STMT_TAG)) {
281                         attrValue.append(valueNode.getNodeValue());
282                     } else {
283                         // Parse an embedded <AttributeStatement> as a value
284                         // element.
285                         // This involves reconstructing a String
286                         // representation of the XML.
287                         attrValue.append(encodeAttributeStatement(valueNode));
288                     }
289                 }
290                 if (attrValue.length() > 0) {
291                     hasAttributes = true;
292                     attrList.add(attrValue.toString());
293                 }
294             }
295             if (!attrList.isEmpty()) {
296                 log.debug("Saving attribute \"" + attributeMap.get(attrName.toLowerCase()) + "\" with value " + attrList);
297                 this.put(attributeMap.get(attrName.toLowerCase()), attrList);
298             }
299         }
300     }
301 
302     /**
303      * Parses an XML file and returns a DOM document.
304      * 
305      * @param userInfo
306      *            The user information in XML Format.
307      * 
308      * @return Document A DOM Document.
309      * 
310      * @throws UserInfoException
311      *             if an error occurs.
312      */
313     private static Document parseXmlFile(String userInfo) throws UserInfoException {
314         try {
315             userInfo = DTD_TEXT + userInfo;
316 
317             // Create a builder factory and set it to validate.
318             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
319             factory.setValidating(true);
320             log.debug("User Info = \"" + userInfo + "\"");
321 
322             // Create the builder and parse the file
323             ByteArrayInputStream inputStream = new ByteArrayInputStream(userInfo.getBytes());
324             return factory.newDocumentBuilder().parse(inputStream);
325         } catch (SAXException e) {
326             log.debug("ShibShimFilter::parseXmlFile:Received a SAXException::" + e.getMessage());
327             throw new UserInfoException("ShibShimFilter::parseXmlFile:Received a SAXException::" + e.getMessage());
328         } catch (ParserConfigurationException e) {
329             log.debug("ShibShimFilter::parseXmlFile:Received a ParserConfigurationException::" + e.getMessage());
330             throw new UserInfoException("ShibShimFilter::parseXmlFile:Received a ParserConfigurationException::" + e.getMessage());
331         } catch (IOException e) {
332             log.debug("ShibShimFilter::parseXmlFile:Received a IOException::" + e.getMessage());
333             throw new UserInfoException("ShibShimFilter::parseXmlFile:Received an IOException::" + e.getMessage());
334         }
335     }
336 
337     /**
338      * Encode the attributes in a sub-object into an XML document.
339      * 
340      * @param attributeStatement
341      *            The attributeStatement node parsed from the XML.
342      * @return the encoded attribute statement.
343      */
344     private StringBuffer encodeAttributeStatement(Node attributeStatement) {
345         StringBuffer result = new StringBuffer();
346         result.append("<" + ATTR_STMT_TAG + ">");
347 
348         // Loop over ATTR_STMT_TAG elements.
349         for (Node attribute = attributeStatement.getFirstChild(); attribute != null; attribute = attribute.getNextSibling()) {
350             String nodeName = attribute.getNodeName();
351             // Skip non-Attribute elements. There shouldn't be any...
352             if (nodeName == null || !nodeName.equals(ATTRIBUTE_TAG)) {
353                 continue;
354             }
355             // Retrieve the attribute name.
356             String attrName = ((Element) attribute).getAttribute(ATTRIBUTE_NAME_TAG);
357             // For every "Attribute" element get the attribute name and its
358             // value.
359             result.append("<" + ATTRIBUTE_TAG + " " + ATTRIBUTE_NAME_TAG + "=\"" + attrName + "\">");
360             for (Node value = attribute.getFirstChild(); value != null; value = value.getNextSibling()) {
361                 String valueNodeName = value.getNodeName();
362                 // Skip anything outside a "Value" tag. There shouldn't be
363                 // anything...
364                 if (valueNodeName == null || !valueNodeName.equals(ATTRIBUTE_VALUE_TAG)) {
365                     continue;
366                 }
367                 StringBuffer attrValue = new StringBuffer();
368                 for (Node valueNode = value.getFirstChild(); valueNode != null; valueNode = valueNode.getNextSibling()) {
369                     if (valueNode.getNodeName() == null || !valueNode.getNodeName().equals(ATTR_STMT_TAG)) {
370                         attrValue.append(valueNode.getNodeValue());
371                     } else {
372                         // Parse an embedded <AttributeStatement> as a value
373                         // element.
374                         // This involves reconstructing a String
375                         // representation of the XML.
376                         attrValue.append(encodeAttributeStatement(valueNode));
377                     }
378                 }
379                 if (attrValue.length() > 0) {
380                     result.append("<" + ATTRIBUTE_VALUE_TAG + ">");
381                     result.append(attrValue.toString());
382                     result.append("</" + ATTRIBUTE_VALUE_TAG + ">");
383                 }
384             }
385             result.append("</" + ATTRIBUTE_TAG + ">");
386         }
387 
388         result.append("</" + ATTR_STMT_TAG + ">");
389         return result;
390     }
391 }