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