Fix linker errors, and correct locking.
[shibboleth/cpp-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 shibsp {\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.XML")), 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 Rule::Rule(const DOMElement* e)\r
144 {\r
145     auto_ptr_char req(e->getAttributeNS(NULL,require));\r
146     if (!req.get() || !*req.get())\r
147         throw ConfigurationException("Access control rule missing require attribute");\r
148     m_alias=req.get();\r
149 \r
150     auto_arrayptr<char> vals(toUTF8(e->hasChildNodes() ? e->getFirstChild()->getNodeValue() : NULL));\r
151     if (!vals.get())\r
152         return;\r
153 \r
154     const XMLCh* flag = e->getAttributeNS(NULL,_list);\r
155     if (flag && (*flag == chLatin_f || *flag == chDigit_0)) {\r
156         if (*vals.get())\r
157             m_vals.push_back(vals.get());\r
158         return;\r
159     }\r
160 \r
161 #ifdef HAVE_STRTOK_R\r
162     char* pos=NULL;\r
163     const char* token=strtok_r(const_cast<char*>(vals.get())," ",&pos);\r
164 #else\r
165     const char* token=strtok(const_cast<char*>(vals.get())," ");\r
166 #endif\r
167     while (token) {\r
168         m_vals.push_back(token);\r
169 #ifdef HAVE_STRTOK_R\r
170         token=strtok_r(NULL," ",&pos);\r
171 #else\r
172         token=strtok(NULL," ");\r
173 #endif\r
174     }\r
175 }\r
176 \r
177 AccessControl::aclresult_t Rule::authorized(const SPRequest& request, const Session* session) const\r
178 {\r
179     // We can make this more complex later using pluggable comparison functions,\r
180     // but for now, just a straight port to the new Attribute API.\r
181 \r
182     // Map alias in rule to the attribute.\r
183     if (!session) {\r
184         request.log(SPRequest::SPWarn, "AccessControl plugin not given a valid session to evaluate, are you using lazy sessions?");\r
185         return shib_acl_false;\r
186     }\r
187 \r
188     if (m_alias == "valid-user") {\r
189         if (session) {\r
190             request.log(SPRequest::SPDebug,"AccessControl plugin accepting valid-user based on active session");\r
191             return shib_acl_true;\r
192         }\r
193         return shib_acl_false;\r
194     }\r
195     if (m_alias == "user") {\r
196         for (vector<string>::const_iterator i=m_vals.begin(); i!=m_vals.end(); ++i) {\r
197             if (*i == request.getRemoteUser()) {\r
198                 request.log(SPRequest::SPDebug, string("AccessControl plugin expecting REMOTE_USER (") + *i + "), authz granted");\r
199                 return shib_acl_true;\r
200             }\r
201         }\r
202         return shib_acl_false;\r
203     }\r
204     else if (m_alias == "authnContextClassRef") {\r
205         const char* ref = session->getAuthnContextClassRef();\r
206         for (vector<string>::const_iterator i=m_vals.begin(); ref && i!=m_vals.end(); ++i) {\r
207             if (!strcmp(i->c_str(),ref)) {\r
208                 request.log(SPRequest::SPDebug, string("AccessControl plugin expecting authnContextClassRef (") + *i + "), authz granted");\r
209                 return shib_acl_true;\r
210             }\r
211         }\r
212         return shib_acl_false;\r
213     }\r
214     else if (m_alias == "authnContextDeclRef") {\r
215         const char* ref = session->getAuthnContextDeclRef();\r
216         for (vector<string>::const_iterator i=m_vals.begin(); ref && i!=m_vals.end(); ++i) {\r
217             if (!strcmp(i->c_str(),ref)) {\r
218                 request.log(SPRequest::SPDebug, string("AccessControl plugin expecting authnContextDeclRef (") + *i + "), authz granted");\r
219                 return shib_acl_true;\r
220             }\r
221         }\r
222         return shib_acl_false;\r
223     }\r
224 \r
225     // Find the attribute(s) matching the require rule.\r
226     pair<multimap<string,const Attribute*>::const_iterator, multimap<string,const Attribute*>::const_iterator> attrs =\r
227         session->getIndexedAttributes().equal_range(m_alias);\r
228     if (attrs.first == attrs.second) {\r
229         request.log(SPRequest::SPWarn, string("rule requires attribute (") + m_alias + "), not found in session");\r
230         return shib_acl_false;\r
231     }\r
232 \r
233     for (; attrs.first != attrs.second; ++attrs.first) {\r
234         bool caseSensitive = attrs.first->second->isCaseSensitive();\r
235 \r
236         // Now we have to intersect the attribute's values against the rule's list.\r
237         const vector<string>& vals = attrs.first->second->getSerializedValues();\r
238         for (vector<string>::const_iterator i=m_vals.begin(); i!=m_vals.end(); ++i) {\r
239             for (vector<string>::const_iterator j=vals.begin(); j!=vals.end(); ++j) {\r
240                 if ((caseSensitive && *i == *j) || (!caseSensitive && !strcasecmp(i->c_str(),j->c_str()))) {\r
241                     request.log(SPRequest::SPDebug, string("AccessControl plugin expecting (") + *j + "), authz granted");\r
242                     return shib_acl_true;\r
243                 }\r
244             }\r
245         }\r
246     }\r
247 \r
248     return shib_acl_false;\r
249 }\r
250 \r
251 RuleRegex::RuleRegex(const DOMElement* e) : m_exp(toUTF8(e->hasChildNodes() ? e->getFirstChild()->getNodeValue() : NULL))\r
252 {\r
253     auto_ptr_char req(e->getAttributeNS(NULL,require));\r
254     if (!req.get() || !*req.get() || !m_exp.get() || !*m_exp.get())\r
255         throw ConfigurationException("Access control rule missing require attribute or element content.");\r
256     m_alias=req.get();\r
257 \r
258     const XMLCh* flag = e->getAttributeNS(NULL,ignoreCase);\r
259     bool ignore = (flag && (*flag == chLatin_t || *flag == chDigit_1));\r
260     try {\r
261         m_re = new RegularExpression(e->getFirstChild()->getNodeValue(), (ignore ? ignoreOption : &chNull));\r
262     }\r
263     catch (XMLException& ex) {\r
264         auto_ptr_char tmp(ex.getMessage());\r
265         throw ConfigurationException("Caught exception while parsing RuleRegex regular expression: $1", params(1,tmp.get()));\r
266     }\r
267 }\r
268 \r
269 AccessControl::aclresult_t RuleRegex::authorized(const SPRequest& request, const Session* session) const\r
270 {\r
271     // Map alias in rule to the attribute.\r
272     if (!session) {\r
273         request.log(SPRequest::SPWarn, "AccessControl plugin not given a valid session to evaluate, are you using lazy sessions?");\r
274         return shib_acl_false;\r
275     }\r
276 \r
277     if (m_alias == "valid-user") {\r
278         if (session) {\r
279             request.log(SPRequest::SPDebug,"AccessControl plugin accepting valid-user based on active session");\r
280             return shib_acl_true;\r
281         }\r
282         return shib_acl_false;\r
283     }\r
284 \r
285     try {\r
286         if (m_alias == "user") {\r
287             if (m_re->matches(request.getRemoteUser().c_str())) {\r
288                 request.log(SPRequest::SPDebug, string("AccessControl plugin expecting REMOTE_USER (") + m_exp.get() + "), authz granted");\r
289                 return shib_acl_true;\r
290             }\r
291             return shib_acl_false;\r
292         }\r
293         else if (m_alias == "authnContextClassRef") {\r
294             if (session->getAuthnContextClassRef() && m_re->matches(session->getAuthnContextClassRef())) {\r
295                 request.log(SPRequest::SPDebug, string("AccessControl plugin expecting authnContextClassRef (") + 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 == "authnContextDeclRef") {\r
301             if (session->getAuthnContextDeclRef() && m_re->matches(session->getAuthnContextDeclRef())) {\r
302                 request.log(SPRequest::SPDebug, string("AccessControl plugin expecting authnContextDeclRef (") + m_exp.get() + "), authz granted");\r
303                 return shib_acl_true;\r
304             }\r
305             return shib_acl_false;\r
306         }\r
307 \r
308         // Find the attribute(s) matching the require rule.\r
309         pair<multimap<string,const Attribute*>::const_iterator, multimap<string,const Attribute*>::const_iterator> attrs =\r
310             session->getIndexedAttributes().equal_range(m_alias);\r
311         if (attrs.first == attrs.second) {\r
312             request.log(SPRequest::SPWarn, string("rule requires attribute (") + m_alias + "), not found in session");\r
313             return shib_acl_false;\r
314         }\r
315 \r
316         for (; attrs.first != attrs.second; ++attrs.first) {\r
317             // Now we have to intersect the attribute's values against the regular expression.\r
318             const vector<string>& vals = attrs.first->second->getSerializedValues();\r
319             for (vector<string>::const_iterator j=vals.begin(); j!=vals.end(); ++j) {\r
320                 if (m_re->matches(j->c_str())) {\r
321                     request.log(SPRequest::SPDebug, string("AccessControl plugin expecting (") + m_exp.get() + "), authz granted");\r
322                     return shib_acl_true;\r
323                 }\r
324             }\r
325         }\r
326     }\r
327     catch (XMLException& ex) {\r
328         auto_ptr_char tmp(ex.getMessage());\r
329         request.log(SPRequest::SPError, string("caught exception while parsing RuleRegex regular expression: ") + tmp.get());\r
330     }\r
331 \r
332     return shib_acl_false;\r
333 }\r
334 \r
335 Operator::Operator(const DOMElement* e)\r
336 {\r
337     if (XMLString::equals(e->getLocalName(),NOT))\r
338         m_op=OP_NOT;\r
339     else if (XMLString::equals(e->getLocalName(),AND))\r
340         m_op=OP_AND;\r
341     else if (XMLString::equals(e->getLocalName(),OR))\r
342         m_op=OP_OR;\r
343     else\r
344         throw ConfigurationException("Unrecognized operator in access control rule");\r
345 \r
346     try {\r
347         e=XMLHelper::getFirstChildElement(e);\r
348         if (XMLString::equals(e->getLocalName(),_Rule))\r
349             m_operands.push_back(new Rule(e));\r
350         else if (XMLString::equals(e->getLocalName(),_RuleRegex))\r
351             m_operands.push_back(new RuleRegex(e));\r
352         else\r
353             m_operands.push_back(new Operator(e));\r
354 \r
355         if (m_op==OP_NOT)\r
356             return;\r
357 \r
358         e=XMLHelper::getNextSiblingElement(e);\r
359         while (e) {\r
360             if (XMLString::equals(e->getLocalName(),_Rule))\r
361                 m_operands.push_back(new Rule(e));\r
362             else if (XMLString::equals(e->getLocalName(),_RuleRegex))\r
363                 m_operands.push_back(new RuleRegex(e));\r
364             else\r
365                 m_operands.push_back(new Operator(e));\r
366             e=XMLHelper::getNextSiblingElement(e);\r
367         }\r
368     }\r
369     catch (exception&) {\r
370         for_each(m_operands.begin(),m_operands.end(),xmltooling::cleanup<AccessControl>());\r
371         throw;\r
372     }\r
373 }\r
374 \r
375 Operator::~Operator()\r
376 {\r
377     for_each(m_operands.begin(),m_operands.end(),xmltooling::cleanup<AccessControl>());\r
378 }\r
379 \r
380 AccessControl::aclresult_t Operator::authorized(const SPRequest& request, const Session* session) const\r
381 {\r
382     switch (m_op) {\r
383         case OP_NOT:\r
384             switch (m_operands.front()->authorized(request,session)) {\r
385                 case shib_acl_true:\r
386                     return shib_acl_false;\r
387                 case shib_acl_false:\r
388                     return shib_acl_true;\r
389                 default:\r
390                     return shib_acl_indeterminate;\r
391             }\r
392 \r
393         case OP_AND:\r
394         {\r
395             for (vector<AccessControl*>::const_iterator i=m_operands.begin(); i!=m_operands.end(); i++) {\r
396                 if ((*i)->authorized(request,session) != shib_acl_true)\r
397                     return shib_acl_false;\r
398             }\r
399             return shib_acl_true;\r
400         }\r
401 \r
402         case OP_OR:\r
403         {\r
404             for (vector<AccessControl*>::const_iterator i=m_operands.begin(); i!=m_operands.end(); i++) {\r
405                 if ((*i)->authorized(request,session) == shib_acl_true)\r
406                     return shib_acl_true;\r
407             }\r
408             return shib_acl_false;\r
409         }\r
410     }\r
411     request.log(SPRequest::SPWarn,"unknown operation in access control policy, denying access");\r
412     return shib_acl_false;\r
413 }\r
414 \r
415 pair<bool,DOMElement*> XMLAccessControl::load()\r
416 {\r
417     // Load from source using base class.\r
418     pair<bool,DOMElement*> raw = ReloadableXMLFile::load();\r
419 \r
420     // If we own it, wrap it.\r
421     XercesJanitor<DOMDocument> docjanitor(raw.first ? raw.second->getOwnerDocument() : NULL);\r
422 \r
423     // Check for AccessControl wrapper and drop a level.\r
424     if (XMLString::equals(raw.second->getLocalName(),_AccessControl))\r
425         raw.second = XMLHelper::getFirstChildElement(raw.second);\r
426 \r
427     AccessControl* authz;\r
428     if (XMLString::equals(raw.second->getLocalName(),_Rule))\r
429         authz=new Rule(raw.second);\r
430     else if (XMLString::equals(raw.second->getLocalName(),_RuleRegex))\r
431         authz=new RuleRegex(raw.second);\r
432     else\r
433         authz=new Operator(raw.second);\r
434 \r
435     delete m_rootAuthz;\r
436     m_rootAuthz = authz;\r
437     return make_pair(false,(DOMElement*)NULL);\r
438 }\r
439 \r
440 AccessControl::aclresult_t XMLAccessControl::authorized(const SPRequest& request, const Session* session) const\r
441 {\r
442     return m_rootAuthz ? m_rootAuthz->authorized(request,session) : shib_acl_false;\r
443 }\r