/*
- * $Id$
+ * Copyright (C) 1996-2017 The Squid Software Foundation and contributors
*
- * DEBUG: section 28 Access Control
- * AUTHOR: Duane Wessels
- *
- * SQUID Web Proxy Cache http://www.squid-cache.org/
- * ----------------------------------------------------------
- *
- * Squid is the result of efforts by numerous individuals from
- * the Internet community; see the CONTRIBUTORS file for full
- * details. Many organizations have provided support for Squid's
- * development; see the SPONSORS file for full details. Squid is
- * Copyrighted (C) 2001 by the Regents of the University of
- * California; see the COPYRIGHT file for full details. Squid
- * incorporates software developed and/or copyrighted by other
- * sources; see the CREDITS file for full details.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA.
- *
- * Copyright (c) 2003, Robert Collins <robertc@squid-cache.org>
+ * Squid software is distributed under GPLv2+ license and includes
+ * contributions from numerous individuals and organizations.
+ * Please see the COPYING and CONTRIBUTORS files for details.
*/
+/* DEBUG: section 28 Access Control */
+
#include "squid.h"
#include "acl/Checklist.h"
+#include "acl/Tree.h"
+#include "Debug.h"
+#include "profiler/Profiler.h"
-allow_t const &
-ACLChecklist::currentAnswer() const
-{
- return allow_;
-}
-
-void
-ACLChecklist::currentAnswer(allow_t const newAnswer)
-{
- allow_ = newAnswer;
-}
+#include <algorithm>
-void
-ACLChecklist::check()
+/// common parts of nonBlockingCheck() and resumeNonBlockingCheck()
+bool
+ACLChecklist::prepNonBlocking()
{
- if (checking())
- return;
-
- /** Deny if no rules present. */
- currentAnswer(ACCESS_DENIED);
+ assert(accessList);
if (callerGone()) {
- checkCallback(currentAnswer());
- return;
+ checkCallback(ACCESS_DUNNO); // the answer does not really matter
+ return false;
}
- /** The ACL List should NEVER be NULL when calling this method.
- * Always caller should check for NULL and handle appropriate to its needs first.
- * We cannot select a sensible default for all callers here. */
- if (accessList == NULL) {
- debugs(28, DBG_CRITICAL, "SECURITY ERROR: ACL " << this << " checked with nothing to match against!!");
- currentAnswer(ACCESS_DENIED);
- checkCallback(currentAnswer());
- return;
- }
-
- /* NOTE: This holds a cbdata reference to the current access_list
- * entry, not the whole list.
+ /** \par
+ * If the accessList is no longer valid (i.e. its been
+ * freed because of a reconfigure), then bail with ACCESS_DUNNO.
*/
- while (accessList != NULL) {
- /** \par
- * If the _acl_access is no longer valid (i.e. its been
- * freed because of a reconfigure), then bail on this
- * access check. For now, return ACCESS_DENIED.
- */
-
- if (!cbdataReferenceValid(accessList)) {
- cbdataReferenceDone(accessList);
- debugs(28, 4, "ACLChecklist::check: " << this << " accessList is invalid");
- continue;
- }
- checking (true);
- checkAccessList();
- checking (false);
-
- if (asyncInProgress()) {
- return;
- }
-
- if (finished()) {
- /** \par
- * Either the request is allowed, denied, requires authentication.
- */
- debugs(28, 3, "ACLChecklist::check: " << this << " match found, calling back with " << currentAnswer());
- cbdataReferenceDone(accessList); /* A */
- checkCallback(currentAnswer());
- /* From here on in, this may be invalid */
- return;
- }
-
- /*
- * Reference the next access entry
- */
- const acl_access *A = accessList;
-
- assert (A);
-
- accessList = cbdataReference(A->next);
-
- cbdataReferenceDone(A);
+ if (!cbdataReferenceValid(accessList)) {
+ cbdataReferenceDone(accessList);
+ debugs(28, 4, "ACLChecklist::check: " << this << " accessList is invalid");
+ checkCallback(ACCESS_DUNNO);
+ return false;
}
- /** If dropped off the end of the list return inversion of last line allow/deny action. */
- debugs(28, 3, HERE << this << " NO match found, returning " <<
- (currentAnswer() != ACCESS_DENIED ? ACCESS_DENIED : ACCESS_ALLOWED));
-
- checkCallback(currentAnswer() != ACCESS_DENIED ? ACCESS_DENIED : ACCESS_ALLOWED);
-}
-
-bool
-ACLChecklist::asyncInProgress() const
-{
- return async_;
+ return true;
}
void
-ACLChecklist::asyncInProgress(bool const newAsync)
+ACLChecklist::completeNonBlocking()
{
- assert (!finished() && !(asyncInProgress() && newAsync));
- async_ = newAsync;
- debugs(28, 3, "ACLChecklist::asyncInProgress: " << this <<
- " async set to " << async_);
-}
+ assert(!asyncInProgress());
-bool
-ACLChecklist::finished() const
-{
- return finished_;
+ if (!finished())
+ calcImplicitAnswer();
+
+ cbdataReferenceDone(accessList);
+ checkCallback(currentAnswer());
}
void
-ACLChecklist::markFinished()
+ACLChecklist::markFinished(const allow_t &finalAnswer, const char *reason)
{
assert (!finished() && !asyncInProgress());
finished_ = true;
- debugs(28, 3, "ACLChecklist::markFinished: " << this <<
- " checklist processing finished");
+ allow_ = finalAnswer;
+ debugs(28, 3, HERE << this << " answer " << allow_ << " for " << reason);
}
+/// Called first (and once) by all checks to initialize their state
void
-ACLChecklist::preCheck()
+ACLChecklist::preCheck(const char *what)
{
- debugs(28, 3, "ACLChecklist::preCheck: " << this << " checking '" << accessList->cfgline << "'");
- /* what is our result on a match? */
- currentAnswer(accessList->allow);
+ debugs(28, 3, HERE << this << " checking " << what);
+
+ // concurrent checks using the same Checklist are not supported
+ assert(!occupied_);
+ occupied_ = true;
+ asyncLoopDepth_ = 0;
+
+ AclMatchedName = NULL;
+ finished_ = false;
}
-void
-ACLChecklist::checkAccessList()
+bool
+ACLChecklist::matchChild(const Acl::InnerNode *current, Acl::Nodes::const_iterator pos, const ACL *child)
{
- preCheck();
- /* does the current AND clause match */
- matchAclListSlow(accessList->aclList);
+ assert(current && child);
+
+ // Remember the current tree location to prevent "async loop" cases where
+ // the same child node wants to go async more than once.
+ matchLoc_ = Breadcrumb(current, pos);
+ asyncLoopDepth_ = 0;
+
+ // if there are any breadcrumbs left, then follow them on the way down
+ bool result = false;
+ if (matchPath.empty()) {
+ result = child->matches(this);
+ } else {
+ const Breadcrumb top(matchPath.top());
+ assert(child == top.parent);
+ matchPath.pop();
+ result = top.parent->resumeMatchingAt(this, top.position);
+ }
+
+ if (asyncInProgress()) {
+ // We get here for node N that called goAsync() and then, as the call
+ // stack unwinds, for the nodes higher in the ACL tree that led to N.
+ matchPath.push(Breadcrumb(current, pos));
+ } else {
+ asyncLoc_.clear();
+ }
+
+ matchLoc_.clear();
+ return result;
}
-void
-ACLChecklist::checkForAsync()
+bool
+ACLChecklist::goAsync(AsyncState *state)
{
- asyncState()->checkForAsync(this);
+ assert(state);
+ assert(!asyncInProgress());
+ assert(matchLoc_.parent);
+
+ // TODO: add a once-in-a-while WARNING about fast directive using slow ACL?
+ if (!asyncCaller_) {
+ debugs(28, 2, this << " a fast-only directive uses a slow ACL!");
+ return false;
+ }
+
+ // TODO: add a once-in-a-while WARNING about async loops?
+ if (matchLoc_ == asyncLoc_) {
+ debugs(28, 2, this << " a slow ACL resumes by going async again! (loop #" << asyncLoopDepth_ << ")");
+ // external_acl_type may cause async auth lookup plus its own async check
+ // which has the appearance of a loop. Allow some retries.
+ // TODO: make it configurable and check BH retry attempts vs this check?
+ if (asyncLoopDepth_ > 5)
+ return false;
+ }
+
+ asyncLoc_ = matchLoc_; // prevent async loops
+ ++asyncLoopDepth_;
+
+ asyncStage_ = asyncStarting;
+ changeState(state);
+ state->checkForAsync(this); // this is supposed to go async
+
+ // Did AsyncState object actually go async? If not, tell the caller.
+ if (asyncStage_ != asyncStarting) {
+ assert(asyncStage_ == asyncFailed);
+ asyncStage_ = asyncNone; // sanity restored
+ return false;
+ }
+
+ // yes, we must pause until the async callback calls resumeNonBlockingCheck
+ asyncStage_ = asyncRunning;
+ return true;
}
// ACLFilledChecklist overwrites this to unclock something before we
void
ACLChecklist::checkCallback(allow_t answer)
{
- PF *callback_;
+ ACLCB *callback_;
void *cbdata_;
debugs(28, 3, "ACLChecklist::checkCallback: " << this << " answer=" << answer);
if (cbdataReferenceValidDone(callback_data, &cbdata_))
callback_(answer, cbdata_);
- delete this;
-}
-
-void
-ACLChecklist::matchAclListSlow(const ACLList * list)
-{
- matchAclList(list, false);
-}
-
-void
-ACLChecklist::matchAclList(const ACLList * head, bool const fast)
-{
- PROF_start(aclMatchAclList);
- const ACLList *node = head;
-
- finished_ = false;
-
- while (node) {
- bool nodeMatched = node->matches(this);
-
- if (fast)
- changeState(NullState::Instance());
-
- if (finished()) {
- PROF_stop(aclMatchAclList);
- return;
- }
-
- if (!nodeMatched || state_ != NullState::Instance()) {
- debugs(28, 3, "aclmatchAclList: " << this << " returning false (AND list entry failed to match)");
-
- bool async = state_ != NullState::Instance();
-
- checkForAsync();
-
- bool async_in_progress = asyncInProgress();
- debugs(28, 3, "aclmatchAclList: async=" << (async ? 1 : 0) <<
- " nodeMatched=" << (nodeMatched ? 1 : 0) <<
- " async_in_progress=" << (async_in_progress ? 1 : 0) <<
- " lastACLResult() = " << (lastACLResult() ? 1 : 0) <<
- " finished() = " << finished());
-
- if (finished()) {
- PROF_stop(aclMatchAclList);
- return;
- }
+ // not really meaningful just before delete, but here for completeness sake
+ occupied_ = false;
- if (async && nodeMatched && !asyncInProgress() && lastACLResult()) {
- // async acl, but using cached response, and it was a match
- node = node->next;
- continue;
- }
-
- PROF_stop(aclMatchAclList);
-
- return;
- }
-
- node = node->next;
- }
-
- debugs(28, 3, "aclmatchAclList: " << this << " returning true (AND list satisfied)");
-
- markFinished();
- PROF_stop(aclMatchAclList);
+ delete this;
}
ACLChecklist::ACLChecklist() :
- accessList (NULL),
- callback (NULL),
- callback_data (NULL),
- async_(false),
- finished_(false),
- allow_(ACCESS_DENIED),
- state_(NullState::Instance()),
- lastACLResult_(false)
+ accessList (NULL),
+ callback (NULL),
+ callback_data (NULL),
+ asyncCaller_(false),
+ occupied_(false),
+ finished_(false),
+ allow_(ACCESS_DENIED),
+ asyncStage_(asyncNone),
+ state_(NullState::Instance()),
+ asyncLoopDepth_(0)
{
}
{
assert (!asyncInProgress());
- cbdataReferenceDone(accessList);
+ changeAcl(nullptr);
debugs(28, 4, "ACLChecklist::~ACLChecklist: destroyed " << this);
}
-
-void
-ACLChecklist::AsyncState::changeState (ACLChecklist *checklist, AsyncState *newState) const
-{
- checklist->changeState(newState);
-}
-
ACLChecklist::NullState *
ACLChecklist::NullState::Instance()
{
void
ACLChecklist::NullState::checkForAsync(ACLChecklist *) const
-{}
+{
+ assert(false); // or the Checklist will never get out of the async state
+}
ACLChecklist::NullState ACLChecklist::NullState::_instance;
* NP: this should probably be made Async now.
*/
void
-ACLChecklist::nonBlockingCheck(PF * callback_, void *callback_data_)
+ACLChecklist::nonBlockingCheck(ACLCB * callback_, void *callback_data_)
{
+ preCheck("slow rules");
callback = callback_;
callback_data = cbdataReference(callback_data_);
- check();
+ asyncCaller_ = true;
+
+ /** The ACL List should NEVER be NULL when calling this method.
+ * Always caller should check for NULL and handle appropriate to its needs first.
+ * We cannot select a sensible default for all callers here. */
+ if (accessList == NULL) {
+ debugs(28, DBG_CRITICAL, "SECURITY ERROR: ACL " << this << " checked with nothing to match against!!");
+ checkCallback(ACCESS_DUNNO);
+ return;
+ }
+
+ if (prepNonBlocking()) {
+ matchAndFinish(); // calls markFinished() on success
+ if (!asyncInProgress())
+ completeNonBlocking();
+ } // else checkCallback() has been called
+}
+
+void
+ACLChecklist::resumeNonBlockingCheck(AsyncState *state)
+{
+ assert(asyncState() == state);
+ changeState(NullState::Instance());
+
+ if (asyncStage_ == asyncStarting) { // oops, we did not really go async
+ asyncStage_ = asyncFailed; // goAsync() checks for that
+ // Do not fall through to resume checks from the async callback. Let
+ // the still-pending(!) goAsync() notice and notify its caller instead.
+ return;
+ }
+ assert(asyncStage_ == asyncRunning);
+ asyncStage_ = asyncNone;
+
+ assert(!matchPath.empty());
+
+ if (!prepNonBlocking())
+ return; // checkCallback() has been called
+
+ if (!finished())
+ matchAndFinish();
+
+ if (asyncInProgress())
+ assert(!matchPath.empty()); // we have breadcrumbs to resume matching
+ else
+ completeNonBlocking();
+}
+
+/// performs (or resumes) an ACL tree match and, if successful, sets the action
+void
+ACLChecklist::matchAndFinish()
+{
+ bool result = false;
+ if (matchPath.empty()) {
+ result = accessList->matches(this);
+ } else {
+ const Breadcrumb top(matchPath.top());
+ matchPath.pop();
+ result = top.parent->resumeMatchingAt(this, top.position);
+ }
+
+ if (result) // the entire tree matched
+ markFinished(accessList->winningAction(), "match");
+}
+
+allow_t const &
+ACLChecklist::fastCheck(const Acl::Tree * list)
+{
+ PROF_start(aclCheckFast);
+
+ preCheck("fast ACLs");
+ asyncCaller_ = false;
+
+ // Concurrent checks are not supported, but sequential checks are, and they
+ // may use a mixture of fastCheck(void) and fastCheck(list) calls.
+ const Acl::Tree * const savedList = changeAcl(list);
+
+ // assume DENY/ALLOW on mis/matches due to action-free accessList
+ // matchAndFinish() takes care of the ALLOW case
+ if (accessList && cbdataReferenceValid(accessList))
+ matchAndFinish(); // calls markFinished() on success
+ if (!finished())
+ markFinished(ACCESS_DENIED, "ACLs failed to match");
+
+ changeAcl(savedList);
+ occupied_ = false;
+ PROF_stop(aclCheckFast);
+ return currentAnswer();
}
/* Warning: do not cbdata lock this here - it
* may be static or on the stack
*/
-int
+allow_t const &
ACLChecklist::fastCheck()
{
PROF_start(aclCheckFast);
- currentAnswer(ACCESS_DENIED);
- debugs(28, 5, "aclCheckFast: list: " << accessList);
- while (accessList) {
- preCheck();
- matchAclListFast(accessList->aclList);
+ preCheck("fast rules");
+ asyncCaller_ = false;
+
+ debugs(28, 5, "aclCheckFast: list: " << accessList);
+ const Acl::Tree *acl = cbdataReference(accessList);
+ if (acl != NULL && cbdataReferenceValid(acl)) {
+ matchAndFinish(); // calls markFinished() on success
+ // if finished (on a match or in exceptional cases), stop
if (finished()) {
+ cbdataReferenceDone(acl);
+ occupied_ = false;
PROF_stop(aclCheckFast);
- cbdataReferenceDone(accessList);
- return currentAnswer() == ACCESS_ALLOWED;
+ return currentAnswer();
}
- /*
- * Reference the next access entry
- */
- const acl_access *A = accessList;
-
- assert (A);
-
- accessList = cbdataReference(A->next);
-
- cbdataReferenceDone(A);
+ // fall through for mismatch handling
}
- debugs(28, 5, "aclCheckFast: no matches, returning: " << (currentAnswer() == ACCESS_DENIED));
-
+ // There were no rules to match or no rules matched
+ calcImplicitAnswer();
+ cbdataReferenceDone(acl);
+ occupied_ = false;
PROF_stop(aclCheckFast);
- return currentAnswer() == ACCESS_DENIED;
-}
-
-bool
-ACLChecklist::checking() const
-{
- return checking_;
+ return currentAnswer();
}
+/// When no rules matched, the answer is the inversion of the last rule
+/// action (or ACCESS_DUNNO if the reversal is not possible).
void
-ACLChecklist::checking (bool const newValue)
+ACLChecklist::calcImplicitAnswer()
{
- checking_ = newValue;
+ const allow_t lastAction = (accessList && cbdataReferenceValid(accessList)) ?
+ accessList->lastAction() : allow_t(ACCESS_DUNNO);
+ allow_t implicitRuleAnswer = ACCESS_DUNNO;
+ if (lastAction == ACCESS_DENIED) // reverse last seen "deny"
+ implicitRuleAnswer = ACCESS_ALLOWED;
+ else if (lastAction == ACCESS_ALLOWED) // reverse last seen "allow"
+ implicitRuleAnswer = ACCESS_DENIED;
+ // else we saw no rules and will respond with ACCESS_DUNNO
+
+ debugs(28, 3, HERE << this << " NO match found, last action " <<
+ lastAction << " so returning " << implicitRuleAnswer);
+ markFinished(implicitRuleAnswer, "implicit rule won");
}
bool
}
bool
-ACLChecklist::matchAclListFast(const ACLList * list)
+ACLChecklist::bannedAction(const allow_t &action) const
{
- matchAclList(list, true);
- return finished();
+ const bool found = std::find(bannedActions_.begin(), bannedActions_.end(), action) != bannedActions_.end();
+ debugs(28, 5, "Action '" << action << "/" << action.kind << (found ? "' is " : "' is not") << " banned");
+ return found;
}
+void
+ACLChecklist::banAction(const allow_t &action)
+{
+ bannedActions_.push_back(action);
+}