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