21d1dbf438062cbf7dceaef9b75d53360f3cf5e8
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / aa / attrresolv / provider / MappedAttributeDefinition.java
1 /*
2  * The Shibboleth License, Version 1. Copyright (c) 2002 University Corporation for Advanced Internet Development, Inc.
3  * All rights reserved Redistribution and use in source and binary forms, with or without modification, are permitted
4  * provided that the following conditions are met: Redistributions of source code must retain the above copyright
5  * notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above
6  * copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials
7  * provided with the distribution, if any, must include the following acknowledgment: "This product includes software
8  * developed by the University Corporation for Advanced Internet Development <http://www.ucaid.edu>Internet2 Project.
9  * Alternately, this acknowledegement may appear in the software itself, if and wherever such third-party
10  * acknowledgments normally appear. Neither the name of Shibboleth nor the names of its contributors, nor Internet2, nor
11  * the University Corporation for Advanced Internet Development, Inc., nor UCAID may be used to endorse or promote
12  * products derived from this software without specific prior written permission. For written permission, please contact
13  * shibboleth@shibboleth.org Products derived from this software may not be called Shibboleth, Internet2, UCAID, or the
14  * University Corporation for Advanced Internet Development, nor may Shibboleth appear in their name, without prior
15  * written permission of the University Corporation for Advanced Internet Development. THIS SOFTWARE IS PROVIDED BY THE
16  * COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE
18  * DISCLAIMED AND THE ENTIRE RISK OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE. IN NO
19  * EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC.
20  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
23  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 /*
27  * Contributed by SunGard SCT.
28  */
29
30 package edu.internet2.middleware.shibboleth.aa.attrresolv.provider;
31
32 import java.security.Principal;
33 import java.util.Collection;
34 import java.util.HashMap;
35 import java.util.HashSet;
36 import java.util.Iterator;
37 import java.util.Map;
38 import java.util.Set;
39 import java.util.StringTokenizer;
40 import java.util.regex.Pattern;
41
42 import org.apache.log4j.Logger;
43 import org.w3c.dom.Element;
44 import org.w3c.dom.NodeList;
45
46 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeDefinitionPlugIn;
47 import edu.internet2.middleware.shibboleth.aa.attrresolv.Dependencies;
48 import edu.internet2.middleware.shibboleth.aa.attrresolv.ResolutionPlugInException;
49 import edu.internet2.middleware.shibboleth.aa.attrresolv.ResolverAttribute;
50
51 /**
52  * The MappedAttributeDefinition allows an enumeration of mappings between the source attribute values received from a
53  * data connector and the values that are returned. The enumeration is essentially a many-to-many mapping in the general
54  * case, one-to-one being a very specific but plausible case. The mapping is specified using a series of
55  * &lt;ValueMap&gt; elements, each containing a [key set -> value] element. Thus each &lt;ValueMap&gt; is a many-to-one
56  * mapping, but since the elements in the keyset are allowed to re-appear in a subsequent &lt;ValueMap&gt;, it becomes a
57  * many-to-many mapping. For instance, consider a sample mapping of Luminis Role to an eduPersonAffiliation. Luminis
58  * roles are arbitrary whereas eduPersonAffiliation has a constrained set of values, namely affiliate, alum, employee,
59  * faculty, student, staff and member. A potential mapping may look as follows:
60  * 
61  * <pre>
62  * 
63  *  
64  *   
65  *    
66  *     
67  *      
68  *            &lt;ValueMap value=&quot;affiliate&quot;  keyset=&quot;guest, prospect[a-z ]*, friends&quot;                        /&gt;
69  *            &lt;ValueMap value=&quot;alum&quot;       keyset=&quot;alum, alumni&quot;                                           /&gt;
70  *            &lt;ValueMap value=&quot;employee&quot;   keyset=&quot;employee&quot;                                               /&gt;
71  *            &lt;ValueMap value=&quot;faculty&quot;    keyset=&quot;faculty&quot;                                                /&gt;
72  *            &lt;ValueMap value=&quot;member&quot;     keyset=&quot;student, faculty, admin[a-z ]*, [a-z ]*admin, employee&quot; /&gt;
73  *            &lt;ValueMap value=&quot;staff&quot;      keyset=&quot;admin[a-z ]*, [a-z ]*admin&quot;                             /&gt;
74  *            &lt;ValueMap value=&quot;student&quot;    keyset=&quot;student&quot;                                                /&gt;
75  *       
76  *      
77  *     
78  *    
79  *   
80  *  
81  * </pre>
82  * 
83  * This many-to-many mapping will result in a Luminis role of 'student' to imply eduPersonAffiliation values of [member,
84  * student] and a Luminis role of admin to imply eduPersonAffiliation value of [member, staff]. The separator used in
85  * specifying the keyset can be specified in the &lt;ValueMap&gt; itself and defaults to ",". Leading or trailing spaces
86  * in keys are ignored. As illustrated by the above example, the keyset can contain regular expressions against which
87  * the source values of the attributes may be matched to arrive at the value mapping. To allow special characters that
88  * have special significance as regular expressions (such as <code>*</code>) to appear in the keyset, an attribute
89  * 'regex' can be set to false, thus implying that all keys in the keyset should be literally matched. The match can be
90  * specified to be case insensitive by setting 'ignoreCase' attribute to true. Since one attribute value can have
91  * multiple mappings, and the ValueMap elements are unordered, specifing a catch-all mapping, such as: &lt;ValueMap
92  * value="member" keyset="[a-z]*" /&gt; is sure to match every value, irrespective of whether another match was found
93  * for the attribute. To allow such a catch-all specification, an attribute 'defaultValue' can be set to the 'catch-all'
94  * value. If 'defaultValue' is set to a special value of ampersand (&amp;), the original attribute value itself is added
95  * to the attribute. The algorithm for this implementation is the following: We take the [keyset -> value]* mappings
96  * specified in the MappedAttributeDefinition (which is perhaps easier to specify) and reverse it to [key -> value set]*
97  * mappings internally. These mappings are stored as HashMaps, one HashMap for regex keys and another for non-regex
98  * keys. Every attribute value to be resolved is looked up in both these HashMaps and all matching values in the value
99  * set is added in lieu of the attribute value to be resolved. If no mapping is found, we use the defaultValue (if
100  * specified).
101  * 
102  * @author <a href="mailto:vgoenka@sungardsct.com">Vishal Goenka </a>
103  */
104
105 public class MappedAttributeDefinition extends SimpleBaseAttributeDefinition implements AttributeDefinitionPlugIn {
106
107         private static Logger log = Logger.getLogger(MappedAttributeDefinition.class.getName());
108
109         // [simple-key -> mapped value set], where simple-key is not a regular expression
110         private HashMap simpleValueMap;
111
112         // [regex-key -> mapped value set], where regex-key is a regular expression
113         private HashMap regexValueMap;
114
115         // Should we ignore case when matching against simple or regex keys
116         private boolean ignoreCase = false;
117
118         // Default value, if no other value mapping is found
119         private String defaultValue;
120
121         // Does the keyset contain regular expressions or simply value strings?
122         private boolean regexExpected = false;
123
124         // A pattern that describes a word (without any white space or special characters) This is used to separate 'simple'
125         // keys from 'regex' keys when the ValueMap is parsed.
126         private Pattern word;
127
128         public MappedAttributeDefinition(Element e) throws ResolutionPlugInException {
129
130                 super(e);
131
132                 // Does the keyset contain regular expressions or simply value strings?
133                 regexExpected = Boolean.valueOf(e.getAttribute("regex")).booleanValue();
134
135                 // Is the keyset case sensitive
136                 ignoreCase = Boolean.valueOf(e.getAttribute("ignoreCase")).booleanValue();
137
138                 defaultValue = e.getAttribute("defaultValue");
139
140                 // Initialize maps to contain [keyset -> value] mappings
141                 initializeValueMaps();
142
143                 NodeList valueMaps = e.getElementsByTagName("ValueMap");
144                 int count = valueMaps.getLength();
145                 for (int i = 0; i < count; i++) {
146                         Element valueMap = (Element) valueMaps.item(i);
147                         // Parse each ValueMap and initialize the internal data structures
148                         parseValueMap(valueMap);
149                 }
150         }
151
152         /**
153          * @see edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeDefinitionPlugIn#resolve(edu.internet2.middleware.shibboleth.aa.attrresolv.ResolverAttribute,
154          *      java.security.Principal, java.lang.String, java.lang.String,
155          *      edu.internet2.middleware.shibboleth.aa.attrresolv.Dependencies)
156          */
157         public void resolve(ResolverAttribute attribute, Principal principal, String requester, String responder,
158                         Dependencies depends) throws ResolutionPlugInException {
159
160                 // Resolve all dependencies to arrive at the source values (unformatted)
161                 Collection results = resolveDependencies(attribute, principal, requester, depends);
162
163                 Iterator resultsIt = results.iterator();
164
165                 // Create string for debugging output
166                 StringBuffer debugBuffer = new StringBuffer();
167
168                 while (resultsIt.hasNext()) {
169                         // Read the source value (prior to mapping)
170                         String valueExactCase = getString(resultsIt.next());
171                         String value = valueExactCase;
172                         boolean mapped = false;
173
174                         if (log.isDebugEnabled()) debugBuffer.append("[").append(value).append(" --> ");
175
176                         // The source is converted to lowercase for matching ... the mapped value is returned in 'exact case'
177                         if (ignoreCase) value = value.toLowerCase();
178
179                         // First check if there are value mappings based on exact-match rather than regex matches
180                         Set simpleMappedValues = (Set) simpleValueMap.get(value);
181                         if (simpleMappedValues != null) {
182                                 // Add all mapped values to the attribute
183                                 for (Iterator it = simpleMappedValues.iterator(); it.hasNext();) {
184                                         String simpleMappedValue = (String) it.next();
185                                         attribute.addValue(simpleMappedValue);
186                                         mapped = true;
187                                         if (log.isDebugEnabled()) debugBuffer.append(simpleMappedValue).append(", ");
188                                 }
189                         }
190
191                         // If we are expecting the keyset to contain regular expressions, the matching process is exhaustive!
192                         try {
193                                 if (regexExpected) {
194                                         // Check all entries in the hashmap for a regex match between the source value and the regex-key
195                                         for (Iterator it = regexValueMap.entrySet().iterator(); it.hasNext();) {
196                                                 Map.Entry entry = (Map.Entry) it.next();
197                                                 Pattern regexKey = (Pattern) entry.getKey();
198                                                 if (regexKey.matcher(value).matches()) {
199                                                         // Add all values
200                                                         Set regexMappedValues = (Set) entry.getValue();
201                                                         for (Iterator vit = regexMappedValues.iterator(); vit.hasNext();) {
202                                                                 String regexMappedValue = (String) vit.next();
203                                                                 attribute.addValue(regexMappedValue);
204                                                                 mapped = true;
205                                                                 if (log.isDebugEnabled()) debugBuffer.append(regexMappedValue).append(", ");
206                                                         }
207                                                 }
208                                         }
209                                 }
210                         } catch (Exception e) {
211                                 // Any exception during the regex match only skips the attribute value being matched ...
212                                 log.error("Attribute value for (" + getId() + ") --> (" + value
213                                                 + ") failed during regex processing with exception: " + e.getMessage());
214                         }
215
216                         // Was there was no mapping found for this value?
217                         if (!mapped && (defaultValue != null) && (defaultValue.length() > 0)) {
218                                 if (defaultValue.equals("&")) {
219                                         attribute.addValue(valueExactCase);
220                                         if (log.isDebugEnabled()) debugBuffer.append(valueExactCase);
221                                 } else {
222                                         attribute.addValue(defaultValue);
223                                         if (log.isDebugEnabled()) debugBuffer.append(defaultValue);
224                                 }
225                         }
226
227                         if (log.isDebugEnabled()) debugBuffer.append("] ");
228                 }
229                 attribute.setResolved();
230                 if (log.isDebugEnabled())
231                         log.debug("Attribute values upon mapping for (" + getId() + "): " + debugBuffer.toString());
232         }
233
234         /**
235          * Helper method ... allocates hashmaps etc.
236          */
237         private void initializeValueMaps() {
238
239                 simpleValueMap = new HashMap();
240                 regexValueMap = new HashMap();
241                 word = Pattern.compile("^\\w+$");
242         }
243
244         /**
245          * This method reads the [value --> keyset] and reverses the mapping to [key -> value set]* for easier attribute
246          * resolution. Each key in the keyset is evaluated for whether it is a regex or not, and is stored in the
247          * regexValueMap or simpleValueMap based on whether the key contains any non-word characters.
248          */
249         private void parseValueMap(Element element) throws ResolutionPlugInException {
250
251                 String value = element.getAttribute("value");
252                 String keyset = element.getAttribute("keyset");
253                 String separator = element.getAttribute("separator");
254
255                 if ((value == null) || ("".equals(value)) || (keyset == null) || ("".equals(keyset))) {
256                         String error = "value and keyset attributes MUST both be non-empty in the ValueMap element for attribute ("
257                                         + getId() + ")";
258                         log.error(error);
259                         throw new ResolutionPlugInException(error);
260                 }
261
262                 // The separator for entries in the keyset
263                 if ((separator == null) || ("".equals(separator))) separator = ",";
264
265                 StringTokenizer st = new StringTokenizer(keyset, separator);
266                 while (st.hasMoreTokens()) {
267                         // trim to remove spaces that are used immediately after a separator in the keyset, even when space
268                         // otherwise is
269                         // not a separator
270                         String key = st.nextToken().trim();
271
272                         // If ignoreCase, values will also be converted to lowercase before the match is attempted
273                         if (ignoreCase) key = key.toLowerCase();
274
275                         // Lets assume that the key is a simple String and therefore the valueMap to store the mapping is
276                         // simpleValueMap
277                         Object keyObject = key;
278                         HashMap valueMap = simpleValueMap;
279
280                         // If we are expecting regex and this is one, add it to regex value map, else add it to simpleValueMap
281                         if (regexExpected && !word.matcher(key).matches()) {
282                                 keyObject = Pattern.compile(key);
283                                 valueMap = regexValueMap;
284                         }
285
286                         HashSet valueSet = (HashSet) valueMap.get(keyObject);
287                         if (valueSet == null) {
288                                 valueSet = new HashSet();
289                                 valueMap.put(keyObject, valueSet);
290                         }
291                         valueSet.add(value);
292                 }
293         }
294 }