]>
Commit | Line | Data |
---|---|---|
225b7b10 | 1 | /* |
f70aedc4 | 2 | * Copyright (C) 1996-2021 The Squid Software Foundation and contributors |
bbc27441 AJ |
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. | |
225b7b10 | 7 | */ |
8 | ||
bbc27441 AJ |
9 | /* DEBUG: section 28 Access Control */ |
10 | ||
582c2af2 | 11 | #include "squid.h" |
351fe86d | 12 | #include "acl/Checklist.h" |
6f58d7d7 | 13 | #include "acl/Tree.h" |
582c2af2 FC |
14 | #include "Debug.h" |
15 | #include "profiler/Profiler.h" | |
225b7b10 | 16 | |
640fe8fb CT |
17 | #include <algorithm> |
18 | ||
6f58d7d7 AR |
19 | /// common parts of nonBlockingCheck() and resumeNonBlockingCheck() |
20 | bool | |
21 | ACLChecklist::prepNonBlocking() | |
225b7b10 | 22 | { |
6f58d7d7 | 23 | assert(accessList); |
62e76326 | 24 | |
315b856d | 25 | if (callerGone()) { |
e0f7153c | 26 | checkCallback(ACCESS_DUNNO); // the answer does not really matter |
6f58d7d7 | 27 | return false; |
070b2c0c AJ |
28 | } |
29 | ||
e936c41c | 30 | /** \par |
b5e48e70 | 31 | * If the accessList is no longer valid (i.e. its been |
e936c41c AR |
32 | * freed because of a reconfigure), then bail with ACCESS_DUNNO. |
33 | */ | |
62e76326 | 34 | |
e936c41c AR |
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 | ||
ef689e2b | 42 | return true; |
225b7b10 | 43 | } |
44 | ||
45 | void | |
6f58d7d7 | 46 | ACLChecklist::completeNonBlocking() |
225b7b10 | 47 | { |
6f58d7d7 | 48 | assert(!asyncInProgress()); |
225b7b10 | 49 | |
6f58d7d7 AR |
50 | if (!finished()) |
51 | calcImplicitAnswer(); | |
52 | ||
53 | cbdataReferenceDone(accessList); | |
54 | checkCallback(currentAnswer()); | |
225b7b10 | 55 | } |
56 | ||
57 | void | |
329c128c | 58 | ACLChecklist::markFinished(const Acl::Answer &finalAnswer, const char *reason) |
225b7b10 | 59 | { |
3841dd46 | 60 | assert (!finished() && !asyncInProgress()); |
225b7b10 | 61 | finished_ = true; |
329c128c | 62 | answer_ = finalAnswer; |
63 | debugs(28, 3, HERE << this << " answer " << answer_ << " for " << reason); | |
225b7b10 | 64 | } |
65 | ||
e0f7153c | 66 | /// Called first (and once) by all checks to initialize their state |
c35e260d | 67 | void |
e0f7153c | 68 | ACLChecklist::preCheck(const char *what) |
c35e260d | 69 | { |
e0f7153c | 70 | debugs(28, 3, HERE << this << " checking " << what); |
7c469a68 AR |
71 | |
72 | // concurrent checks using the same Checklist are not supported | |
73 | assert(!occupied_); | |
74 | occupied_ = true; | |
1707b9ad | 75 | asyncLoopDepth_ = 0; |
7c469a68 | 76 | |
6f58d7d7 | 77 | AclMatchedName = NULL; |
e0f7153c | 78 | finished_ = false; |
c35e260d | 79 | } |
80 | ||
6f58d7d7 AR |
81 | bool |
82 | ACLChecklist::matchChild(const Acl::InnerNode *current, Acl::Nodes::const_iterator pos, const ACL *child) | |
225b7b10 | 83 | { |
6f58d7d7 AR |
84 | assert(current && child); |
85 | ||
115b045f | 86 | // Remember the current tree location to prevent "async loop" cases where |
6f58d7d7 AR |
87 | // the same child node wants to go async more than once. |
88 | matchLoc_ = Breadcrumb(current, pos); | |
1707b9ad | 89 | asyncLoopDepth_ = 0; |
6f58d7d7 AR |
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 | } | |
e0f7153c | 109 | |
6f58d7d7 AR |
110 | matchLoc_.clear(); |
111 | return result; | |
225b7b10 | 112 | } |
113 | ||
6f58d7d7 AR |
114 | bool |
115 | ACLChecklist::goAsync(AsyncState *state) | |
225b7b10 | 116 | { |
6f58d7d7 AR |
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_) { | |
1707b9ad AJ |
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; | |
6f58d7d7 AR |
135 | } |
136 | ||
137 | asyncLoc_ = matchLoc_; // prevent async loops | |
1707b9ad | 138 | ++asyncLoopDepth_; |
6f58d7d7 AR |
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 | } | |
e936c41c | 150 | |
6f58d7d7 AR |
151 | // yes, we must pause until the async callback calls resumeNonBlockingCheck |
152 | asyncStage_ = asyncRunning; | |
153 | return true; | |
225b7b10 | 154 | } |
155 | ||
351fe86d AR |
156 | // ACLFilledChecklist overwrites this to unclock something before we |
157 | // "delete this" | |
225b7b10 | 158 | void |
329c128c | 159 | ACLChecklist::checkCallback(Acl::Answer answer) |
225b7b10 | 160 | { |
2efeb0b7 | 161 | ACLCB *callback_; |
225b7b10 | 162 | void *cbdata_; |
bf8fe701 | 163 | debugs(28, 3, "ACLChecklist::checkCallback: " << this << " answer=" << answer); |
164 | ||
225b7b10 | 165 | callback_ = callback; |
166 | callback = NULL; | |
62e76326 | 167 | |
225b7b10 | 168 | if (cbdataReferenceValidDone(callback_data, &cbdata_)) |
62e76326 | 169 | callback_(answer, cbdata_); |
170 | ||
7c469a68 AR |
171 | // not really meaningful just before delete, but here for completeness sake |
172 | occupied_ = false; | |
173 | ||
00d77d6b | 174 | delete this; |
225b7b10 | 175 | } |
62e76326 | 176 | |
cc192b50 | 177 | ACLChecklist::ACLChecklist() : |
f53969cc SM |
178 | accessList (NULL), |
179 | callback (NULL), | |
180 | callback_data (NULL), | |
181 | asyncCaller_(false), | |
182 | occupied_(false), | |
183 | finished_(false), | |
329c128c | 184 | answer_(ACCESS_DENIED), |
f53969cc SM |
185 | asyncStage_(asyncNone), |
186 | state_(NullState::Instance()), | |
187 | asyncLoopDepth_(0) | |
225b7b10 | 188 | { |
225b7b10 | 189 | } |
190 | ||
191 | ACLChecklist::~ACLChecklist() | |
192 | { | |
193 | assert (!asyncInProgress()); | |
62e76326 | 194 | |
3d29e126 | 195 | changeAcl(nullptr); |
62e76326 | 196 | |
bf8fe701 | 197 | debugs(28, 4, "ACLChecklist::~ACLChecklist: destroyed " << this); |
225b7b10 | 198 | } |
199 | ||
225b7b10 | 200 | ACLChecklist::NullState * |
201 | ACLChecklist::NullState::Instance() | |
202 | { | |
203 | return &_instance; | |
204 | } | |
205 | ||
206 | void | |
207 | ACLChecklist::NullState::checkForAsync(ACLChecklist *) const | |
6f58d7d7 AR |
208 | { |
209 | assert(false); // or the Checklist will never get out of the async state | |
210 | } | |
225b7b10 | 211 | |
212 | ACLChecklist::NullState ACLChecklist::NullState::_instance; | |
213 | ||
214 | void | |
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 * | |
227 | ACLChecklist::asyncState() const | |
228 | { | |
229 | return state_; | |
230 | } | |
231 | ||
b50e327b AJ |
232 | /** |
233 | * Kick off a non-blocking (slow) ACL access list test | |
234 | * | |
235 | * NP: this should probably be made Async now. | |
236 | */ | |
225b7b10 | 237 | void |
2efeb0b7 | 238 | ACLChecklist::nonBlockingCheck(ACLCB * callback_, void *callback_data_) |
225b7b10 | 239 | { |
e0f7153c | 240 | preCheck("slow rules"); |
225b7b10 | 241 | callback = callback_; |
242 | callback_data = cbdataReference(callback_data_); | |
6f58d7d7 AR |
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 | |
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 | ||
ef689e2b AR |
281 | if (!finished()) |
282 | matchAndFinish(); | |
6f58d7d7 AR |
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 | |
e936c41c AR |
292 | ACLChecklist::matchAndFinish() |
293 | { | |
6f58d7d7 AR |
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 | } | |
e936c41c | 302 | |
6f58d7d7 AR |
303 | if (result) // the entire tree matched |
304 | markFinished(accessList->winningAction(), "match"); | |
2efeb0b7 AJ |
305 | } |
306 | ||
329c128c | 307 | Acl::Answer const & |
6f58d7d7 | 308 | ACLChecklist::fastCheck(const Acl::Tree * list) |
2efeb0b7 AJ |
309 | { |
310 | PROF_start(aclCheckFast); | |
e0f7153c AR |
311 | |
312 | preCheck("fast ACLs"); | |
6f58d7d7 AR |
313 | asyncCaller_ = false; |
314 | ||
7c469a68 AR |
315 | // Concurrent checks are not supported, but sequential checks are, and they |
316 | // may use a mixture of fastCheck(void) and fastCheck(list) calls. | |
3d29e126 | 317 | const Acl::Tree * const savedList = changeAcl(list); |
e0f7153c | 318 | |
b5e48e70 | 319 | // assume DENY/ALLOW on mis/matches due to action-free accessList |
6f58d7d7 | 320 | // matchAndFinish() takes care of the ALLOW case |
6f58d7d7 AR |
321 | if (accessList && cbdataReferenceValid(accessList)) |
322 | matchAndFinish(); // calls markFinished() on success | |
323 | if (!finished()) | |
324 | markFinished(ACCESS_DENIED, "ACLs failed to match"); | |
325 | ||
3d29e126 | 326 | changeAcl(savedList); |
7c469a68 | 327 | occupied_ = false; |
2efeb0b7 AJ |
328 | PROF_stop(aclCheckFast); |
329 | return currentAnswer(); | |
225b7b10 | 330 | } |
331 | ||
b448c119 | 332 | /* Warning: do not cbdata lock this here - it |
333 | * may be static or on the stack | |
334 | */ | |
329c128c | 335 | Acl::Answer const & |
b448c119 | 336 | ACLChecklist::fastCheck() |
337 | { | |
338 | PROF_start(aclCheckFast); | |
2efeb0b7 | 339 | |
e0f7153c | 340 | preCheck("fast rules"); |
6f58d7d7 | 341 | asyncCaller_ = false; |
e0f7153c | 342 | |
bf8fe701 | 343 | debugs(28, 5, "aclCheckFast: list: " << accessList); |
6f58d7d7 AR |
344 | const Acl::Tree *acl = cbdataReference(accessList); |
345 | if (acl != NULL && cbdataReferenceValid(acl)) { | |
346 | matchAndFinish(); // calls markFinished() on success | |
e0f7153c AR |
347 | |
348 | // if finished (on a match or in exceptional cases), stop | |
2efeb0b7 | 349 | if (finished()) { |
bea5dacc | 350 | cbdataReferenceDone(acl); |
7c469a68 | 351 | occupied_ = false; |
e0f7153c | 352 | PROF_stop(aclCheckFast); |
2efeb0b7 | 353 | return currentAnswer(); |
b448c119 | 354 | } |
355 | ||
6f58d7d7 | 356 | // fall through for mismatch handling |
b448c119 | 357 | } |
358 | ||
e0f7153c | 359 | // There were no rules to match or no rules matched |
6f58d7d7 AR |
360 | calcImplicitAnswer(); |
361 | cbdataReferenceDone(acl); | |
7c469a68 | 362 | occupied_ = false; |
b448c119 | 363 | PROF_stop(aclCheckFast); |
2efeb0b7 AJ |
364 | |
365 | return currentAnswer(); | |
b448c119 | 366 | } |
367 | ||
b5e48e70 AR |
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). | |
e0f7153c | 370 | void |
6f58d7d7 | 371 | ACLChecklist::calcImplicitAnswer() |
e0f7153c | 372 | { |
329c128c | 373 | const auto lastAction = (accessList && cbdataReferenceValid(accessList)) ? |
a70e75b7 | 374 | accessList->lastAction() : Acl::Answer(ACCESS_DUNNO); |
329c128c | 375 | auto implicitRuleAnswer = Acl::Answer(ACCESS_DUNNO); |
b5e48e70 | 376 | if (lastAction == ACCESS_DENIED) // reverse last seen "deny" |
329c128c | 377 | implicitRuleAnswer = Acl::Answer(ACCESS_ALLOWED); |
b5e48e70 | 378 | else if (lastAction == ACCESS_ALLOWED) // reverse last seen "allow" |
329c128c | 379 | implicitRuleAnswer = Acl::Answer(ACCESS_DENIED); |
e0f7153c AR |
380 | // else we saw no rules and will respond with ACCESS_DUNNO |
381 | ||
1c2b4465 | 382 | implicitRuleAnswer.implicit = true; |
e0f7153c | 383 | debugs(28, 3, HERE << this << " NO match found, last action " << |
b5e48e70 | 384 | lastAction << " so returning " << implicitRuleAnswer); |
e0f7153c AR |
385 | markFinished(implicitRuleAnswer, "implicit rule won"); |
386 | } | |
b448c119 | 387 | |
351fe86d AR |
388 | bool |
389 | ACLChecklist::callerGone() | |
b448c119 | 390 | { |
351fe86d | 391 | return !cbdataReferenceValid(callback_data); |
b448c119 | 392 | } |
f53969cc | 393 | |
640fe8fb | 394 | bool |
329c128c | 395 | ACLChecklist::bannedAction(const Acl::Answer &action) const |
640fe8fb CT |
396 | { |
397 | const bool found = std::find(bannedActions_.begin(), bannedActions_.end(), action) != bannedActions_.end(); | |
0f250ac4 | 398 | debugs(28, 5, "Action '" << action << "/" << action.kind << (found ? "' is " : "' is not") << " banned"); |
640fe8fb CT |
399 | return found; |
400 | } | |
401 | ||
402 | void | |
329c128c | 403 | ACLChecklist::banAction(const Acl::Answer &action) |
640fe8fb CT |
404 | { |
405 | bannedActions_.push_back(action); | |
406 | } | |
407 |