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