d8f2b0dda53a0b49b8b2314b65991d36b37faf88
[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.regex.PatternSyntaxException;
9
10 import org.apache.log4j.Logger;
11 import org.opensaml.MalformedException;
12 import org.opensaml.SAMLAttribute;
13 import org.opensaml.SAMLException;
14 import org.opensaml.XML;
15 import org.w3c.dom.Element;
16 import org.w3c.dom.Node;
17 import org.w3c.dom.NodeList;
18
19 import edu.internet2.middleware.shibboleth.aap.AAP;
20 import edu.internet2.middleware.shibboleth.aap.AttributeRule;
21 import edu.internet2.middleware.shibboleth.common.Constants;
22 import edu.internet2.middleware.shibboleth.metadata.EntitiesDescriptor;
23 import edu.internet2.middleware.shibboleth.metadata.RoleDescriptor;
24 import edu.internet2.middleware.shibboleth.metadata.ScopedRoleDescriptor;
25 import edu.internet2.middleware.shibboleth.metadata.ScopedRoleDescriptor.Scope;
26
27 public class XMLAAPProvider implements AAP {
28
29     private static Logger log = Logger.getLogger(XMLAAPProvider.class.getName());
30     private Map /* <String,AttributeRule> */ attrmap = new HashMap();
31     private Map /* <String,AttributeRule> */ aliasmap = new HashMap();
32     private boolean anyAttribute = false;
33     
34     public XMLAAPProvider(Element e) throws MalformedException {
35         if (!XML.isElementNamed(e,edu.internet2.middleware.shibboleth.common.XML.SHIB_NS,"AttributeAcceptancePolicy")) {
36             log.error("Construction requires a valid AAP file: (shib:AttributeAcceptancePolicy as root element)");
37             throw new MalformedException("Construction requires a valid AAP file: (shib:AttributeAcceptancePolicy as root element)");
38         }
39
40         // Check for AnyAttribute element.
41         Element anyAttr = XML.getFirstChildElement(e,edu.internet2.middleware.shibboleth.common.XML.SHIB_NS,"AnyAttribute");
42         if (anyAttr != null) {
43             anyAttribute = true;
44             log.warn("<AnyAttribute> found, will short-circuit all attribute value and scope filtering");
45         }
46
47         // Loop over the AttributeRule elements.
48         NodeList nlist = e.getElementsByTagNameNS(edu.internet2.middleware.shibboleth.common.XML.SHIB_NS,"AttributeRule");
49         for (int i=0; i<nlist.getLength(); i++) {
50             AttributeRule rule=new XMLAttributeRule((Element)(nlist.item(i)));
51             String key = rule.getName() + "!!" + ((rule.getNamespace() != null) ? rule.getNamespace() : Constants.SHIB_ATTRIBUTE_NAMESPACE_URI); 
52             attrmap.put(key,rule);
53             if (rule.getAlias() != null)
54                 aliasmap.put(rule.getAlias(),rule);
55         }
56     }
57     
58     class XMLAttributeRule implements AttributeRule {
59
60         private String name = null;
61         private String namespace = null;
62         private String factory = null;
63         private String alias = null;
64         private String header = null;
65         private boolean caseSensitive = true;
66         private boolean scoped = false;
67         private SiteRule anySiteRule = new SiteRule();
68         private Map /* <String,SiteRule> */ siteMap = new HashMap(); 
69         
70         class Rule {
71             static final int LITERAL = 0;
72             static final int REGEXP = 1;
73             static final int XPATH = 2;
74             
75             Rule(int type, String expression) {
76                 this.type = type;
77                 this.expression = expression;
78             }
79             int type;
80             String expression;
81         }
82         
83         class SiteRule {
84             boolean anyValue = false;
85             ArrayList valueDenials = new ArrayList();
86             ArrayList valueAccepts = new ArrayList();
87             ArrayList scopeDenials = new ArrayList();
88             ArrayList scopeAccepts = new ArrayList();
89         }
90         
91         XMLAttributeRule(Element e) throws MalformedException {
92             factory = XML.assign(e.getAttributeNS(null,"Factory"));
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 (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 getFactory() {
210             return factory;
211         }
212
213         public String getAlias() {
214             return alias;
215         }
216
217         public String getHeader() {
218             return header;
219         }
220
221         public boolean getCaseSensitive() {
222             return caseSensitive;
223         }
224
225         public boolean getScoped() {
226             return scoped;
227         }
228
229         public void apply(SAMLAttribute attribute, RoleDescriptor role) throws SAMLException {
230             ScopedRoleDescriptor scoper = ((role instanceof ScopedRoleDescriptor) ? (ScopedRoleDescriptor)role : null);
231             
232             // This is a little tricky because if we remove anything,
233             // the NodeList is out of sync with the underlying object.
234             // We have to maintain a separate index counter into the object.
235             int index = 0;
236             NodeList vals = attribute.getValueElements();
237             for (int i=0; i < vals.getLength(); i++) {
238                 if (!accept((Element)vals.item(i),scoper))
239                     attribute.removeValue(index);
240                 else
241                     index++;
242             }
243         }
244
245         boolean match(String exp, String test) {
246             try {
247                 if (test.matches(exp))
248                     return true;
249             }
250             catch (PatternSyntaxException ex) {
251                 log.error("caught exception while parsing regular expression ()");
252             }
253             return false;
254         }
255
256         public boolean scopeCheck(Element e, ScopedRoleDescriptor role, Collection ruleStack) {
257             // Are we scoped?
258             String scope=XML.assign(e.getAttributeNS(null,"Scope"));
259             if (scope == null) {
260                 // Are we allowed to be unscoped?
261                 if (scoped)
262                     log.warn("attribute (" + name + ") is scoped, no scope supplied, rejecting it");
263                 return !scoped;
264             }
265
266             // With the new algorithm, we evaluate each matching rule in sequence, separately.
267             Iterator srules = ruleStack.iterator();
268             while (srules.hasNext()) {
269                 SiteRule srule = (SiteRule)srules.next();
270                 
271                 // Now run any denials.
272                 Iterator denials = srule.scopeDenials.iterator();
273                 while (denials.hasNext()) {
274                     Rule denial = (Rule)denials.next();
275                     if ((denial.type==Rule.LITERAL && XML.safeCompare(denial.expression,scope)) ||
276                         (denial.type==Rule.REGEXP && match(denial.expression,scope))) {
277                         log.warn("attribute (" + name + ") scope {" + scope + "} denied by site rule, rejecting it");
278                         return false;
279                     }
280                     else if (denial.type==Rule.XPATH)
281                         log.warn("scope checking does not permit XPath rules");
282                 }
283
284                 // Now run any accepts.
285                 Iterator accepts = srule.scopeAccepts.iterator();
286                 while (accepts.hasNext()) {
287                     Rule accept = (Rule)accepts.next();
288                     if ((accept.type==Rule.LITERAL && XML.safeCompare(accept.expression,scope)) ||
289                         (accept.type==Rule.REGEXP && match(accept.expression,scope))) {
290                         log.debug("matching site rule, scope match");
291                         return true;
292                     }
293                     else if (accept.type==Rule.XPATH)
294                         log.warn("scope checking does not permit XPath rules");
295                 }
296             }
297
298             // If we still can't decide, defer to metadata.
299             if (role != null) {
300                 Iterator scopes=role.getScopes();
301                 while (scopes.hasNext()) {
302                     ScopedRoleDescriptor.Scope p = (Scope)scopes.next();
303                     if ((p.regexp && match(p.scope,scope)) || XML.safeCompare(p.scope,scope)) {
304                         log.debug("scope match via site metadata");
305                         return true;
306                     }
307                 }
308             }
309             
310             log.warn("attribute (" + name + ") scope {" + scope + "} not accepted");
311             return false;
312         }
313         
314         boolean accept(Element e, ScopedRoleDescriptor role) {
315             log.debug("evaluating value for attribute (" + name + ") from site (" +
316                     ((role!=null) ? role.getEntityDescriptor().getId() : "<unspecified>") +
317                     ")");
318             
319             // This is a complete revamp. The "any" cases become a degenerate case, the "least-specific" matching rule.
320             // The first step is to build a list of matching rules, most-specific to least-specific.
321             
322             ArrayList ruleStack = new ArrayList();
323             if (role != null) {
324                 // Primary match is against entityID.
325                 SiteRule srule=(SiteRule)siteMap.get(role.getEntityDescriptor().getId());
326                 if (srule!=null)
327                     ruleStack.add(srule);
328                 
329                 // Secondary matches are on groups.
330                 EntitiesDescriptor group=role.getEntityDescriptor().getEntitiesDescriptor();
331                 while (group != null) {
332                     srule=(SiteRule)siteMap.get(group.getName());
333                     if (srule!=null)
334                         ruleStack.add(srule);
335                     group = group.getEntitiesDescriptor();
336                 }
337             }
338             // Tertiary match is the AnySite rule.
339             ruleStack.add(anySiteRule);
340
341             // Still don't support complex content models...
342             Node n=e.getFirstChild();
343             boolean bSimple=(n != null && n.getNodeType()==Node.TEXT_NODE);
344
345             // With the new algorithm, we evaluate each matching rule in sequence, separately.
346             Iterator srules = ruleStack.iterator();
347             while (srules.hasNext()) {
348                 SiteRule srule = (SiteRule)srules.next();
349                 
350                 // Check for shortcut AnyValue blanket rule.
351                 if (srule.anyValue) {
352                     log.debug("matching site rule, any value match");
353                     return scopeCheck(e,role,ruleStack);
354                 }
355
356                 // Now run any denials.
357                 Iterator denials = srule.valueDenials.iterator();
358                 while (bSimple && denials.hasNext()) {
359                     Rule denial = (Rule)denials.next();
360                     switch (denial.type) {
361                         case Rule.LITERAL:
362                             if ((caseSensitive && !XML.safeCompare(denial.expression,n.getNodeValue())) ||
363                                 (!caseSensitive && denial.expression.equalsIgnoreCase(n.getNodeValue()))) {
364                                 log.warn("attribute (" + name + ") value explicitly denied by site rule, rejecting it");
365                                 return false;
366                             }
367                             break;
368                         
369                         case Rule.REGEXP:
370                             if (match(denial.expression,n.getNodeValue())) {
371                                 log.warn("attribute (" + name + ") value explicitly denied by site rule, rejecting it");
372                                 return false;
373                             }
374                             break;
375                         
376                         case Rule.XPATH:
377                             log.warn("implementation does not support XPath value rules");
378                             break;
379                     }
380                 }
381
382                 // Now run any accepts.
383                 Iterator accepts = srule.valueAccepts.iterator();
384                 while (bSimple && accepts.hasNext()) {
385                     Rule accept = (Rule)accepts.next();
386                     switch (accept.type) {
387                         case Rule.LITERAL:
388                             if ((caseSensitive && !XML.safeCompare(accept.expression,n.getNodeValue())) ||
389                                 (!caseSensitive && accept.expression.equalsIgnoreCase(n.getNodeValue()))) {
390                                 log.debug("site rule, value match");
391                                 return scopeCheck(e,role,ruleStack);
392                             }
393                             break;
394                         
395                         case Rule.REGEXP:
396                             if (match(accept.expression,n.getNodeValue())) {
397                                 log.debug("site rule, value match");
398                                 return scopeCheck(e,role,ruleStack);
399                             }
400                             break;
401                         
402                         case Rule.XPATH:
403                             log.warn("implementation does not support XPath value rules");
404                             break;
405                     }
406                 }
407             }
408
409             log.warn((bSimple ? "" : "complex ") + "attribute (" + name + ") value {" +
410                     n.getNodeValue() + ") could not be validated by policy, rejecting it"
411                     );
412             return false;
413         }
414     }
415     
416     public boolean anyAttribute() {
417         return anyAttribute;
418     }
419
420     public AttributeRule lookup(String name, String namespace) {
421         return (AttributeRule)attrmap.get(name + "||" + namespace);
422     }
423
424     public AttributeRule lookup(String alias) {
425         return (AttributeRule)aliasmap.get(alias);
426     }
427
428     public Iterator getAttributeRules() {
429         return attrmap.values().iterator();
430     }
431 }