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