1f1ce12eaa55a0da5915d58f1cb0bf40ed59a8f9
[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  *       
69  *             &lt;ValueMap value=&quot;affiliate&quot;  keyset=&quot;guest, prospect[a-z ]*, friends&quot;                        /&gt;
70  *             &lt;ValueMap value=&quot;alum&quot;       keyset=&quot;alum, alumni&quot;                                           /&gt;
71  *             &lt;ValueMap value=&quot;employee&quot;   keyset=&quot;employee&quot;                                               /&gt;
72  *             &lt;ValueMap value=&quot;faculty&quot;    keyset=&quot;faculty&quot;                                                /&gt;
73  *             &lt;ValueMap value=&quot;member&quot;     keyset=&quot;student, faculty, admin[a-z ]*, [a-z ]*admin, employee&quot; /&gt;
74  *             &lt;ValueMap value=&quot;staff&quot;      keyset=&quot;admin[a-z ]*, [a-z ]*admin&quot;                             /&gt;
75  *             &lt;ValueMap value=&quot;student&quot;    keyset=&quot;student&quot;                                                /&gt;
76  *        
77  *       
78  *      
79  *     
80  *    
81  *   
82  *  
83  * </pre>
84  * 
85  * This many-to-many mapping will result in a Luminis role of 'student' to imply eduPersonAffiliation values of [member,
86  * student] and a Luminis role of admin to imply eduPersonAffiliation value of [member, staff]. The separator used in
87  * specifying the keyset can be specified in the &lt;ValueMap&gt; itself and defaults to ",". Leading or trailing spaces
88  * in keys are ignored. As illustrated by the above example, the keyset can contain regular expressions against which
89  * the source values of the attributes may be matched to arrive at the value mapping. To allow special characters that
90  * have special significance as regular expressions (such as <code>*</code>) to appear in the keyset, an attribute
91  * 'regex' can be set to false, thus implying that all keys in the keyset should be literally matched. The match can be
92  * specified to be case insensitive by setting 'ignoreCase' attribute to true. Since one attribute value can have
93  * multiple mappings, and the ValueMap elements are unordered, specifing a catch-all mapping, such as: &lt;ValueMap
94  * value="member" keyset="[a-z]*" /&gt; is sure to match every value, irrespective of whether another match was found
95  * for the attribute. To allow such a catch-all specification, an attribute 'defaultValue' can be set to the 'catch-all'
96  * value. If 'defaultValue' is set to a special value of ampersand (&amp;), the original attribute value itself is added
97  * to the attribute. The algorithm for this implementation is the following: We take the [keyset -> value]* mappings
98  * specified in the MappedAttributeDefinition (which is perhaps easier to specify) and reverse it to [key -> value set]*
99  * mappings internally. These mappings are stored as HashMaps, one HashMap for regex keys and another for non-regex
100  * keys. Every attribute value to be resolved is looked up in both these HashMaps and all matching values in the value
101  * set is added in lieu of the attribute value to be resolved. If no mapping is found, we use the defaultValue (if
102  * specified).
103  * 
104  * @author <a href="mailto:vgoenka@sungardsct.com">Vishal Goenka </a>
105  */
106
107 public class MappedAttributeDefinition extends SimpleBaseAttributeDefinition implements AttributeDefinitionPlugIn {
108
109         private static Logger log = Logger.getLogger(MappedAttributeDefinition.class.getName());
110
111         // [simple-key -> mapped value set], where simple-key is not a regular expression
112         private HashMap simpleValueMap;
113
114         // [regex-key -> mapped value set], where regex-key is a regular expression
115         private HashMap regexValueMap;
116
117         // Should we ignore case when matching against simple or regex keys
118         private boolean ignoreCase = false;
119
120         // Default value, if no other value mapping is found
121         private String defaultValue;
122
123         // Does the keyset contain regular expressions or simply value strings?
124         private boolean regexExpected = false;
125
126         // A pattern that describes a word (without any white space or special characters) This is used to separate 'simple'
127         // keys from 'regex' keys when the ValueMap is parsed.
128         private Pattern word;
129
130         public MappedAttributeDefinition(Element e) throws ResolutionPlugInException {
131
132                 super(e);
133
134                 // Does the keyset contain regular expressions or simply value strings?
135                 regexExpected = Boolean.valueOf(e.getAttribute("regex")).booleanValue();
136
137                 // Is the keyset case sensitive
138                 ignoreCase = Boolean.valueOf(e.getAttribute("ignoreCase")).booleanValue();
139
140                 defaultValue = e.getAttribute("defaultValue");
141
142                 // Initialize maps to contain [keyset -> value] mappings
143                 initializeValueMaps();
144
145                 NodeList valueMaps = e.getElementsByTagName("ValueMap");
146                 int count = valueMaps.getLength();
147                 for (int i = 0; i < count; i++) {
148                         Element valueMap = (Element) valueMaps.item(i);
149                         // Parse each ValueMap and initialize the internal data structures
150                         parseValueMap(valueMap);
151                 }
152         }
153
154         /**
155          * @see edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeDefinitionPlugIn#resolve(edu.internet2.middleware.shibboleth.aa.attrresolv.ResolverAttribute,
156          *      java.security.Principal, java.lang.String, java.lang.String,
157          *      edu.internet2.middleware.shibboleth.aa.attrresolv.Dependencies)
158          */
159         public void resolve(ResolverAttribute attribute, Principal principal, String requester, String responder,
160                         Dependencies depends) throws ResolutionPlugInException {
161
162                 standardProcessing(attribute);
163
164                 // Resolve all dependencies to arrive at the source values (unformatted)
165                 Collection results = resolveDependencies(attribute, principal, requester, depends);
166
167                 Iterator resultsIt = results.iterator();
168
169                 // Create string for debugging output
170                 StringBuffer debugBuffer = new StringBuffer();
171
172                 while (resultsIt.hasNext()) {
173                         // Read the source value (prior to mapping)
174                         String valueExactCase = getString(resultsIt.next());
175                         String value = valueExactCase;
176                         boolean mapped = false;
177
178                         if (log.isDebugEnabled()) debugBuffer.append("[").append(value).append(" --> ");
179
180                         // The source is converted to lowercase for matching ... the mapped value is returned in 'exact case'
181                         if (ignoreCase) value = value.toLowerCase();
182
183                         // First check if there are value mappings based on exact-match rather than regex matches
184                         Set simpleMappedValues = (Set) simpleValueMap.get(value);
185                         if (simpleMappedValues != null) {
186                                 // Add all mapped values to the attribute
187                                 for (Iterator it = simpleMappedValues.iterator(); it.hasNext();) {
188                                         String simpleMappedValue = (String) it.next();
189                                         attribute.addValue(simpleMappedValue);
190                                         mapped = true;
191                                         if (log.isDebugEnabled()) debugBuffer.append(simpleMappedValue).append(", ");
192                                 }
193                         }
194
195                         // If we are expecting the keyset to contain regular expressions, the matching process is exhaustive!
196                         try {
197                                 if (regexExpected) {
198                                         // Check all entries in the hashmap for a regex match between the source value and the regex-key
199                                         for (Iterator it = regexValueMap.entrySet().iterator(); it.hasNext();) {
200                                                 Map.Entry entry = (Map.Entry) it.next();
201                                                 Pattern regexKey = (Pattern) entry.getKey();
202                                                 if (regexKey.matcher(value).matches()) {
203                                                         // Add all values
204                                                         Set regexMappedValues = (Set) entry.getValue();
205                                                         for (Iterator vit = regexMappedValues.iterator(); vit.hasNext();) {
206                                                                 String regexMappedValue = (String) vit.next();
207                                                                 attribute.addValue(regexMappedValue);
208                                                                 mapped = true;
209                                                                 if (log.isDebugEnabled()) debugBuffer.append(regexMappedValue).append(", ");
210                                                         }
211                                                 }
212                                         }
213                                 }
214                         } catch (Exception e) {
215                                 // Any exception during the regex match only skips the attribute value being matched ...
216                                 log.error("Attribute value for (" + getId() + ") --> (" + value
217                                                 + ") failed during regex processing with exception: " + e.getMessage());
218                         }
219
220                         // Was there was no mapping found for this value?
221                         if (!mapped && (defaultValue != null) && (defaultValue.length() > 0)) {
222                                 if (defaultValue.equals("&")) {
223                                         attribute.addValue(valueExactCase);
224                                         if (log.isDebugEnabled()) debugBuffer.append(valueExactCase);
225                                 } else {
226                                         attribute.addValue(defaultValue);
227                                         if (log.isDebugEnabled()) debugBuffer.append(defaultValue);
228                                 }
229                         }
230
231                         if (log.isDebugEnabled()) debugBuffer.append("] ");
232                 }
233                 attribute.setResolved();
234                 if (log.isDebugEnabled())
235                         log.debug("Attribute values upon mapping for (" + getId() + "): " + debugBuffer.toString());
236         }
237
238         /**
239          * Helper method ... allocates hashmaps etc.
240          */
241         private void initializeValueMaps() {
242
243                 simpleValueMap = new HashMap();
244                 regexValueMap = new HashMap();
245                 word = Pattern.compile("^\\w+$");
246         }
247
248         /**
249          * This method reads the [value --> keyset] and reverses the mapping to [key -> value set]* for easier attribute
250          * resolution. Each key in the keyset is evaluated for whether it is a regex or not, and is stored in the
251          * regexValueMap or simpleValueMap based on whether the key contains any non-word characters.
252          */
253         private void parseValueMap(Element element) throws ResolutionPlugInException {
254
255                 String value = element.getAttribute("value");
256                 String keyset = element.getAttribute("keyset");
257                 String separator = element.getAttribute("separator");
258
259                 if ((value == null) || ("".equals(value)) || (keyset == null) || ("".equals(keyset))) {
260                         String error = "value and keyset attributes MUST both be non-empty in the ValueMap element for attribute ("
261                                         + getId() + ")";
262                         log.error(error);
263                         throw new ResolutionPlugInException(error);
264                 }
265
266                 // The separator for entries in the keyset
267                 if ((separator == null) || ("".equals(separator))) separator = ",";
268
269                 StringTokenizer st = new StringTokenizer(keyset, separator);
270                 while (st.hasMoreTokens()) {
271                         // trim to remove spaces that are used immediately after a separator in the keyset, even when space
272                         // otherwise is
273                         // not a separator
274                         String key = st.nextToken().trim();
275
276                         // If ignoreCase, values will also be converted to lowercase before the match is attempted
277                         if (ignoreCase) key = key.toLowerCase();
278
279                         // Lets assume that the key is a simple String and therefore the valueMap to store the mapping is
280                         // simpleValueMap
281                         Object keyObject = key;
282                         HashMap valueMap = simpleValueMap;
283
284                         // If we are expecting regex and this is one, add it to regex value map, else add it to simpleValueMap
285                         if (regexExpected && !word.matcher(key).matches()) {
286                                 keyObject = Pattern.compile(key);
287                                 valueMap = regexValueMap;
288                         }
289
290                         HashSet valueSet = (HashSet) valueMap.get(keyObject);
291                         if (valueSet == null) {
292                                 valueSet = new HashSet();
293                                 valueMap.put(keyObject, valueSet);
294                         }
295                         valueSet.add(value);
296                 }
297         }
298 }