2 * Copyright 2001-2007 Internet2
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 /* XMLAAP.cpp - XML AAP implementation
27 #include <log4cpp/Category.hh>
28 #include <shibsp/metadata/MetadataExt.h>
29 #include <shibsp/util/SPConstants.h>
30 #include <xmltooling/util/ReloadableXMLFile.h>
31 #include <xmltooling/util/XMLHelper.h>
33 using namespace shibsp;
34 using namespace shibboleth;
36 using namespace opensaml::saml2md;
37 using namespace xmltooling;
38 using namespace log4cpp;
41 #include <xercesc/util/regx/RegularExpression.hpp>
48 XMLAAPImpl(const DOMElement* e);
51 void setDocument(DOMDocument* doc) {
55 class AttributeRule : public IAttributeRule
58 AttributeRule(const DOMElement* e);
61 const XMLCh* getName() const { return m_name; }
62 const XMLCh* getNamespace() const { return m_namespace; }
63 const char* getAlias() const { return m_alias.get(); }
64 const char* getHeader() const { return m_header.get(); }
65 bool getCaseSensitive() const { return m_caseSensitive; }
66 bool getScoped() const { return m_scoped; }
67 void apply(SAMLAttribute& attribute, const RoleDescriptor* role=NULL) const;
69 enum value_type { literal, regexp, xpath };
72 const XMLCh* m_namespace;
73 xmltooling::auto_ptr_char m_alias;
74 xmltooling::auto_ptr_char m_header;
80 SiteRule() : anyValue(false) {}
82 vector<pair<value_type,const XMLCh*> > valueDenials;
83 vector<pair<value_type,const XMLCh*> > valueAccepts;
84 vector<pair<value_type,const XMLCh*> > scopeDenials;
85 vector<pair<value_type,const XMLCh*> > scopeAccepts;
88 value_type toValueType(const DOMElement* e);
91 const RoleDescriptor* role,
92 const vector<const SiteRule*>& ruleStack
94 bool accept(const DOMElement* e, const RoleDescriptor* role=NULL) const;
96 SiteRule m_anySiteRule;
98 typedef map<xmltooling::xstring,SiteRule> sitemap_t;
100 typedef map<string,SiteRule> sitemap_t;
105 DOMDocument* m_document;
107 vector<const IAttributeRule*> m_attrs;
108 map<string,const IAttributeRule*> m_aliasMap;
110 typedef map<xmltooling::xstring,AttributeRule*> attrmap_t;
112 typedef map<string,AttributeRule*> attrmap_t;
117 #if defined (_MSC_VER)
118 #pragma warning( push )
119 #pragma warning( disable : 4250 )
122 class XMLAAP : public IAAP, public ReloadableXMLFile
125 XMLAAP(const DOMElement* e) : ReloadableXMLFile(e), m_impl(NULL) {
132 bool anyAttribute() const {return m_impl->anyAttribute;}
133 const IAttributeRule* lookup(const XMLCh* attrName, const XMLCh* attrNamespace=NULL) const;
134 const IAttributeRule* lookup(const char* alias) const;
135 Iterator<const IAttributeRule*> getAttributeRules() const;
138 pair<bool,DOMElement*> load();
142 #if defined (_MSC_VER)
143 #pragma warning( pop )
146 static const XMLCh Accept[]= UNICODE_LITERAL_6(A,c,c,e,p,t);
147 static const XMLCh Alias[]= UNICODE_LITERAL_5(A,l,i,a,s);
148 static const XMLCh AnyAttribute[]= UNICODE_LITERAL_12(A,n,y,A,t,t,r,i,b,u,t,e);
149 static const XMLCh AnySite[]= UNICODE_LITERAL_7(A,n,y,S,i,t,e);
150 static const XMLCh AnyValue[]= UNICODE_LITERAL_8(A,n,y,V,a,l,u,e);
151 static const XMLCh _AttributeRule[]=UNICODE_LITERAL_13(A,t,t,r,i,b,u,t,e,R,u,l,e);
152 static const XMLCh CaseSensitive[]= UNICODE_LITERAL_13(C,a,s,e,S,e,n,s,i,t,i,v,e);
153 static const XMLCh Header[]= UNICODE_LITERAL_6(H,e,a,d,e,r);
154 static const XMLCh Name[]= UNICODE_LITERAL_4(N,a,m,e);
155 static const XMLCh Namespace[]= UNICODE_LITERAL_9(N,a,m,e,s,p,a,c,e);
156 static const XMLCh Scoped[]= UNICODE_LITERAL_6(S,c,o,p,e,d);
157 static const XMLCh _SiteRule[]= UNICODE_LITERAL_8(S,i,t,e,R,u,l,e);
158 static const XMLCh Type[]= UNICODE_LITERAL_4(T,y,p,e);
159 static const XMLCh Value[]= UNICODE_LITERAL_5(V,a,l,u,e);
161 static const XMLCh _literal[]= UNICODE_LITERAL_7(l,i,t,e,r,a,l);
162 static const XMLCh _regexp[]= UNICODE_LITERAL_6(r,e,g,e,x,p);
163 static const XMLCh _xpath[]= UNICODE_LITERAL_5(x,p,a,t,h);
166 IPlugIn* XMLAAPFactory(const DOMElement* e)
168 return new XMLAAP(e);
171 pair<bool,DOMElement*> XMLAAP::load()
173 // Load from source using base class.
174 pair<bool,DOMElement*> raw = ReloadableXMLFile::load();
176 // If we own it, wrap it.
177 XercesJanitor<DOMDocument> docjanitor(raw.first ? raw.second->getOwnerDocument() : NULL);
179 XMLAAPImpl* impl = new XMLAAPImpl(raw.second);
181 // If we held the document, transfer it to the impl. If we didn't, it's a no-op.
182 impl->setDocument(docjanitor.release());
187 return make_pair(false,(DOMElement*)NULL);
190 XMLAAPImpl::XMLAAPImpl(const DOMElement* e) : anyAttribute(false), m_document(NULL)
193 xmltooling::NDC ndc("XMLAAPImpl");
195 Category& log=Category::getInstance(XMLPROVIDERS_LOGCAT".AAP");
198 // Check for AnyAttribute element.
199 if (XMLHelper::getFirstChildElement(e,AnyAttribute)) {
201 log.warn("<AnyAttribute> found, will short-circuit all attribute value and scope filtering");
204 // Loop over the AttributeRule elements.
205 e = XMLHelper::getFirstChildElement(e, _AttributeRule);
207 AttributeRule* rule=new AttributeRule(e);
209 xmltooling::xstring key=rule->getName();
210 key=key + chBang + chBang + (rule->getNamespace() ? rule->getNamespace() : shibspconstants::SHIB1_ATTRIBUTE_NAMESPACE_URI);
212 xmltooling::auto_ptr_char aname(rule->getName());
213 string key(aname.get());
215 if (rule->getNamespace()) {
216 xmltooling::auto_ptr_char ans(rule->getNamespace());
220 key+="urn:mace:shibboleth:1.0:attributeNamespace:uri";
224 m_attrs.push_back(rule);
225 if (rule->getAlias()) {
226 // user can only apply to REMOTE_USER
227 if (!strcmp(rule->getAlias(),"user")) {
228 if (strcmp(rule->getHeader(),"REMOTE_USER"))
229 log.error("<AttributeRule> cannot specify Alias of 'user', please use alternate value");
231 m_aliasMap[rule->getAlias()]=rule;
235 m_aliasMap[rule->getAlias()]=rule;
239 e = XMLHelper::getNextSiblingElement(e, _AttributeRule);
244 for_each(m_attrMap.begin(),m_attrMap.end(),xmltooling::cleanup_pair<xmltooling::xstring,AttributeRule>());
246 for_each(m_attrMap.begin(),m_attrMap.end(),xmltooling::cleanup_pair<string,AttributeRule>());
252 XMLAAPImpl::~XMLAAPImpl()
255 for_each(m_attrMap.begin(),m_attrMap.end(),xmltooling::cleanup_pair<xmltooling::xstring,AttributeRule>());
257 for_each(m_attrMap.begin(),m_attrMap.end(),xmltooling::cleanup_pair<string,AttributeRule>());
260 m_document->release();
263 XMLAAPImpl::AttributeRule::AttributeRule(const DOMElement* e) :
264 m_alias(e->hasAttributeNS(NULL,Alias) ? e->getAttributeNS(NULL,Alias) : NULL),
265 m_header(e->hasAttributeNS(NULL,Header) ? e->getAttributeNS(NULL,Header) : NULL),
269 m_name=e->getAttributeNS(NULL,Name);
270 m_namespace=e->getAttributeNS(NULL,Namespace);
271 if (!m_namespace || !*m_namespace)
272 m_namespace=shibspconstants::SHIB1_ATTRIBUTE_NAMESPACE_URI;
274 const XMLCh* caseSensitive=e->getAttributeNS(NULL,CaseSensitive);
275 m_caseSensitive=(!caseSensitive || !*caseSensitive || *caseSensitive==chDigit_1 || *caseSensitive==chLatin_t);
277 const XMLCh* scoped=e->getAttributeNS(NULL,Scoped);
278 m_scoped=(scoped && (*scoped==chDigit_1 || *scoped==chLatin_t));
280 // Check for an AnySite rule.
281 const DOMElement* anysite = XMLHelper::getFirstChildElement(e);
282 if (anysite && XMLString::equals(anysite->getLocalName(),AnySite)) {
283 // Process Scope elements.
284 const DOMElement* se = XMLHelper::getFirstChildElement(anysite,Scope::LOCAL_NAME);
287 DOMNode* valnode=se->getFirstChild();
288 if (valnode && valnode->getNodeType()==DOMNode::TEXT_NODE) {
289 const XMLCh* accept=se->getAttributeNS(NULL,Accept);
290 if (!accept || !*accept || *accept==chDigit_1 || *accept==chLatin_t)
291 m_anySiteRule.scopeAccepts.push_back(pair<value_type,const XMLCh*>(toValueType(se),valnode->getNodeValue()));
293 m_anySiteRule.scopeDenials.push_back(pair<value_type,const XMLCh*>(toValueType(se),valnode->getNodeValue()));
296 se = XMLHelper::getNextSiblingElement(se,Scope::LOCAL_NAME);
299 // Check for an AnyValue rule.
300 if (XMLHelper::getFirstChildElement(anysite,AnyValue)) {
301 m_anySiteRule.anyValue=true;
304 // Process each Value element.
305 const DOMElement* ve = XMLHelper::getFirstChildElement(anysite,Value);
307 DOMNode* valnode=ve->getFirstChild();
308 if (valnode && valnode->getNodeType()==DOMNode::TEXT_NODE) {
309 const XMLCh* accept=ve->getAttributeNS(NULL,Accept);
310 if (!accept || !*accept || *accept==chDigit_1 || *accept==chLatin_t)
311 m_anySiteRule.valueAccepts.push_back(pair<value_type,const XMLCh*>(toValueType(ve),valnode->getNodeValue()));
313 m_anySiteRule.valueDenials.push_back(pair<value_type,const XMLCh*>(toValueType(ve),valnode->getNodeValue()));
316 ve = XMLHelper::getNextSiblingElement(ve,Value);
321 // Loop over the SiteRule elements.
322 const DOMElement* sr = XMLHelper::getFirstChildElement(e,_SiteRule);
324 const XMLCh* srulename=sr->getAttributeNS(NULL,Name);
326 m_siteMap[srulename]=SiteRule();
327 SiteRule& srule=m_siteMap[srulename];
329 xmltooling::auto_ptr_char srulename2(srulename);
330 m_siteMap[srulename2.get()]=SiteRule();
331 SiteRule& srule=m_siteMap[srulename2.get()];
334 // Process Scope elements.
335 const DOMElement* se = XMLHelper::getFirstChildElement(sr,Scope::LOCAL_NAME);
338 DOMNode* valnode=se->getFirstChild();
339 if (valnode && valnode->getNodeType()==DOMNode::TEXT_NODE) {
340 const XMLCh* accept=se->getAttributeNS(NULL,Accept);
341 if (!accept || !*accept || *accept==chDigit_1 || *accept==chLatin_t)
342 srule.scopeAccepts.push_back(pair<value_type,const XMLCh*>(toValueType(se),valnode->getNodeValue()));
344 srule.scopeDenials.push_back(pair<value_type,const XMLCh*>(toValueType(se),valnode->getNodeValue()));
347 se = XMLHelper::getNextSiblingElement(se,Scope::LOCAL_NAME);
350 // Check for an AnyValue rule.
351 if (XMLHelper::getFirstChildElement(sr,AnyValue)) {
356 // Process each Value element.
357 const DOMElement* ve = XMLHelper::getFirstChildElement(sr,Value);
359 DOMNode* valnode=ve->getFirstChild();
360 if (valnode && valnode->getNodeType()==DOMNode::TEXT_NODE) {
361 const XMLCh* accept=ve->getAttributeNS(NULL,Accept);
362 if (!accept || !*accept || *accept==chDigit_1 || *accept==chLatin_t)
363 srule.valueAccepts.push_back(pair<value_type,const XMLCh*>(toValueType(ve),valnode->getNodeValue()));
365 srule.valueDenials.push_back(pair<value_type,const XMLCh*>(toValueType(ve),valnode->getNodeValue()));
368 ve = XMLHelper::getNextSiblingElement(ve,Value);
372 sr = XMLHelper::getNextSiblingElement(sr,_SiteRule);
376 XMLAAPImpl::AttributeRule::value_type XMLAAPImpl::AttributeRule::toValueType(const DOMElement* e)
378 if (XMLString::equals(_literal,e->getAttributeNS(NULL,Type)))
380 else if (XMLString::equals(_regexp,e->getAttributeNS(NULL,Type)))
382 else if (XMLString::equals(_xpath,e->getAttributeNS(NULL,Type)))
384 throw ConfigurationException("Found an invalid value or scope rule type.");
387 const IAttributeRule* XMLAAP::lookup(const XMLCh* attrName, const XMLCh* attrNamespace) const
390 xmltooling::xstring key=attrName;
391 key=key + chBang + chBang + (attrNamespace ? attrNamespace : shibspconstants::SHIB1_ATTRIBUTE_NAMESPACE_URI);
393 xmltooling::auto_ptr_char aname(attrName);
394 string key=aname.get();
397 xmltooling::auto_ptr_char ans(attrNamespace);
401 key+="urn:mace:shibboleth:1.0:attributeNamespace:uri";
404 XMLAAPImpl::attrmap_t::const_iterator i=m_impl->m_attrMap.find(key);
405 return (i==m_impl->m_attrMap.end()) ? NULL : i->second;
408 const IAttributeRule* XMLAAP::lookup(const char* alias) const
410 map<string,const IAttributeRule*>::const_iterator i=m_impl->m_aliasMap.find(alias);
411 return (i==m_impl->m_aliasMap.end()) ? NULL : i->second;
414 Iterator<const IAttributeRule*> XMLAAP::getAttributeRules() const
416 return m_impl->m_attrs;
420 bool match(const XMLCh* exp, const XMLCh* test)
423 RegularExpression re(exp);
424 if (re.matches(test))
427 catch (XMLException& ex) {
428 xmltooling::auto_ptr_char tmp(ex.getMessage());
429 Category::getInstance(XMLPROVIDERS_LOGCAT".AAP").errorStream()
430 << "caught exception while parsing regular expression: " << tmp.get() << CategoryStream::ENDLINE;
436 bool XMLAAPImpl::AttributeRule::scopeCheck(
438 const RoleDescriptor* role,
439 const vector<const SiteRule*>& ruleStack
443 xmltooling::NDC ndc("scopeCheck");
445 Category& log=Category::getInstance(XMLPROVIDERS_LOGCAT".AAP");
448 const XMLCh* scope=e->getAttributeNS(NULL,Scope::LOCAL_NAME);
449 if (!scope || !*scope) {
450 // Are we allowed to be unscoped?
451 if (m_scoped && log.isWarnEnabled()) {
452 xmltooling::auto_ptr_char temp(m_name);
453 log.warn("attribute (%s) is scoped, no scope supplied, rejecting it",temp.get());
458 // With the new algorithm, we evaluate each matching rule in sequence, separately.
459 for (vector<const SiteRule*>::const_iterator rule=ruleStack.begin(); rule!=ruleStack.end(); rule++) {
461 // Now run any denials.
462 vector<pair<value_type,const XMLCh*> >::const_iterator i;
463 for (i=(*rule)->scopeDenials.begin(); i!=(*rule)->scopeDenials.end(); i++) {
464 if ((i->first==literal && XMLString::equals(i->second,scope)) ||
465 (i->first==regexp && match(i->second,scope))) {
466 if (log.isWarnEnabled()) {
467 xmltooling::auto_ptr_char temp(m_name);
468 xmltooling::auto_ptr_char temp2(scope);
469 log.warn("attribute (%s) scope (%s) denied by site rule, rejecting it",temp.get(),temp2.get());
473 else if (i->first==xpath)
474 log.warn("scope checking does not permit XPath rules");
477 // Now run any accepts.
478 for (i=(*rule)->scopeAccepts.begin(); i!=(*rule)->scopeAccepts.end(); i++) {
479 if ((i->first==literal && XMLString::equals(i->second,scope)) ||
480 (i->first==regexp && match(i->second,scope))) {
481 log.debug("matching site rule, scope match");
484 else if (i->first==xpath)
485 log.warn("scope checking does not permit XPath rules");
489 // If we still can't decide, defer to metadata.
490 if (role && role->getExtensions()) {
491 const vector<XMLObject*>& exts=const_cast<const Extensions*>(role->getExtensions())->getUnknownXMLObjects();
492 for (vector<XMLObject*>::const_iterator it=exts.begin(); it!=exts.end(); ++it) {
493 const Scope* s=dynamic_cast<const Scope*>(*it);
496 if ((s->Regexp() && match(s->getValue(),scope)) || XMLString::equals(s->getValue(),scope)) {
497 log.debug("scope match via site metadata");
503 if (log.isWarnEnabled()) {
504 xmltooling::auto_ptr_char temp(m_name);
505 xmltooling::auto_ptr_char temp2(scope);
506 log.warn("attribute (%s) scope (%s) not accepted",temp.get(),temp2.get());
511 bool XMLAAPImpl::AttributeRule::accept(const DOMElement* e, const RoleDescriptor* role) const
514 xmltooling::NDC ndc("accept");
516 Category& log=Category::getInstance(XMLPROVIDERS_LOGCAT".AAP");
518 const EntityDescriptor* source = role ? dynamic_cast<const EntityDescriptor*>(role->getParent()) : NULL;
520 if (log.isDebugEnabled()) {
521 xmltooling::auto_ptr_char temp(m_name);
522 xmltooling::auto_ptr_char temp2(source ? source->getEntityID() : NULL);
523 log.debug("evaluating value for attribute (%s) from site (%s)",temp.get(),temp2.get() ? temp2.get() : "<unspecified>");
526 // This is a complete revamp. The "any" cases become a degenerate case, the "least-specific" matching rule.
527 // The first step is to build a list of matching rules, most-specific to least-specific.
529 vector<const SiteRule*> ruleStack;
531 // Primary match is against entityID.
533 const XMLCh* os=source->getEntityID();
535 auto_ptr_char pos(source->getEntityID());
536 const char* os=pos.get();
538 sitemap_t::const_iterator srule=m_siteMap.find(os);
539 if (srule!=m_siteMap.end())
540 ruleStack.push_back(&srule->second);
542 // Secondary matches are on groups.
543 const EntitiesDescriptor* group=dynamic_cast<const EntitiesDescriptor*>(source->getParent());
545 if (group->getName()) {
549 auto_ptr_char gname(group->getName());
550 const char* os=gname.get();
552 srule=m_siteMap.find(os);
553 if (srule!=m_siteMap.end())
554 ruleStack.push_back(&srule->second);
556 group=dynamic_cast<const EntitiesDescriptor*>(group->getParent());
559 // Tertiary match is the AnySite rule.
560 ruleStack.push_back(&m_anySiteRule);
562 // Still don't support complex content models...
563 DOMNode* n=e->getFirstChild();
564 bool bSimple=(n && n->getNodeType()==DOMNode::TEXT_NODE);
566 // With the new algorithm, we evaluate each matching rule in sequence, separately.
567 for (vector<const SiteRule*>::const_iterator rule=ruleStack.begin(); rule!=ruleStack.end(); rule++) {
569 // Check for shortcut AnyValue blanket rule.
570 if ((*rule)->anyValue) {
571 log.debug("matching site rule, any value match");
572 return scopeCheck(e,role,ruleStack);
575 // Now run any denials.
576 vector<pair<value_type,const XMLCh*> >::const_iterator i;
577 for (i=(*rule)->valueDenials.begin(); bSimple && i!=(*rule)->valueDenials.end(); i++) {
580 if ((m_caseSensitive && !XMLString::compareString(i->second,n->getNodeValue())) ||
581 (!m_caseSensitive && !XMLString::compareIString(i->second,n->getNodeValue()))) {
582 if (log.isWarnEnabled()) {
583 xmltooling::auto_ptr_char temp(m_name);
584 log.warn("attribute (%s) value explicitly denied by site rule, rejecting it",temp.get());
591 if (match(i->second,n->getNodeValue())) {
592 if (log.isWarnEnabled()) {
593 xmltooling::auto_ptr_char temp(m_name);
594 log.warn("attribute (%s) value explicitly denied by site rule, rejecting it",temp.get());
601 log.warn("implementation does not support XPath value rules");
606 // Now run any accepts.
607 for (i=(*rule)->valueAccepts.begin(); bSimple && i!=(*rule)->valueAccepts.end(); i++) {
610 if ((m_caseSensitive && !XMLString::compareString(i->second,n->getNodeValue())) ||
611 (!m_caseSensitive && !XMLString::compareIString(i->second,n->getNodeValue()))) {
612 log.debug("site rule, value match");
613 return scopeCheck(e,role,ruleStack);
618 if (match(i->second,n->getNodeValue())) {
619 log.debug("site rule, value match");
620 return scopeCheck(e,role,ruleStack);
625 log.warn("implementation does not support XPath value rules");
631 if (log.isWarnEnabled()) {
632 xmltooling::auto_ptr_char temp(m_name);
633 xmltooling::auto_ptr_char temp2(n->getNodeValue());
634 log.warn("%sattribute (%s) value (%s) could not be validated by policy, rejecting it",
635 (bSimple ? "" : "complex "),temp.get(),temp2.get());
640 void XMLAAPImpl::AttributeRule::apply(SAMLAttribute& attribute, const RoleDescriptor* role) const
643 DOMNodeList* vals=attribute.getValueElements();
645 for (XMLSize_t i=0; vals && i < vals->getLength(); i++) {
646 if (!accept(static_cast<DOMElement*>(vals->item(i)),role))
647 attribute.removeValue(i2);
652 // Now see if we trashed it irrevocably.
653 attribute.checkValidity();