1 /*
2  * Copyright (C) 1996-2021 The Squid Software Foundation and contributors
3  *
4  * Squid software is distributed under GPLv2+ license and includes
5  * contributions from numerous individuals and organizations.
6  * Please see the COPYING and CONTRIBUTORS files for details.
7  */
8 
9 /* DEBUG: section 28    Access Control */
10 
11 #include "squid.h"
12 #include "acl/Checklist.h"
13 #include "acl/Tree.h"
14 #include "Debug.h"
15 #include "profiler/Profiler.h"
16 
17 #include <algorithm>
18 
19 /// common parts of nonBlockingCheck() and resumeNonBlockingCheck()
20 bool
prepNonBlocking()21 ACLChecklist::prepNonBlocking()
22 {
23     assert(accessList);
24 
25     if (callerGone()) {
26         checkCallback(ACCESS_DUNNO); // the answer does not really matter
27         return false;
28     }
29 
30     /** \par
31      * If the accessList is no longer valid (i.e. its been
32      * freed because of a reconfigure), then bail with ACCESS_DUNNO.
33      */
34 
35     if (!cbdataReferenceValid(accessList)) {
36         cbdataReferenceDone(accessList);
37         debugs(28, 4, "ACLChecklist::check: " << this << " accessList is invalid");
38         checkCallback(ACCESS_DUNNO);
39         return false;
40     }
41 
42     return true;
43 }
44 
45 void
completeNonBlocking()46 ACLChecklist::completeNonBlocking()
47 {
48     assert(!asyncInProgress());
49 
50     if (!finished())
51         calcImplicitAnswer();
52 
53     cbdataReferenceDone(accessList);
54     checkCallback(currentAnswer());
55 }
56 
57 void
markFinished(const allow_t & finalAnswer,const char * reason)58 ACLChecklist::markFinished(const allow_t &finalAnswer, const char *reason)
59 {
60     assert (!finished() && !asyncInProgress());
61     finished_ = true;
62     allow_ = finalAnswer;
63     debugs(28, 3, HERE << this << " answer " << allow_ << " for " << reason);
64 }
65 
66 /// Called first (and once) by all checks to initialize their state
67 void
preCheck(const char * what)68 ACLChecklist::preCheck(const char *what)
69 {
70     debugs(28, 3, HERE << this << " checking " << what);
71 
72     // concurrent checks using the same Checklist are not supported
73     assert(!occupied_);
74     occupied_ = true;
75     asyncLoopDepth_ = 0;
76 
77     AclMatchedName = NULL;
78     finished_ = false;
79 }
80 
81 bool
matchChild(const Acl::InnerNode * current,Acl::Nodes::const_iterator pos,const ACL * child)82 ACLChecklist::matchChild(const Acl::InnerNode *current, Acl::Nodes::const_iterator pos, const ACL *child)
83 {
84     assert(current && child);
85 
86     // Remember the current tree location to prevent "async loop" cases where
87     // the same child node wants to go async more than once.
88     matchLoc_ = Breadcrumb(current, pos);
89     asyncLoopDepth_ = 0;
90 
91     // if there are any breadcrumbs left, then follow them on the way down
92     bool result = false;
93     if (matchPath.empty()) {
94         result = child->matches(this);
95     } else {
96         const Breadcrumb top(matchPath.top());
97         assert(child == top.parent);
98         matchPath.pop();
99         result = top.parent->resumeMatchingAt(this, top.position);
100     }
101 
102     if (asyncInProgress()) {
103         // We get here for node N that called goAsync() and then, as the call
104         // stack unwinds, for the nodes higher in the ACL tree that led to N.
105         matchPath.push(Breadcrumb(current, pos));
106     } else {
107         asyncLoc_.clear();
108     }
109 
110     matchLoc_.clear();
111     return result;
112 }
113 
114 bool
goAsync(AsyncState * state)115 ACLChecklist::goAsync(AsyncState *state)
116 {
117     assert(state);
118     assert(!asyncInProgress());
119     assert(matchLoc_.parent);
120 
121     // TODO: add a once-in-a-while WARNING about fast directive using slow ACL?
122     if (!asyncCaller_) {
123         debugs(28, 2, this << " a fast-only directive uses a slow ACL!");
124         return false;
125     }
126 
127     // TODO: add a once-in-a-while WARNING about async loops?
128     if (matchLoc_ == asyncLoc_) {
129         debugs(28, 2, this << " a slow ACL resumes by going async again! (loop #" << asyncLoopDepth_ << ")");
130         // external_acl_type may cause async auth lookup plus its own async check
131         // which has the appearance of a loop. Allow some retries.
132         // TODO: make it configurable and check BH retry attempts vs this check?
133         if (asyncLoopDepth_ > 5)
134             return false;
135     }
136 
137     asyncLoc_ = matchLoc_; // prevent async loops
138     ++asyncLoopDepth_;
139 
140     asyncStage_ = asyncStarting;
141     changeState(state);
142     state->checkForAsync(this); // this is supposed to go async
143 
144     // Did AsyncState object actually go async? If not, tell the caller.
145     if (asyncStage_ != asyncStarting) {
146         assert(asyncStage_ == asyncFailed);
147         asyncStage_ = asyncNone; // sanity restored
148         return false;
149     }
150 
151     // yes, we must pause until the async callback calls resumeNonBlockingCheck
152     asyncStage_ = asyncRunning;
153     return true;
154 }
155 
156 // ACLFilledChecklist overwrites this to unclock something before we
157 // "delete this"
158 void
checkCallback(allow_t answer)159 ACLChecklist::checkCallback(allow_t answer)
160 {
161     ACLCB *callback_;
162     void *cbdata_;
163     debugs(28, 3, "ACLChecklist::checkCallback: " << this << " answer=" << answer);
164 
165     callback_ = callback;
166     callback = NULL;
167 
168     if (cbdataReferenceValidDone(callback_data, &cbdata_))
169         callback_(answer, cbdata_);
170 
171     // not really meaningful just before delete, but here for completeness sake
172     occupied_ = false;
173 
174     delete this;
175 }
176 
ACLChecklist()177 ACLChecklist::ACLChecklist() :
178     accessList (NULL),
179     callback (NULL),
180     callback_data (NULL),
181     asyncCaller_(false),
182     occupied_(false),
183     finished_(false),
184     allow_(ACCESS_DENIED),
185     asyncStage_(asyncNone),
186     state_(NullState::Instance()),
187     asyncLoopDepth_(0)
188 {
189 }
190 
~ACLChecklist()191 ACLChecklist::~ACLChecklist()
192 {
193     assert (!asyncInProgress());
194 
195     changeAcl(nullptr);
196 
197     debugs(28, 4, "ACLChecklist::~ACLChecklist: destroyed " << this);
198 }
199 
200 ACLChecklist::NullState *
Instance()201 ACLChecklist::NullState::Instance()
202 {
203     return &_instance;
204 }
205 
206 void
checkForAsync(ACLChecklist *) const207 ACLChecklist::NullState::checkForAsync(ACLChecklist *) const
208 {
209     assert(false); // or the Checklist will never get out of the async state
210 }
211 
212 ACLChecklist::NullState ACLChecklist::NullState::_instance;
213 
214 void
changeState(AsyncState * newState)215 ACLChecklist::changeState (AsyncState *newState)
216 {
217     /* only change from null to active and back again,
218      * not active to active.
219      * relax this once conversion to states is complete
220      * RBC 02 2003
221      */
222     assert (state_ == NullState::Instance() || newState == NullState::Instance());
223     state_ = newState;
224 }
225 
226 ACLChecklist::AsyncState *
asyncState() const227 ACLChecklist::asyncState() const
228 {
229     return state_;
230 }
231 
232 /**
233  * Kick off a non-blocking (slow) ACL access list test
234  *
235  * NP: this should probably be made Async now.
236  */
237 void
nonBlockingCheck(ACLCB * callback_,void * callback_data_)238 ACLChecklist::nonBlockingCheck(ACLCB * callback_, void *callback_data_)
239 {
240     preCheck("slow rules");
241     callback = callback_;
242     callback_data = cbdataReference(callback_data_);
243     asyncCaller_ = true;
244 
245     /** The ACL List should NEVER be NULL when calling this method.
246      * Always caller should check for NULL and handle appropriate to its needs first.
247      * We cannot select a sensible default for all callers here. */
248     if (accessList == NULL) {
249         debugs(28, DBG_CRITICAL, "SECURITY ERROR: ACL " << this << " checked with nothing to match against!!");
250         checkCallback(ACCESS_DUNNO);
251         return;
252     }
253 
254     if (prepNonBlocking()) {
255         matchAndFinish(); // calls markFinished() on success
256         if (!asyncInProgress())
257             completeNonBlocking();
258     } // else checkCallback() has been called
259 }
260 
261 void
resumeNonBlockingCheck(AsyncState * state)262 ACLChecklist::resumeNonBlockingCheck(AsyncState *state)
263 {
264     assert(asyncState() == state);
265     changeState(NullState::Instance());
266 
267     if (asyncStage_ == asyncStarting) { // oops, we did not really go async
268         asyncStage_ = asyncFailed; // goAsync() checks for that
269         // Do not fall through to resume checks from the async callback. Let
270         // the still-pending(!) goAsync() notice and notify its caller instead.
271         return;
272     }
273     assert(asyncStage_ == asyncRunning);
274     asyncStage_ = asyncNone;
275 
276     assert(!matchPath.empty());
277 
278     if (!prepNonBlocking())
279         return; // checkCallback() has been called
280 
281     if (!finished())
282         matchAndFinish();
283 
284     if (asyncInProgress())
285         assert(!matchPath.empty()); // we have breadcrumbs to resume matching
286     else
287         completeNonBlocking();
288 }
289 
290 /// performs (or resumes) an ACL tree match and, if successful, sets the action
291 void
matchAndFinish()292 ACLChecklist::matchAndFinish()
293 {
294     bool result = false;
295     if (matchPath.empty()) {
296         result = accessList->matches(this);
297     } else {
298         const Breadcrumb top(matchPath.top());
299         matchPath.pop();
300         result = top.parent->resumeMatchingAt(this, top.position);
301     }
302 
303     if (result) // the entire tree matched
304         markFinished(accessList->winningAction(), "match");
305 }
306 
307 allow_t const &
fastCheck(const Acl::Tree * list)308 ACLChecklist::fastCheck(const Acl::Tree * list)
309 {
310     PROF_start(aclCheckFast);
311 
312     preCheck("fast ACLs");
313     asyncCaller_ = false;
314 
315     // Concurrent checks are not supported, but sequential checks are, and they
316     // may use a mixture of fastCheck(void) and fastCheck(list) calls.
317     const Acl::Tree * const savedList = changeAcl(list);
318 
319     // assume DENY/ALLOW on mis/matches due to action-free accessList
320     // matchAndFinish() takes care of the ALLOW case
321     if (accessList && cbdataReferenceValid(accessList))
322         matchAndFinish(); // calls markFinished() on success
323     if (!finished())
324         markFinished(ACCESS_DENIED, "ACLs failed to match");
325 
326     changeAcl(savedList);
327     occupied_ = false;
328     PROF_stop(aclCheckFast);
329     return currentAnswer();
330 }
331 
332 /* Warning: do not cbdata lock this here - it
333  * may be static or on the stack
334  */
335 allow_t const &
fastCheck()336 ACLChecklist::fastCheck()
337 {
338     PROF_start(aclCheckFast);
339 
340     preCheck("fast rules");
341     asyncCaller_ = false;
342 
343     debugs(28, 5, "aclCheckFast: list: " << accessList);
344     const Acl::Tree *acl = cbdataReference(accessList);
345     if (acl != NULL && cbdataReferenceValid(acl)) {
346         matchAndFinish(); // calls markFinished() on success
347 
348         // if finished (on a match or in exceptional cases), stop
349         if (finished()) {
350             cbdataReferenceDone(acl);
351             occupied_ = false;
352             PROF_stop(aclCheckFast);
353             return currentAnswer();
354         }
355 
356         // fall through for mismatch handling
357     }
358 
359     // There were no rules to match or no rules matched
360     calcImplicitAnswer();
361     cbdataReferenceDone(acl);
362     occupied_ = false;
363     PROF_stop(aclCheckFast);
364 
365     return currentAnswer();
366 }
367 
368 /// When no rules matched, the answer is the inversion of the last rule
369 /// action (or ACCESS_DUNNO if the reversal is not possible).
370 void
calcImplicitAnswer()371 ACLChecklist::calcImplicitAnswer()
372 {
373     const allow_t lastAction = (accessList && cbdataReferenceValid(accessList)) ?
374                                accessList->lastAction() : allow_t(ACCESS_DUNNO);
375     allow_t implicitRuleAnswer = ACCESS_DUNNO;
376     if (lastAction == ACCESS_DENIED) // reverse last seen "deny"
377         implicitRuleAnswer = ACCESS_ALLOWED;
378     else if (lastAction == ACCESS_ALLOWED) // reverse last seen "allow"
379         implicitRuleAnswer = ACCESS_DENIED;
380     // else we saw no rules and will respond with ACCESS_DUNNO
381 
382     debugs(28, 3, HERE << this << " NO match found, last action " <<
383            lastAction << " so returning " << implicitRuleAnswer);
384     markFinished(implicitRuleAnswer, "implicit rule won");
385 }
386 
387 bool
callerGone()388 ACLChecklist::callerGone()
389 {
390     return !cbdataReferenceValid(callback_data);
391 }
392 
393 bool
bannedAction(const allow_t & action) const394 ACLChecklist::bannedAction(const allow_t &action) const
395 {
396     const bool found = std::find(bannedActions_.begin(), bannedActions_.end(), action) != bannedActions_.end();
397     debugs(28, 5, "Action '" << action << "/" << action.kind << (found ? "' is " : "' is not") << " banned");
398     return found;
399 }
400 
401 void
banAction(const allow_t & action)402 ACLChecklist::banAction(const allow_t &action)
403 {
404     bannedActions_.push_back(action);
405 }
406 
407