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