Allow search on attribute name without namespace
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / aap / provider / XMLAAPProvider.java
1 package edu.internet2.middleware.shibboleth.aap.provider;
2
3 import java.util.ArrayList;
4 import java.util.Collection;
5 import java.util.HashMap;
6 import java.util.Iterator;
7 import java.util.Map;
8 import java.util.SortedMap;
9 import java.util.TreeMap;
10 import java.util.regex.PatternSyntaxException;
11
12 import org.apache.log4j.Logger;
13 import org.opensaml.MalformedException;
14 import org.opensaml.SAMLAttribute;
15 import org.opensaml.SAMLException;
16 import org.opensaml.XML;
17 import org.w3c.dom.Element;
18 import org.w3c.dom.Node;
19 import org.w3c.dom.NodeList;
20
21 import edu.internet2.middleware.shibboleth.aap.AAP;
22 import edu.internet2.middleware.shibboleth.aap.AttributeRule;
23 import edu.internet2.middleware.shibboleth.common.Constants;
24 import edu.internet2.middleware.shibboleth.metadata.EntitiesDescriptor;
25 import edu.internet2.middleware.shibboleth.metadata.RoleDescriptor;
26 import edu.internet2.middleware.shibboleth.metadata.ScopedRoleDescriptor;
27 import edu.internet2.middleware.shibboleth.metadata.ScopedRoleDescriptor.Scope;
28
29 public class XMLAAPProvider implements AAP {
30
31     private static Logger log = Logger.getLogger(XMLAAPProvider.class.getName());
32     private SortedMap /* <String,AttributeRule> */ attrmap = new TreeMap();
33     private SortedMap /* <String,AttributeRule> */ aliasmap = new TreeMap();
34     private boolean anyAttribute = false;
35     
36     public XMLAAPProvider(Element e) throws MalformedException {
37         if (!XML.isElementNamed(e,edu.internet2.middleware.shibboleth.common.XML.SHIB_NS,"AttributeAcceptancePolicy")) {
38             log.error("Construction requires a valid AAP file: (shib:AttributeAcceptancePolicy as root element)");
39             throw new MalformedException("Construction requires a valid AAP file: (shib:AttributeAcceptancePolicy as root element)");
40         }
41
42         // Check for AnyAttribute element.
43         Element anyAttr = XML.getFirstChildElement(e,edu.internet2.middleware.shibboleth.common.XML.SHIB_NS,"AnyAttribute");
44         if (anyAttr != null) {
45             anyAttribute = true;
46             log.warn("<AnyAttribute> found, will short-circuit all attribute value and scope filtering");
47         }
48
49         // Loop over the AttributeRule elements.
50         NodeList nlist = e.getElementsByTagNameNS(edu.internet2.middleware.shibboleth.common.XML.SHIB_NS,"AttributeRule");
51         for (int i=0; i<nlist.getLength(); i++) {
52             AttributeRule rule=new XMLAttributeRule((Element)(nlist.item(i)));
53             String key = rule.getName() + "!!" + ((rule.getNamespace() != null) ? rule.getNamespace() : Constants.SHIB_ATTRIBUTE_NAMESPACE_URI); 
54             attrmap.put(key,rule);
55             if (rule.getAlias() != null)
56                 aliasmap.put(rule.getAlias(),rule);
57         }
58     }
59     
60     class XMLAttributeRule implements AttributeRule {
61
62         private String name = null;
63         private String namespace = null;
64         private String alias = null;
65         private String header = null;
66         private boolean caseSensitive = true;
67         private boolean scoped = false;
68         private SiteRule anySiteRule = new SiteRule();
69         private Map /* <String,SiteRule> */ siteMap = new HashMap(); 
70         
71         class Rule {
72             static final int LITERAL = 0;
73             static final int REGEXP = 1;
74             static final int XPATH = 2;
75             
76             Rule(int type, String expression) {
77                 this.type = type;
78                 this.expression = expression;
79             }
80             int type;
81             String expression;
82         }
83         
84         class SiteRule {
85             boolean anyValue = false;
86             ArrayList valueDenials = new ArrayList();
87             ArrayList valueAccepts = new ArrayList();
88             ArrayList scopeDenials = new ArrayList();
89             ArrayList scopeAccepts = new ArrayList();
90         }
91         
92         XMLAttributeRule(Element e) throws MalformedException {
93             alias = XML.assign(e.getAttributeNS(null,"Alias"));
94             header = XML.assign(e.getAttributeNS(null,"Header"));
95             name = XML.assign(e.getAttributeNS(null,"Name"));
96             namespace = XML.assign(e.getAttributeNS(null,"Namespace"));
97             if (namespace == null)
98                 namespace = Constants.SHIB_ATTRIBUTE_NAMESPACE_URI;
99             
100             String flag=XML.assign(e.getAttributeNS(null,"Scoped"));
101             scoped=(XML.safeCompare(flag,"1") || XML.safeCompare(flag,"true"));
102             flag=XML.assign(e.getAttributeNS(null,"CaseSensitive"));
103             caseSensitive=(XML.isEmpty(flag) || XML.safeCompare(flag,"1") || XML.safeCompare(flag,"true"));
104
105             // Check for an AnySite rule.
106             Element anysite = XML.getFirstChildElement(e);
107             if (anysite != null && XML.isElementNamed(anysite,edu.internet2.middleware.shibboleth.common.XML.SHIB_NS,"AnySite")) {
108                 // Process Scope elements.
109                 NodeList vlist = anysite.getElementsByTagNameNS(edu.internet2.middleware.shibboleth.common.XML.SHIB_NS,"Scope");
110                 for (int i=0; i < vlist.getLength(); i++) {
111                     scoped=true;
112                     Element se=(Element)vlist.item(i);
113                     Node valnode=se.getFirstChild();
114                     if (valnode != null && valnode.getNodeType()==Node.TEXT_NODE) {
115                         String accept=se.getAttributeNS(null,"Accept");
116                         if (XML.isEmpty(accept) || XML.safeCompare(flag,"1") || XML.safeCompare(flag,"true"))
117                             anySiteRule.scopeAccepts.add(new Rule(toValueType(se),valnode.getNodeValue()));
118                         else
119                             anySiteRule.scopeDenials.add(new Rule(toValueType(se),valnode.getNodeValue()));
120                     }
121                 }
122
123                 // Check for an AnyValue rule.
124                 vlist = anysite.getElementsByTagNameNS(edu.internet2.middleware.shibboleth.common.XML.SHIB_NS,"AnyValue");
125                 if (vlist.getLength() > 0) {
126                     anySiteRule.anyValue=true;
127                 }
128                 else {
129                     // Process each Value element.
130                     vlist = anysite.getElementsByTagNameNS(edu.internet2.middleware.shibboleth.common.XML.SHIB_NS,"Value");
131                     for (int j=0; j<vlist.getLength(); j++) {
132                         Element ve=(Element)vlist.item(j);
133                         Node valnode=ve.getFirstChild();
134                         if (valnode != null && valnode.getNodeType()==Node.TEXT_NODE) {
135                             String accept=ve.getAttributeNS(null,"Accept");
136                             if (XML.isEmpty(accept) || XML.safeCompare(flag,"1") || XML.safeCompare(flag,"true"))
137                                 anySiteRule.valueAccepts.add(new Rule(toValueType(ve),valnode.getNodeValue()));
138                             else
139                                 anySiteRule.valueDenials.add(new Rule(toValueType(ve),valnode.getNodeValue()));
140                         }
141                     }
142                 }
143             }
144
145             // Loop over the SiteRule elements.
146             NodeList slist = e.getElementsByTagNameNS(edu.internet2.middleware.shibboleth.common.XML.SHIB_NS,"SiteRule");
147             for (int k=0; k<slist.getLength(); k++) {
148                 String srulename=((Element)slist.item(k)).getAttributeNS(null,"Name");
149                 SiteRule srule = new SiteRule();
150                 siteMap.put(srulename,srule);
151
152                 // Process Scope elements.
153                 NodeList vlist = ((Element)slist.item(k)).getElementsByTagNameNS(edu.internet2.middleware.shibboleth.common.XML.SHIB_NS,"Scope");
154                 for (int i=0; i<vlist.getLength(); i++) {
155                     scoped=true;
156                     Element se=(Element)vlist.item(i);
157                     Node valnode=se.getFirstChild();
158                     if (valnode != null && valnode.getNodeType()==Node.TEXT_NODE)
159                     {
160                         String accept=se.getAttributeNS(null,"Accept");
161                         if (XML.isEmpty(accept) || XML.safeCompare(flag,"1") || XML.safeCompare(flag,"true"))
162                             srule.scopeAccepts.add(new Rule(toValueType(se),valnode.getNodeValue()));
163                         else
164                             srule.scopeDenials.add(new Rule(toValueType(se),valnode.getNodeValue()));
165                     }
166                 }
167
168                 // Check for an AnyValue rule.
169                 vlist = ((Element)slist.item(k)).getElementsByTagNameNS(edu.internet2.middleware.shibboleth.common.XML.SHIB_NS,"AnyValue");
170                 if (vlist.getLength() > 0) {
171                     srule.anyValue=true;
172                 }
173                 else {
174                     // Process each Value element.
175                     vlist = ((Element)slist.item(k)).getElementsByTagNameNS(edu.internet2.middleware.shibboleth.common.XML.SHIB_NS,"Value");
176                     for (int j=0; j<vlist.getLength(); j++) {
177                         Element ve=(Element)vlist.item(j);
178                         Node valnode=ve.getFirstChild();
179                         if (valnode != null && valnode.getNodeType()==Node.TEXT_NODE) {
180                             String accept=ve.getAttributeNS(null,"Accept");
181                             if (XML.isEmpty(accept) || XML.safeCompare(flag,"1") || XML.safeCompare(flag,"true"))
182                                 srule.valueAccepts.add(new Rule(toValueType(ve),valnode.getNodeValue()));
183                             else
184                                 srule.valueDenials.add(new Rule(toValueType(ve),valnode.getNodeValue()));
185                         }
186                     }
187                 }
188             }
189         }
190         
191         private int toValueType(Element e) throws MalformedException {
192             if (!e.hasAttributeNS(null,"Type") || XML.safeCompare("literal",e.getAttributeNS(null,"Type")))
193                 return Rule.LITERAL;
194             else if (XML.safeCompare("regexp",e.getAttributeNS(null,"Type")))
195                 return Rule.REGEXP;
196             else if (XML.safeCompare("xpath",e.getAttributeNS(null,"Type")))
197                 return Rule.XPATH;
198             throw new MalformedException("Found an invalid value or scope rule type.");
199         }
200         
201         public String getName() {
202             return name;
203         }
204
205         public String getNamespace() {
206             return namespace;
207         }
208
209         public String getAlias() {
210             return alias;
211         }
212
213         public String getHeader() {
214             return header;
215         }
216
217         public boolean getCaseSensitive() {
218             return caseSensitive;
219         }
220
221         public boolean getScoped() {
222             return scoped;
223         }
224
225         public void apply(SAMLAttribute attribute, RoleDescriptor role) throws SAMLException {
226             ScopedRoleDescriptor scoper = ((role instanceof ScopedRoleDescriptor) ? (ScopedRoleDescriptor)role : null);
227             
228             // This is a little tricky because if we remove anything,
229             // the NodeList is out of sync with the underlying object.
230             // We have to maintain a separate index counter into the object.
231             int index = 0;
232             NodeList vals = attribute.getValueElements();
233             for (int i=0; i < vals.getLength(); i++) {
234                 if (!accept((Element)vals.item(i),scoper))
235                     attribute.removeValue(index);
236                 else
237                     index++;
238             }
239         }
240
241         boolean match(String exp, String test) {
242             try {
243                 if (test.matches(exp))
244                     return true;
245             }
246             catch (PatternSyntaxException ex) {
247                 log.error("caught exception while parsing regular expression ()");
248             }
249             return false;
250         }
251
252         public boolean scopeCheck(Element e, ScopedRoleDescriptor role, Collection ruleStack) {
253             // Are we scoped?
254             String scope=XML.assign(e.getAttributeNS(null,"Scope"));
255             if (scope == null) {
256                 // Are we allowed to be unscoped?
257                 if (scoped)
258                     log.warn("attribute (" + name + ") is scoped, no scope supplied, rejecting it");
259                 return !scoped;
260             }
261
262             // With the new algorithm, we evaluate each matching rule in sequence, separately.
263             Iterator srules = ruleStack.iterator();
264             while (srules.hasNext()) {
265                 SiteRule srule = (SiteRule)srules.next();
266                 
267                 // Now run any denials.
268                 Iterator denials = srule.scopeDenials.iterator();
269                 while (denials.hasNext()) {
270                     Rule denial = (Rule)denials.next();
271                     if ((denial.type==Rule.LITERAL && XML.safeCompare(denial.expression,scope)) ||
272                         (denial.type==Rule.REGEXP && match(denial.expression,scope))) {
273                         log.warn("attribute (" + name + ") scope {" + scope + "} denied by site rule, rejecting it");
274                         return false;
275                     }
276                     else if (denial.type==Rule.XPATH)
277                         log.warn("scope checking does not permit XPath rules");
278                 }
279
280                 // Now run any accepts.
281                 Iterator accepts = srule.scopeAccepts.iterator();
282                 while (accepts.hasNext()) {
283                     Rule accept = (Rule)accepts.next();
284                     if ((accept.type==Rule.LITERAL && XML.safeCompare(accept.expression,scope)) ||
285                         (accept.type==Rule.REGEXP && match(accept.expression,scope))) {
286                         log.debug("matching site rule, scope match");
287                         return true;
288                     }
289                     else if (accept.type==Rule.XPATH)
290                         log.warn("scope checking does not permit XPath rules");
291                 }
292             }
293
294             // If we still can't decide, defer to metadata.
295             if (role != null) {
296                 Iterator scopes=role.getScopes();
297                 while (scopes.hasNext()) {
298                     ScopedRoleDescriptor.Scope p = (Scope)scopes.next();
299                     if ((p.regexp && match(p.scope,scope)) || XML.safeCompare(p.scope,scope)) {
300                         log.debug("scope match via site metadata");
301                         return true;
302                     }
303                 }
304             }
305             
306             log.warn("attribute (" + name + ") scope {" + scope + "} not accepted");
307             return false;
308         }
309         
310         boolean accept(Element e, ScopedRoleDescriptor role) {
311             log.debug("evaluating value for attribute (" + name + ") from site (" +
312                     ((role!=null) ? role.getEntityDescriptor().getId() : "<unspecified>") +
313                     ")");
314             
315             // This is a complete revamp. The "any" cases become a degenerate case, the "least-specific" matching rule.
316             // The first step is to build a list of matching rules, most-specific to least-specific.
317             
318             ArrayList ruleStack = new ArrayList();
319             if (role != null) {
320                 // Primary match is against entityID.
321                 SiteRule srule=(SiteRule)siteMap.get(role.getEntityDescriptor().getId());
322                 if (srule!=null)
323                     ruleStack.add(srule);
324                 
325                 // Secondary matches are on groups.
326                 EntitiesDescriptor group=role.getEntityDescriptor().getEntitiesDescriptor();
327                 while (group != null) {
328                     srule=(SiteRule)siteMap.get(group.getName());
329                     if (srule!=null)
330                         ruleStack.add(srule);
331                     group = group.getEntitiesDescriptor();
332                 }
333             }
334             // Tertiary match is the AnySite rule.
335             ruleStack.add(anySiteRule);
336
337             // Still don't support complex content models...
338             Node n=e.getFirstChild();
339             boolean bSimple=(n != null && n.getNodeType()==Node.TEXT_NODE);
340
341             // With the new algorithm, we evaluate each matching rule in sequence, separately.
342             Iterator srules = ruleStack.iterator();
343             while (srules.hasNext()) {
344                 SiteRule srule = (SiteRule)srules.next();
345                 
346                 // Check for shortcut AnyValue blanket rule.
347                 if (srule.anyValue) {
348                     log.debug("matching site rule, any value match");
349                     return scopeCheck(e,role,ruleStack);
350                 }
351
352                 // Now run any denials.
353                 Iterator denials = srule.valueDenials.iterator();
354                 while (bSimple && denials.hasNext()) {
355                     Rule denial = (Rule)denials.next();
356                     switch (denial.type) {
357                         case Rule.LITERAL:
358                             if ((caseSensitive && !XML.safeCompare(denial.expression,n.getNodeValue())) ||
359                                 (!caseSensitive && denial.expression.equalsIgnoreCase(n.getNodeValue()))) {
360                                 log.warn("attribute (" + name + ") value explicitly denied by site rule, rejecting it");
361                                 return false;
362                             }
363                             break;
364                         
365                         case Rule.REGEXP:
366                             if (match(denial.expression,n.getNodeValue())) {
367                                 log.warn("attribute (" + name + ") value explicitly denied by site rule, rejecting it");
368                                 return false;
369                             }
370                             break;
371                         
372                         case Rule.XPATH:
373                             log.warn("implementation does not support XPath value rules");
374                             break;
375                     }
376                 }
377
378                 // Now run any accepts.
379                 Iterator accepts = srule.valueAccepts.iterator();
380                 while (bSimple && accepts.hasNext()) {
381                     Rule accept = (Rule)accepts.next();
382                     switch (accept.type) {
383                         case Rule.LITERAL:
384                             if ((caseSensitive && !XML.safeCompare(accept.expression,n.getNodeValue())) ||
385                                 (!caseSensitive && accept.expression.equalsIgnoreCase(n.getNodeValue()))) {
386                                 log.debug("site rule, value match");
387                                 return scopeCheck(e,role,ruleStack);
388                             }
389                             break;
390                         
391                         case Rule.REGEXP:
392                             if (match(accept.expression,n.getNodeValue())) {
393                                 log.debug("site rule, value match");
394                                 return scopeCheck(e,role,ruleStack);
395                             }
396                             break;
397                         
398                         case Rule.XPATH:
399                             log.warn("implementation does not support XPath value rules");
400                             break;
401                     }
402                 }
403             }
404
405             log.warn((bSimple ? "" : "complex ") + "attribute (" + name + ") value {" +
406                     n.getNodeValue() + ") could not be validated by policy, rejecting it"
407                     );
408             return false;
409         }
410     }
411     
412     public boolean anyAttribute() {
413         return anyAttribute;
414     }
415
416     public AttributeRule lookup(String name, String namespace) {
417         if (namespace==null) {
418                 String keyGreaterEqual = (String) attrmap.tailMap(name).firstKey();
419                 if (keyGreaterEqual.startsWith(name+"!!")) {
420                         return (AttributeRule) attrmap.get(keyGreaterEqual);
421                 } else {
422                         return null;
423                 }
424         }
425         return (AttributeRule)attrmap.get(name + "!!" + namespace);
426     }
427
428     public AttributeRule lookup(String alias) {
429         return (AttributeRule)aliasmap.get(alias);
430     }
431
432     public Iterator getAttributeRules() {
433         return attrmap.values().iterator();
434     }
435 }