Push SAML Attribute namespace configuration into the resolver. (Needed for proper...
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / aa / attrresolv / provider / CompositeAttributeDefinition.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.text.MessageFormat;
34 import java.util.ArrayList;
35 import java.util.HashSet;
36 import java.util.Iterator;
37 import java.util.Set;
38 import java.util.StringTokenizer;
39
40 import javax.naming.directory.Attribute;
41 import javax.naming.directory.Attributes;
42 import javax.naming.directory.BasicAttribute;
43 import javax.naming.directory.BasicAttributes;
44
45 import org.apache.log4j.Logger;
46 import org.w3c.dom.Element;
47
48 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeDefinitionPlugIn;
49 import edu.internet2.middleware.shibboleth.aa.attrresolv.Dependencies;
50 import edu.internet2.middleware.shibboleth.aa.attrresolv.ResolutionPlugInException;
51 import edu.internet2.middleware.shibboleth.aa.attrresolv.ResolverAttribute;
52
53 /**
54  * The CompositeAttributeDefinition allows composing a single attribute from multiple attributes. It is particularly
55  * useful when values from several columns of a DataBase must be 'concatenated' to form a single composite attribute. To
56  * ensure that the results are same for a given set of source values, multi-valued source attributes must be ordered and
57  * each must have the same number of values. This is true for the attribute values read using the JDBCDataConnector, but
58  * not true with attributes read using the JNDIDirectoryDataConnector or the LDAPDirectoryDataConnector. Hence,
59  * CompositeAttributeDefinition is only currently meaningful for attributes read from an RDB using the
60  * JDBCDataConnector. The specification of this attribute definition is simple. You specify which source attributes to
61  * compose and the format for composing them using the notation of java.text.MessageFormat. The format defaults to a
62  * space separated concatenation of all source attributes. One use case is in the construction of a labeledURI attribute
63  * from two attributes, the 'URL' and the 'URL_Title' that may appear as two columns in a DataBase. As per the
64  * definition of labeledURI, it is essentially a 'space' separated concatenation of URL followed by the title. Since URL
65  * itself should not contain any spaces (assuming it is properly encoded, converting spaces to +), where the URL ends
66  * and the title begins is unambiguous. The specification fpr such a composite attribute is simply: format="{0} {1}"
67  * orderedSourceNames="URL, URL_Title" The format definition in the above example is optional, since that is infact the
68  * default if not specified. Another example of usage of this attribute is as follows: format="{0} ({1})"
69  * orderedSourceNames="Group_Name, Group_Title" Notice that in this example, we are composing the name of a Group and
70  * the descriptive title of the group in parenthesis using the format to create a single attribute from two attributes.
71  * 
72  * @author <a href="mailto:vgoenka@sungardsct.com">Vishal Goenka </a>
73  */
74
75 /**
76  * @author Walter Hoehn
77  */
78 public class CompositeAttributeDefinition extends SimpleBaseAttributeDefinition implements AttributeDefinitionPlugIn {
79
80         private static Logger log = Logger.getLogger(CompositeAttributeDefinition.class.getName());
81
82         // The formatter used to compose from all Source Attributes
83         private MessageFormat sourceFormat;
84
85         // Names of source attributes to compose the target from, in ordered list
86         private String[] sourceNames;
87
88         // Names of source attributes in a Set for convenience of checking membership
89         private Set sourceNamesSet;
90
91         // Number of values that each source attribute has (must be same for all attributes)
92         private int valueCount = -1;
93
94         public CompositeAttributeDefinition(Element e) throws ResolutionPlugInException {
95
96                 super(e);
97
98                 try {
99                         // Since there are more than one source objects in an ordered list, one sourceName doesn't make sense. In
100                         // this
101                         // respect, it differs from other attribute definition
102                         if (e.hasAttribute("sourceName"))
103                                 throw new ResolutionPlugInException(
104                                                 "sourceName is not an allowed attribute for CompositeAttributeDefinition (" + getId() + ")");
105
106                         String orderedSourceNames = e.getAttribute("orderedSourceNames");
107                         if ((orderedSourceNames == null) || ("".equals(orderedSourceNames)))
108                                 throw new ResolutionPlugInException(
109                                                 "orderedSourceNames is a required attribute for CompositeAttributeDefinition (" + getId() + ")");
110
111                         // We assume space or comma as separators
112                         StringTokenizer st = new StringTokenizer(orderedSourceNames, " ,");
113                         ArrayList sourceNamesList = new ArrayList();
114                         while (st.hasMoreTokens()) {
115                                 String token = st.nextToken().trim();
116                                 if (token.length() > 0) sourceNamesList.add(token);
117                         }
118                         sourceNamesSet = new HashSet();
119                         sourceNamesSet.addAll(sourceNamesList);
120                         sourceNames = (String[]) sourceNamesList.toArray(new String[0]);
121
122                         String format = e.getAttribute("format");
123                         // default format is essentially all ordered attribute values separated by a space
124                         if ((format == null) || ("".equals(format))) {
125                                 StringBuffer defaultFormat = new StringBuffer();
126                                 for (int i = 0; i < sourceNames.length; i++) {
127                                         defaultFormat.append("{").append(i).append("}");
128                                         if (i < sourceNames.length - 1) defaultFormat.append(" ");
129                                 }
130                                 format = defaultFormat.toString();
131                         }
132                         sourceFormat = new MessageFormat(format);
133                 } catch (ResolutionPlugInException ex) {
134                         // To ensure that exceptions thrown in the constructor are logged!
135                         log.error(ex.getMessage());
136                         throw ex;
137                 } catch (RuntimeException ex) {
138                         // To ensure that exceptions thrown in the constructor are logged!
139                         log.error(ex.getMessage());
140                         throw ex;
141                 }
142         }
143
144         /**
145          * Get ordered attribute values for all source attributes from the dependent data connectors. The values of all
146          * multi-valued attribute MUST be ordered and MUST be of same size or else the results can be unpredictable.
147          */
148         private void addAttributesFromConnectors(Dependencies depends, Attributes sourceAttrs)
149                         throws ResolutionPlugInException {
150
151                 Iterator connectorDependIt = connectorDependencyIds.iterator();
152                 while (connectorDependIt.hasNext()) {
153                         Attributes attrs = depends.getConnectorResolution((String) connectorDependIt.next());
154                         if (attrs != null) {
155                                 for (int i = 0; i < sourceNames.length; i++) {
156                                         Attribute attr = attrs.get(sourceNames[i]);
157                                         if (attr != null) {
158                                                 int size = attr.size();
159                                                 if (!attr.isOrdered() && (size > 1)) { throw new ResolutionPlugInException(
160                                                                 "Multi-valued attribute (" + attr.getID()
161                                                                                 + ") MUST be ordered for CompositeAttributeDefinition (" + getId() + ")"); }
162                                                 if (valueCount == -1) {
163                                                         valueCount = size; // initialize valueCount
164                                                 } else if (valueCount != size) { throw new ResolutionPlugInException("Multi-valued attribute ("
165                                                                 + attr.getID() + ") has different number of values (" + size
166                                                                 + ") than other attribute(s) that have (" + valueCount + ") values. "
167                                                                 + "All attributes must have same number of values for CompositeAttributeDefinition ("
168                                                                 + getId() + ")"); }
169                                                 if (sourceAttrs.put(attr) != null) { throw new ResolutionPlugInException("Attribute ("
170                                                                 + attr.getID() + ") occured more than once in the dependency chain for (" + getId()
171                                                                 + ") and I don't know which one to pick"); }
172                                         }
173                                 }
174                         }
175                 }
176         }
177
178         /**
179          * Get ordered attribute values for all source attributes from the dependent attributes. The values of all
180          * multi-valued attribute MUST be ordered and MUST be of same size or else the results can be unpredictable.
181          */
182         private void addAttributesFromAttributeDependencies(Dependencies depends, Attributes sourceAttrs)
183                         throws ResolutionPlugInException {
184
185                 Iterator attrDependIt = attributeDependencyIds.iterator();
186                 while (attrDependIt.hasNext()) {
187                         ResolverAttribute attribute = depends.getAttributeResolution((String) attrDependIt.next());
188                         if (attribute != null) {
189                                 if (sourceNamesSet.contains(attribute.getName())) {
190                                         BasicAttribute attr = new BasicAttribute(attribute.getName(), true);
191                                         int size = 0;
192                                         for (Iterator iterator = attribute.getValues(); iterator.hasNext();) {
193                                                 attr.add(size++, iterator.next());
194                                         }
195                                         if (valueCount == -1) {
196                                                 valueCount = size; // initialize valueCount
197                                         } else if (valueCount != size) { throw new ResolutionPlugInException("Multi-valued attribute ("
198                                                         + attr.getID() + ") has different number of values (" + size
199                                                         + ") than other attribute(s) that have (" + valueCount + ") values. "
200                                                         + "All attributes must have same number of values for CompositeAttributeDefinition ("
201                                                         + getId() + ")"); }
202                                         if (sourceAttrs.put(attr) != null) { throw new ResolutionPlugInException("Attribute ("
203                                                         + attr.getID() + ") occured more than once in the dependency chain for (" + getId()
204                                                         + ") and I don't know which one to pick"); }
205                                 } else {
206                                         log
207                                                         .warn("Attribute Dependency ("
208                                                                         + attribute.getName()
209                                                                         + ") is not listed in the orderedSourceNames attribute for the CustomAttributeDefinition for ("
210                                                                         + getId() + ")");
211                                 }
212                         }
213                 }
214         }
215
216         /**
217          * @see edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeDefinitionPlugIn#resolve(edu.internet2.middleware.shibboleth.aa.attrresolv.ResolverAttribute,
218          *      java.security.Principal, java.lang.String, java.lang.String,
219          *      edu.internet2.middleware.shibboleth.aa.attrresolv.Dependencies)
220          */
221         public void resolve(ResolverAttribute attribute, Principal principal, String requester, String responder,
222                         Dependencies depends) throws ResolutionPlugInException {
223
224                 // Collect attribute values from dependencies
225                 BasicAttributes attributes = new BasicAttributes();
226                 addAttributesFromConnectors(depends, attributes);
227                 addAttributesFromAttributeDependencies(depends, attributes);
228
229                 // If we got this far, all attributes are ordered and have 'valueCount' number of values
230                 for (int i = 0; i < valueCount; i++) {
231                         // put values in an array so we can use the formatter for creating the composite value
232                         Object[] values = new Object[sourceNames.length];
233                         try {
234                                 for (int j = 0; j < sourceNames.length; j++) {
235                                         Attribute attr = attributes.get(sourceNames[j]);
236                                         if (attr == null)
237                                                 throw new ResolutionPlugInException("No value found for attribute (" + sourceNames[j]
238                                                                 + ") during resolution of (" + getId() + ")");
239                                         // get the ordered (i'th) value of the attribute
240                                         values[j] = attr.get(i);
241                                 }
242                                 attribute.addValue(sourceFormat.format(values));
243                         } catch (ResolutionPlugInException e) {
244                                 // Simply rethrow ...
245                                 throw e;
246                         } catch (Exception e) {
247                                 StringBuffer err = new StringBuffer();
248                                 err.append("Error creating composite attribute [");
249                                 if (values != null) {
250                                         for (int ii = 0; ii < values.length; ii++) {
251                                                 if (values[ii] != null) err.append(values[ii].toString()).append(", ");
252                                                 else err.append("null ");
253                                         }
254                                 } else err.append(" null ");
255                                 err.append("] using format ").append(sourceFormat.toPattern()).append(" for (").append(getId()).append(
256                                                 "): ").append(e.getMessage());
257                                 log.error(err.toString());
258                                 throw new ResolutionPlugInException(err.toString());
259                         }
260                 }
261
262                 standardProcessing(attribute);
263
264                 if (valueHandler != null) {
265                         attribute.registerValueHandler(valueHandler);
266                 }
267                 attribute.setResolved();
268         }
269
270 }