Add regex support to acl plugin.
[shibboleth/sp.git] / shibsp / impl / XMLAccessControl.cpp
1 /*\r
2  *  Copyright 2001-2007 Internet2\r
3  * \r
4  * Licensed under the Apache License, Version 2.0 (the "License");\r
5  * you may not use this file except in compliance with the License.\r
6  * You may obtain a copy of the License at\r
7  *\r
8  *     http://www.apache.org/licenses/LICENSE-2.0\r
9  *\r
10  * Unless required by applicable law or agreed to in writing, software\r
11  * distributed under the License is distributed on an "AS IS" BASIS,\r
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
13  * See the License for the specific language governing permissions and\r
14  * limitations under the License.\r
15  */\r
16 \r
17 /**\r
18  * XMLAccessControl.cpp\r
19  *\r
20  * XML-based access control syntax\r
21  */\r
22 \r
23 #include "internal.h"\r
24 #include "exceptions.h"\r
25 #include "AccessControl.h"\r
26 #include "SessionCache.h"\r
27 #include "SPRequest.h"\r
28 #include "attribute/Attribute.h"\r
29 \r
30 #include <xmltooling/util/ReloadableXMLFile.h>\r
31 #include <xmltooling/util/XMLHelper.h>\r
32 #include <xercesc/util/XMLUniDefs.hpp>\r
33 #include <xercesc/util/regx/RegularExpression.hpp>\r
34 \r
35 #ifndef HAVE_STRCASECMP\r
36 # define strcasecmp _stricmp\r
37 #endif\r
38 \r
39 using namespace shibsp;\r
40 using namespace xmltooling;\r
41 using namespace std;\r
42 \r
43 namespace {\r
44     \r
45     class Rule : public AccessControl\r
46     {\r
47     public:\r
48         Rule(const DOMElement* e);\r
49         ~Rule() {}\r
50 \r
51         Lockable* lock() {return this;}\r
52         void unlock() {}\r
53 \r
54         aclresult_t authorized(const SPRequest& request, const Session* session) const;\r
55     \r
56     private:\r
57         string m_alias;\r
58         vector <string> m_vals;\r
59     };\r
60     \r
61     class RuleRegex : public AccessControl\r
62     {\r
63     public:\r
64         RuleRegex(const DOMElement* e);\r
65         ~RuleRegex() {\r
66             delete m_re;\r
67         }\r
68         \r
69         Lockable* lock() {return this;}\r
70         void unlock() {}\r
71 \r
72         aclresult_t authorized(const SPRequest& request, const Session* session) const;\r
73         \r
74     private:\r
75         string m_alias;\r
76         auto_arrayptr<char> m_exp;\r
77         RegularExpression* m_re;\r
78     };\r
79     \r
80     class Operator : public AccessControl\r
81     {\r
82     public:\r
83         Operator(const DOMElement* e);\r
84         ~Operator();\r
85 \r
86         Lockable* lock() {return this;}\r
87         void unlock() {}\r
88 \r
89         aclresult_t authorized(const SPRequest& request, const Session* session) const;\r
90         \r
91     private:\r
92         enum operator_t { OP_NOT, OP_AND, OP_OR } m_op;\r
93         vector<AccessControl*> m_operands;\r
94     };\r
95 \r
96 #if defined (_MSC_VER)\r
97     #pragma warning( push )\r
98     #pragma warning( disable : 4250 )\r
99 #endif\r
100 \r
101     class XMLAccessControl : public AccessControl, public ReloadableXMLFile\r
102     {\r
103     public:\r
104         XMLAccessControl(const DOMElement* e)\r
105                 : ReloadableXMLFile(e, Category::getInstance(SHIBSP_LOGCAT".AccessControl")), m_rootAuthz(NULL) {\r
106             load(); // guarantees an exception or the policy is loaded\r
107         }\r
108         \r
109         ~XMLAccessControl() {\r
110             delete m_rootAuthz;\r
111         }\r
112 \r
113         aclresult_t authorized(const SPRequest& request, const Session* session) const;\r
114 \r
115     protected:\r
116         pair<bool,DOMElement*> load();\r
117 \r
118     private:\r
119         AccessControl* m_rootAuthz;\r
120     };\r
121 \r
122 #if defined (_MSC_VER)\r
123     #pragma warning( pop )\r
124 #endif\r
125 \r
126     AccessControl* SHIBSP_DLLLOCAL XMLAccessControlFactory(const DOMElement* const & e)\r
127     {\r
128         return new XMLAccessControl(e);\r
129     }\r
130 \r
131     static const XMLCh _AccessControl[] =   UNICODE_LITERAL_13(A,c,c,e,s,s,C,o,n,t,r,o,l);\r
132     static const XMLCh ignoreCase[] =       UNICODE_LITERAL_10(i,g,n,o,r,e,C,a,s,e);\r
133     static const XMLCh ignoreOption[] =     UNICODE_LITERAL_1(i);\r
134     static const XMLCh _list[] =            UNICODE_LITERAL_4(l,i,s,t);\r
135     static const XMLCh require[] =          UNICODE_LITERAL_7(r,e,q,u,i,r,e);\r
136     static const XMLCh NOT[] =              UNICODE_LITERAL_3(N,O,T);\r
137     static const XMLCh AND[] =              UNICODE_LITERAL_3(A,N,D);\r
138     static const XMLCh OR[] =               UNICODE_LITERAL_2(O,R);\r
139     static const XMLCh _Rule[] =            UNICODE_LITERAL_4(R,u,l,e);\r
140     static const XMLCh _RuleRegex[] =       UNICODE_LITERAL_9(R,u,l,e,R,e,g,e,x);\r
141 }\r
142 \r
143 void SHIBSP_API shibsp::registerAccessControls()\r
144 {\r
145     SPConfig& conf=SPConfig::getConfig();\r
146     conf.AccessControlManager.registerFactory(XML_ACCESS_CONTROL, XMLAccessControlFactory);\r
147     conf.AccessControlManager.registerFactory("edu.internet2.middleware.shibboleth.sp.provider.XMLAccessControl", XMLAccessControlFactory);\r
148 }\r
149 \r
150 Rule::Rule(const DOMElement* e)\r
151 {\r
152     auto_ptr_char req(e->getAttributeNS(NULL,require));\r
153     if (!req.get() || !*req.get())\r
154         throw ConfigurationException("Access control rule missing require attribute");\r
155     m_alias=req.get();\r
156 \r
157     auto_arrayptr<char> vals(toUTF8(e->hasChildNodes() ? e->getFirstChild()->getNodeValue() : NULL));\r
158     if (!vals.get())\r
159         return;\r
160     \r
161     const XMLCh* flag = e->getAttributeNS(NULL,_list);\r
162     if (flag && (*flag == chLatin_f || *flag == chDigit_0)) {\r
163         if (*vals.get())\r
164             m_vals.push_back(vals.get());\r
165         return;\r
166     }\r
167     \r
168 #ifdef HAVE_STRTOK_R\r
169     char* pos=NULL;\r
170     const char* token=strtok_r(const_cast<char*>(vals.get())," ",&pos);\r
171 #else\r
172     const char* token=strtok(const_cast<char*>(vals.get())," ");\r
173 #endif\r
174     while (token) {\r
175         m_vals.push_back(token);\r
176 #ifdef HAVE_STRTOK_R\r
177         token=strtok_r(NULL," ",&pos);\r
178 #else\r
179         token=strtok(NULL," ");\r
180 #endif\r
181     }\r
182 }\r
183 \r
184 AccessControl::aclresult_t Rule::authorized(const SPRequest& request, const Session* session) const\r
185 {\r
186     // We can make this more complex later using pluggable comparison functions,\r
187     // but for now, just a straight port to the new Attribute API.\r
188 \r
189     // Map alias in rule to the attribute.\r
190     if (!session) {\r
191         request.log(SPRequest::SPWarn, "AccessControl plugin not given a valid session to evaluate, are you using lazy sessions?");\r
192         return shib_acl_false;\r
193     }\r
194     \r
195     if (m_alias == "valid-user") {\r
196         if (session) {\r
197             request.log(SPRequest::SPDebug,"AccessControl plugin accepting valid-user based on active session");\r
198             return shib_acl_true;\r
199         }\r
200         return shib_acl_false;\r
201     }\r
202     if (m_alias == "user") {\r
203         for (vector<string>::const_iterator i=m_vals.begin(); i!=m_vals.end(); ++i) {\r
204             if (*i == request.getRemoteUser()) {\r
205                 request.log(SPRequest::SPDebug, string("AccessControl plugin expecting REMOTE_USER (") + *i + "), authz granted");\r
206                 return shib_acl_true;\r
207             }\r
208         }\r
209         return shib_acl_false;\r
210     }\r
211     else if (m_alias == "authnContextClassRef") {\r
212         const char* ref = session->getAuthnContextClassRef();\r
213         for (vector<string>::const_iterator i=m_vals.begin(); ref && i!=m_vals.end(); ++i) {\r
214             if (!strcmp(i->c_str(),ref)) {\r
215                 request.log(SPRequest::SPDebug, string("AccessControl plugin expecting authnContextClassRef (") + *i + "), authz granted");\r
216                 return shib_acl_true;\r
217             }\r
218         }\r
219         return shib_acl_false;\r
220     }\r
221     else if (m_alias == "authnContextDeclRef") {\r
222         const char* ref = session->getAuthnContextDeclRef();\r
223         for (vector<string>::const_iterator i=m_vals.begin(); ref && i!=m_vals.end(); ++i) {\r
224             if (!strcmp(i->c_str(),ref)) {\r
225                 request.log(SPRequest::SPDebug, string("AccessControl plugin expecting authnContextDeclRef (") + *i + "), authz granted");\r
226                 return shib_acl_true;\r
227             }\r
228         }\r
229         return shib_acl_false;\r
230     }\r
231 \r
232     // Find the attribute(s) matching the require rule.\r
233     pair<multimap<string,const Attribute*>::const_iterator, multimap<string,const Attribute*>::const_iterator> attrs =\r
234         session->getIndexedAttributes().equal_range(m_alias);\r
235     if (attrs.first == attrs.second) {\r
236         request.log(SPRequest::SPWarn, string("rule requires attribute (") + m_alias + "), not found in session");\r
237         return shib_acl_false;\r
238     }\r
239 \r
240     for (; attrs.first != attrs.second; ++attrs.first) {\r
241         bool caseSensitive = attrs.first->second->isCaseSensitive();\r
242 \r
243         // Now we have to intersect the attribute's values against the rule's list.\r
244         const vector<string>& vals = attrs.first->second->getSerializedValues();\r
245         for (vector<string>::const_iterator i=m_vals.begin(); i!=m_vals.end(); ++i) {\r
246             for (vector<string>::const_iterator j=vals.begin(); j!=vals.end(); ++j) {\r
247                 if ((caseSensitive && *i == *j) || (!caseSensitive && !strcasecmp(i->c_str(),j->c_str()))) {\r
248                     request.log(SPRequest::SPDebug, string("AccessControl plugin expecting (") + *j + "), authz granted");\r
249                     return shib_acl_true;\r
250                 }\r
251             }\r
252         }\r
253     }\r
254 \r
255     return shib_acl_false;\r
256 }\r
257 \r
258 RuleRegex::RuleRegex(const DOMElement* e) : m_exp(toUTF8(e->hasChildNodes() ? e->getFirstChild()->getNodeValue() : NULL))\r
259 {\r
260     auto_ptr_char req(e->getAttributeNS(NULL,require));\r
261     if (!req.get() || !*req.get() || !m_exp.get() || !*m_exp.get())\r
262         throw ConfigurationException("Access control rule missing require attribute or element content.");\r
263     m_alias=req.get();\r
264     \r
265     const XMLCh* flag = e->getAttributeNS(NULL,ignoreCase);\r
266     bool ignore = (flag && (*flag == chLatin_t || *flag == chDigit_1));\r
267     try {\r
268         m_re = new RegularExpression(e->getFirstChild()->getNodeValue(), (ignore ? ignoreOption : &chNull)); \r
269     }\r
270     catch (XMLException& ex) {\r
271         auto_ptr_char tmp(ex.getMessage());\r
272         throw ConfigurationException("Caught exception while parsing RuleRegex regular expression: $1", params(1,tmp.get()));\r
273     }\r
274 }\r
275 \r
276 AccessControl::aclresult_t RuleRegex::authorized(const SPRequest& request, const Session* session) const\r
277 {\r
278     // Map alias in rule to the attribute.\r
279     if (!session) {\r
280         request.log(SPRequest::SPWarn, "AccessControl plugin not given a valid session to evaluate, are you using lazy sessions?");\r
281         return shib_acl_false;\r
282     }\r
283     \r
284     if (m_alias == "valid-user") {\r
285         if (session) {\r
286             request.log(SPRequest::SPDebug,"AccessControl plugin accepting valid-user based on active session");\r
287             return shib_acl_true;\r
288         }\r
289         return shib_acl_false;\r
290     }\r
291 \r
292     try {\r
293         if (m_alias == "user") {\r
294             if (m_re->matches(request.getRemoteUser().c_str())) {\r
295                 request.log(SPRequest::SPDebug, string("AccessControl plugin expecting REMOTE_USER (") + m_exp.get() + "), authz granted");\r
296                 return shib_acl_true;\r
297             }\r
298             return shib_acl_false;\r
299         }\r
300         else if (m_alias == "authnContextClassRef") {\r
301             if (session->getAuthnContextClassRef() && m_re->matches(session->getAuthnContextClassRef())) {\r
302                 request.log(SPRequest::SPDebug, string("AccessControl plugin expecting authnContextClassRef (") + m_exp.get() + "), authz granted");\r
303                 return shib_acl_true;\r
304             }\r
305             return shib_acl_false;\r
306         }\r
307         else if (m_alias == "authnContextDeclRef") {\r
308             if (session->getAuthnContextDeclRef() && m_re->matches(session->getAuthnContextDeclRef())) {\r
309                 request.log(SPRequest::SPDebug, string("AccessControl plugin expecting authnContextDeclRef (") + m_exp.get() + "), authz granted");\r
310                 return shib_acl_true;\r
311             }\r
312             return shib_acl_false;\r
313         }\r
314 \r
315         // Find the attribute(s) matching the require rule.\r
316         pair<multimap<string,const Attribute*>::const_iterator, multimap<string,const Attribute*>::const_iterator> attrs =\r
317             session->getIndexedAttributes().equal_range(m_alias);\r
318         if (attrs.first == attrs.second) {\r
319             request.log(SPRequest::SPWarn, string("rule requires attribute (") + m_alias + "), not found in session");\r
320             return shib_acl_false;\r
321         }\r
322 \r
323         for (; attrs.first != attrs.second; ++attrs.first) {\r
324             // Now we have to intersect the attribute's values against the regular expression.\r
325             const vector<string>& vals = attrs.first->second->getSerializedValues();\r
326             for (vector<string>::const_iterator j=vals.begin(); j!=vals.end(); ++j) {\r
327                 if (m_re->matches(j->c_str())) {\r
328                     request.log(SPRequest::SPDebug, string("AccessControl plugin expecting (") + m_exp.get() + "), authz granted");\r
329                     return shib_acl_true;\r
330                 }\r
331             }\r
332         }\r
333     }\r
334     catch (XMLException& ex) {\r
335         auto_ptr_char tmp(ex.getMessage());\r
336         request.log(SPRequest::SPError, string("caught exception while parsing RuleRegex regular expression: ") + tmp.get());\r
337     }\r
338     \r
339     return shib_acl_false;\r
340 }\r
341 \r
342 Operator::Operator(const DOMElement* e)\r
343 {\r
344     if (XMLString::equals(e->getLocalName(),NOT))\r
345         m_op=OP_NOT;\r
346     else if (XMLString::equals(e->getLocalName(),AND))\r
347         m_op=OP_AND;\r
348     else if (XMLString::equals(e->getLocalName(),OR))\r
349         m_op=OP_OR;\r
350     else\r
351         throw ConfigurationException("Unrecognized operator in access control rule");\r
352     \r
353     try {\r
354         e=XMLHelper::getFirstChildElement(e);\r
355         if (XMLString::equals(e->getLocalName(),_Rule))\r
356             m_operands.push_back(new Rule(e));\r
357         else if (XMLString::equals(e->getLocalName(),_RuleRegex))\r
358             m_operands.push_back(new RuleRegex(e));\r
359         else\r
360             m_operands.push_back(new Operator(e));\r
361         \r
362         if (m_op==OP_NOT)\r
363             return;\r
364         \r
365         e=XMLHelper::getNextSiblingElement(e);\r
366         while (e) {\r
367             if (XMLString::equals(e->getLocalName(),_Rule))\r
368                 m_operands.push_back(new Rule(e));\r
369             else if (XMLString::equals(e->getLocalName(),_RuleRegex))\r
370                 m_operands.push_back(new RuleRegex(e));\r
371             else\r
372                 m_operands.push_back(new Operator(e));\r
373             e=XMLHelper::getNextSiblingElement(e);\r
374         }\r
375     }\r
376     catch (exception&) {\r
377         for_each(m_operands.begin(),m_operands.end(),xmltooling::cleanup<AccessControl>());\r
378         throw;\r
379     }\r
380 }\r
381 \r
382 Operator::~Operator()\r
383 {\r
384     for_each(m_operands.begin(),m_operands.end(),xmltooling::cleanup<AccessControl>());\r
385 }\r
386 \r
387 AccessControl::aclresult_t Operator::authorized(const SPRequest& request, const Session* session) const\r
388 {\r
389     switch (m_op) {\r
390         case OP_NOT:\r
391             switch (m_operands.front()->authorized(request,session)) {\r
392                 case shib_acl_true:\r
393                     return shib_acl_false;\r
394                 case shib_acl_false:\r
395                     return shib_acl_true;\r
396                 default:\r
397                     return shib_acl_indeterminate;\r
398             }\r
399         \r
400         case OP_AND:\r
401         {\r
402             for (vector<AccessControl*>::const_iterator i=m_operands.begin(); i!=m_operands.end(); i++) {\r
403                 if ((*i)->authorized(request,session) != shib_acl_true)\r
404                     return shib_acl_false;\r
405             }\r
406             return shib_acl_true;\r
407         }\r
408         \r
409         case OP_OR:\r
410         {\r
411             for (vector<AccessControl*>::const_iterator i=m_operands.begin(); i!=m_operands.end(); i++) {\r
412                 if ((*i)->authorized(request,session) == shib_acl_true)\r
413                     return shib_acl_true;\r
414             }\r
415             return shib_acl_false;\r
416         }\r
417     }\r
418     request.log(SPRequest::SPWarn,"unknown operation in access control policy, denying access");\r
419     return shib_acl_false;\r
420 }\r
421 \r
422 pair<bool,DOMElement*> XMLAccessControl::load()\r
423 {\r
424     // Load from source using base class.\r
425     pair<bool,DOMElement*> raw = ReloadableXMLFile::load();\r
426     \r
427     // If we own it, wrap it.\r
428     XercesJanitor<DOMDocument> docjanitor(raw.first ? raw.second->getOwnerDocument() : NULL);\r
429 \r
430     // Check for AccessControl wrapper and drop a level.\r
431     if (XMLString::equals(raw.second->getLocalName(),_AccessControl))\r
432         raw.second = XMLHelper::getFirstChildElement(raw.second);\r
433     \r
434     AccessControl* authz;\r
435     if (XMLString::equals(raw.second->getLocalName(),_Rule))\r
436         authz=new Rule(raw.second);\r
437     else if (XMLString::equals(raw.second->getLocalName(),_RuleRegex))\r
438         authz=new RuleRegex(raw.second);\r
439     else\r
440         authz=new Operator(raw.second);\r
441 \r
442     delete m_rootAuthz;\r
443     m_rootAuthz = authz;\r
444     return make_pair(false,(DOMElement*)NULL);\r
445 }\r
446 \r
447 AccessControl::aclresult_t XMLAccessControl::authorized(const SPRequest& request, const Session* session) const\r
448 {\r
449     return m_rootAuthz ? m_rootAuthz->authorized(request,session) : shib_acl_false;\r
450 }\r