]>
Commit | Line | Data |
---|---|---|
225b7b10 | 1 | /* |
b8ae064d | 2 | * Copyright (C) 1996-2023 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" |
5fb6afec | 13 | #include "acl/FilledChecklist.h" |
6f58d7d7 | 14 | #include "acl/Tree.h" |
675b8408 | 15 | #include "debug/Stream.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; |
25aa6c9a | 63 | answer_.lastCheckedName = lastCheckedName_; |
bf95c10a | 64 | debugs(28, 3, this << " answer " << answer_ << " for " << reason); |
225b7b10 | 65 | } |
66 | ||
e0f7153c | 67 | /// Called first (and once) by all checks to initialize their state |
c35e260d | 68 | void |
e0f7153c | 69 | ACLChecklist::preCheck(const char *what) |
c35e260d | 70 | { |
bf95c10a | 71 | debugs(28, 3, this << " checking " << what); |
7c469a68 AR |
72 | |
73 | // concurrent checks using the same Checklist are not supported | |
74 | assert(!occupied_); | |
75 | occupied_ = true; | |
1707b9ad | 76 | asyncLoopDepth_ = 0; |
7c469a68 | 77 | |
25aa6c9a | 78 | lastCheckedName_.reset(); |
e0f7153c | 79 | finished_ = false; |
c35e260d | 80 | } |
81 | ||
6f58d7d7 | 82 | bool |
922513e5 | 83 | ACLChecklist::matchChild(const Acl::InnerNode *current, Acl::Nodes::const_iterator pos, const Acl::Node *child) |
225b7b10 | 84 | { |
6f58d7d7 AR |
85 | assert(current && child); |
86 | ||
115b045f | 87 | // Remember the current tree location to prevent "async loop" cases where |
6f58d7d7 AR |
88 | // the same child node wants to go async more than once. |
89 | matchLoc_ = Breadcrumb(current, pos); | |
1707b9ad | 90 | asyncLoopDepth_ = 0; |
6f58d7d7 AR |
91 | |
92 | // if there are any breadcrumbs left, then follow them on the way down | |
93 | bool result = false; | |
94 | if (matchPath.empty()) { | |
95 | result = child->matches(this); | |
96 | } else { | |
97 | const Breadcrumb top(matchPath.top()); | |
98 | assert(child == top.parent); | |
99 | matchPath.pop(); | |
100 | result = top.parent->resumeMatchingAt(this, top.position); | |
101 | } | |
102 | ||
103 | if (asyncInProgress()) { | |
104 | // We get here for node N that called goAsync() and then, as the call | |
105 | // stack unwinds, for the nodes higher in the ACL tree that led to N. | |
106 | matchPath.push(Breadcrumb(current, pos)); | |
107 | } else { | |
108 | asyncLoc_.clear(); | |
109 | } | |
e0f7153c | 110 | |
6f58d7d7 AR |
111 | matchLoc_.clear(); |
112 | return result; | |
225b7b10 | 113 | } |
114 | ||
6f58d7d7 | 115 | bool |
922513e5 | 116 | ACLChecklist::goAsync(AsyncStarter starter, const Acl::Node &acl) |
225b7b10 | 117 | { |
6f58d7d7 AR |
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; | |
5fb6afec | 141 | starter(*Filled(this), acl); // this is supposed to go async |
6f58d7d7 | 142 | |
5fb6afec | 143 | // Did starter() actually go async? If not, tell the caller. |
6f58d7d7 AR |
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 |
25aa6c9a | 158 | ACLChecklist::checkCallback(const 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 | 184 | asyncStage_(asyncNone), |
f53969cc | 185 | asyncLoopDepth_(0) |
225b7b10 | 186 | { |
225b7b10 | 187 | } |
188 | ||
189 | ACLChecklist::~ACLChecklist() | |
190 | { | |
191 | assert (!asyncInProgress()); | |
62e76326 | 192 | |
3d29e126 | 193 | changeAcl(nullptr); |
62e76326 | 194 | |
bf8fe701 | 195 | debugs(28, 4, "ACLChecklist::~ACLChecklist: destroyed " << this); |
225b7b10 | 196 | } |
197 | ||
b50e327b AJ |
198 | /** |
199 | * Kick off a non-blocking (slow) ACL access list test | |
200 | * | |
201 | * NP: this should probably be made Async now. | |
202 | */ | |
225b7b10 | 203 | void |
2efeb0b7 | 204 | ACLChecklist::nonBlockingCheck(ACLCB * callback_, void *callback_data_) |
225b7b10 | 205 | { |
e0f7153c | 206 | preCheck("slow rules"); |
225b7b10 | 207 | callback = callback_; |
208 | callback_data = cbdataReference(callback_data_); | |
6f58d7d7 AR |
209 | asyncCaller_ = true; |
210 | ||
922513e5 | 211 | /** The ACL list should NEVER be NULL when calling this method. |
6f58d7d7 AR |
212 | * Always caller should check for NULL and handle appropriate to its needs first. |
213 | * We cannot select a sensible default for all callers here. */ | |
aee3523a | 214 | if (accessList == nullptr) { |
6f58d7d7 AR |
215 | debugs(28, DBG_CRITICAL, "SECURITY ERROR: ACL " << this << " checked with nothing to match against!!"); |
216 | checkCallback(ACCESS_DUNNO); | |
217 | return; | |
218 | } | |
219 | ||
220 | if (prepNonBlocking()) { | |
221 | matchAndFinish(); // calls markFinished() on success | |
222 | if (!asyncInProgress()) | |
223 | completeNonBlocking(); | |
224 | } // else checkCallback() has been called | |
225 | } | |
226 | ||
227 | void | |
5fb6afec | 228 | ACLChecklist::resumeNonBlockingCheck() |
6f58d7d7 | 229 | { |
6f58d7d7 AR |
230 | if (asyncStage_ == asyncStarting) { // oops, we did not really go async |
231 | asyncStage_ = asyncFailed; // goAsync() checks for that | |
232 | // Do not fall through to resume checks from the async callback. Let | |
233 | // the still-pending(!) goAsync() notice and notify its caller instead. | |
234 | return; | |
235 | } | |
236 | assert(asyncStage_ == asyncRunning); | |
237 | asyncStage_ = asyncNone; | |
238 | ||
239 | assert(!matchPath.empty()); | |
240 | ||
241 | if (!prepNonBlocking()) | |
242 | return; // checkCallback() has been called | |
243 | ||
ef689e2b AR |
244 | if (!finished()) |
245 | matchAndFinish(); | |
6f58d7d7 AR |
246 | |
247 | if (asyncInProgress()) | |
248 | assert(!matchPath.empty()); // we have breadcrumbs to resume matching | |
249 | else | |
250 | completeNonBlocking(); | |
251 | } | |
252 | ||
253 | /// performs (or resumes) an ACL tree match and, if successful, sets the action | |
254 | void | |
e936c41c AR |
255 | ACLChecklist::matchAndFinish() |
256 | { | |
6f58d7d7 AR |
257 | bool result = false; |
258 | if (matchPath.empty()) { | |
259 | result = accessList->matches(this); | |
260 | } else { | |
261 | const Breadcrumb top(matchPath.top()); | |
262 | matchPath.pop(); | |
263 | result = top.parent->resumeMatchingAt(this, top.position); | |
264 | } | |
e936c41c | 265 | |
6f58d7d7 AR |
266 | if (result) // the entire tree matched |
267 | markFinished(accessList->winningAction(), "match"); | |
2efeb0b7 AJ |
268 | } |
269 | ||
329c128c | 270 | Acl::Answer const & |
6f58d7d7 | 271 | ACLChecklist::fastCheck(const Acl::Tree * list) |
2efeb0b7 | 272 | { |
e0f7153c | 273 | preCheck("fast ACLs"); |
6f58d7d7 AR |
274 | asyncCaller_ = false; |
275 | ||
7c469a68 AR |
276 | // Concurrent checks are not supported, but sequential checks are, and they |
277 | // may use a mixture of fastCheck(void) and fastCheck(list) calls. | |
3d29e126 | 278 | const Acl::Tree * const savedList = changeAcl(list); |
e0f7153c | 279 | |
b5e48e70 | 280 | // assume DENY/ALLOW on mis/matches due to action-free accessList |
6f58d7d7 | 281 | // matchAndFinish() takes care of the ALLOW case |
6f58d7d7 AR |
282 | if (accessList && cbdataReferenceValid(accessList)) |
283 | matchAndFinish(); // calls markFinished() on success | |
284 | if (!finished()) | |
285 | markFinished(ACCESS_DENIED, "ACLs failed to match"); | |
286 | ||
3d29e126 | 287 | changeAcl(savedList); |
7c469a68 | 288 | occupied_ = false; |
2efeb0b7 | 289 | return currentAnswer(); |
225b7b10 | 290 | } |
291 | ||
b448c119 | 292 | /* Warning: do not cbdata lock this here - it |
293 | * may be static or on the stack | |
294 | */ | |
329c128c | 295 | Acl::Answer const & |
b448c119 | 296 | ACLChecklist::fastCheck() |
297 | { | |
e0f7153c | 298 | preCheck("fast rules"); |
6f58d7d7 | 299 | asyncCaller_ = false; |
e0f7153c | 300 | |
bf8fe701 | 301 | debugs(28, 5, "aclCheckFast: list: " << accessList); |
6f58d7d7 | 302 | const Acl::Tree *acl = cbdataReference(accessList); |
aee3523a | 303 | if (acl != nullptr && cbdataReferenceValid(acl)) { |
6f58d7d7 | 304 | matchAndFinish(); // calls markFinished() on success |
e0f7153c AR |
305 | |
306 | // if finished (on a match or in exceptional cases), stop | |
2efeb0b7 | 307 | if (finished()) { |
bea5dacc | 308 | cbdataReferenceDone(acl); |
7c469a68 | 309 | occupied_ = false; |
2efeb0b7 | 310 | return currentAnswer(); |
b448c119 | 311 | } |
312 | ||
6f58d7d7 | 313 | // fall through for mismatch handling |
b448c119 | 314 | } |
315 | ||
e0f7153c | 316 | // There were no rules to match or no rules matched |
6f58d7d7 AR |
317 | calcImplicitAnswer(); |
318 | cbdataReferenceDone(acl); | |
7c469a68 | 319 | occupied_ = false; |
2efeb0b7 AJ |
320 | |
321 | return currentAnswer(); | |
b448c119 | 322 | } |
323 | ||
b5e48e70 AR |
324 | /// When no rules matched, the answer is the inversion of the last rule |
325 | /// action (or ACCESS_DUNNO if the reversal is not possible). | |
e0f7153c | 326 | void |
6f58d7d7 | 327 | ACLChecklist::calcImplicitAnswer() |
e0f7153c | 328 | { |
329c128c | 329 | const auto lastAction = (accessList && cbdataReferenceValid(accessList)) ? |
a70e75b7 | 330 | accessList->lastAction() : Acl::Answer(ACCESS_DUNNO); |
329c128c | 331 | auto implicitRuleAnswer = Acl::Answer(ACCESS_DUNNO); |
b5e48e70 | 332 | if (lastAction == ACCESS_DENIED) // reverse last seen "deny" |
329c128c | 333 | implicitRuleAnswer = Acl::Answer(ACCESS_ALLOWED); |
b5e48e70 | 334 | else if (lastAction == ACCESS_ALLOWED) // reverse last seen "allow" |
329c128c | 335 | implicitRuleAnswer = Acl::Answer(ACCESS_DENIED); |
e0f7153c AR |
336 | // else we saw no rules and will respond with ACCESS_DUNNO |
337 | ||
1c2b4465 | 338 | implicitRuleAnswer.implicit = true; |
bf95c10a | 339 | debugs(28, 3, this << " NO match found, last action " << |
b5e48e70 | 340 | lastAction << " so returning " << implicitRuleAnswer); |
e0f7153c AR |
341 | markFinished(implicitRuleAnswer, "implicit rule won"); |
342 | } | |
b448c119 | 343 | |
351fe86d AR |
344 | bool |
345 | ACLChecklist::callerGone() | |
b448c119 | 346 | { |
351fe86d | 347 | return !cbdataReferenceValid(callback_data); |
b448c119 | 348 | } |
f53969cc | 349 | |
640fe8fb | 350 | bool |
329c128c | 351 | ACLChecklist::bannedAction(const Acl::Answer &action) const |
640fe8fb CT |
352 | { |
353 | const bool found = std::find(bannedActions_.begin(), bannedActions_.end(), action) != bannedActions_.end(); | |
0f250ac4 | 354 | debugs(28, 5, "Action '" << action << "/" << action.kind << (found ? "' is " : "' is not") << " banned"); |
640fe8fb CT |
355 | return found; |
356 | } | |
357 | ||
358 | void | |
329c128c | 359 | ACLChecklist::banAction(const Acl::Answer &action) |
640fe8fb CT |
360 | { |
361 | bannedActions_.push_back(action); | |
362 | } | |
363 |