Blame SOURCES/0089-Ticket-49652-DENY-aci-s-are-not-handled-properly.patch

b663b9
From 5b7d67bdef7810c661ae4ba1fdfa620c86985661 Mon Sep 17 00:00:00 2001
b663b9
From: Mark Reynolds <mreynolds@redhat.com>
b663b9
Date: Fri, 27 Apr 2018 08:34:51 -0400
b663b9
Subject: [PATCH] Ticket 49652 - DENY aci's are not handled properly
b663b9
b663b9
Bug Description:  There are really two issues here.  One, when a resource
b663b9
                  is denied by a DENY aci the cached results for that resource
b663b9
                  are not proprely set, and on the same connection if the same
b663b9
                  operation repeated it will be allowed instead of denied because
b663b9
                  the cache result was not proprely updated.
b663b9
b663b9
                  Two, if there are no ALLOW aci's on a resource, then we don't
b663b9
                  check the deny rules, and resources that are restricted are
b663b9
                  returned to the client.
b663b9
b663b9
Fix Description:  For issue one, when an entry is denied access reset all the
b663b9
                  attributes' cache results to DENIED as it's possible previously
b663b9
                  evaluated aci's granted access to some of these attributes which
b663b9
                  are still present in the acl result cache.
b663b9
b663b9
                  For issue two, if there are no ALLOW aci's on a resource but
b663b9
                  there are DENY aci's, then set the aclpb state flags to
b663b9
                  process DENY aci's
b663b9
b663b9
https://pagure.io/389-ds-base/issue/49652
b663b9
b663b9
Reviewed by: tbordaz & lkrispenz(Thanks!!)
b663b9
b663b9
(cherry picked from commit d77c7f0754f67022b42784c05be8a493a00f2ec5)
b663b9
---
b663b9
 dirsrvtests/tests/suites/acl/acl_deny_test.py | 198 ++++++++++++++++++
b663b9
 ldap/servers/plugins/acl/acl.c                |  24 ++-
b663b9
 2 files changed, 220 insertions(+), 2 deletions(-)
b663b9
 create mode 100644 dirsrvtests/tests/suites/acl/acl_deny_test.py
b663b9
b663b9
diff --git a/dirsrvtests/tests/suites/acl/acl_deny_test.py b/dirsrvtests/tests/suites/acl/acl_deny_test.py
b663b9
new file mode 100644
b663b9
index 000000000..285664150
b663b9
--- /dev/null
b663b9
+++ b/dirsrvtests/tests/suites/acl/acl_deny_test.py
b663b9
@@ -0,0 +1,198 @@
b663b9
+import logging
b663b9
+import pytest
b663b9
+import os
b663b9
+import ldap
b663b9
+import time
b663b9
+from lib389._constants import *
b663b9
+from lib389.topologies import topology_st as topo
b663b9
+from lib389.idm.user import UserAccount, UserAccounts, TEST_USER_PROPERTIES
b663b9
+from lib389.idm.domain import Domain
b663b9
+
b663b9
+DEBUGGING = os.getenv("DEBUGGING", default=False)
b663b9
+if DEBUGGING:
b663b9
+    logging.getLogger(__name__).setLevel(logging.DEBUG)
b663b9
+else:
b663b9
+    logging.getLogger(__name__).setLevel(logging.INFO)
b663b9
+log = logging.getLogger(__name__)
b663b9
+
b663b9
+BIND_DN2 = 'uid=tuser,ou=People,dc=example,dc=com'
b663b9
+BIND_RDN2 = 'tuser'
b663b9
+BIND_DN = 'uid=tuser1,ou=People,dc=example,dc=com'
b663b9
+BIND_RDN = 'tuser1'
b663b9
+SRCH_FILTER = "uid=tuser1"
b663b9
+SRCH_FILTER2 = "uid=tuser"
b663b9
+
b663b9
+aci_list_A = ['(targetattr != "userPassword") (version 3.0; acl "Anonymous access"; allow (read, search, compare)userdn = "ldap:///anyone";)',
b663b9
+              '(targetattr = "*") (version 3.0;acl "allow tuser";allow (all)(userdn = "ldap:///uid=tuser5,ou=People,dc=example,dc=com");)',
b663b9
+              '(targetattr != "uid || mail") (version 3.0; acl "deny-attrs"; deny (all) (userdn = "ldap:///anyone");)',
b663b9
+              '(targetfilter = "(inetUserStatus=1)") ( version 3.0; acl "deny-specific-entry"; deny(all) (userdn = "ldap:///anyone");)']
b663b9
+
b663b9
+aci_list_B = ['(targetattr != "userPassword") (version 3.0; acl "Anonymous access"; allow (read, search, compare)userdn = "ldap:///anyone";)',
b663b9
+              '(targetattr != "uid || mail") (version 3.0; acl "deny-attrs"; deny (all) (userdn = "ldap:///anyone");)',
b663b9
+              '(targetfilter = "(inetUserStatus=1)") ( version 3.0; acl "deny-specific-entry"; deny(all) (userdn = "ldap:///anyone");)']
b663b9
+
b663b9
+
b663b9
+@pytest.fixture(scope="module")
b663b9
+def aci_setup(topo):
b663b9
+    topo.standalone.log.info("Add {}".format(BIND_DN))
b663b9
+    user = UserAccount(topo.standalone, BIND_DN)
b663b9
+    user_props = TEST_USER_PROPERTIES.copy()
b663b9
+    user_props.update({'sn': BIND_RDN,
b663b9
+                       'cn': BIND_RDN,
b663b9
+                       'uid': BIND_RDN,
b663b9
+                       'inetUserStatus': '1',
b663b9
+                       'objectclass': 'extensibleObject',
b663b9
+                       'userpassword': PASSWORD})
b663b9
+    user.create(properties=user_props, basedn=SUFFIX)
b663b9
+
b663b9
+    topo.standalone.log.info("Add {}".format(BIND_DN2))
b663b9
+    user2 = UserAccount(topo.standalone, BIND_DN2)
b663b9
+    user_props = TEST_USER_PROPERTIES.copy()
b663b9
+    user_props.update({'sn': BIND_RDN2,
b663b9
+                       'cn': BIND_RDN2,
b663b9
+                       'uid': BIND_RDN2,
b663b9
+                       'userpassword': PASSWORD})
b663b9
+    user2.create(properties=user_props, basedn=SUFFIX)
b663b9
+
b663b9
+
b663b9
+def test_multi_deny_aci(topo, aci_setup):
b663b9
+    """Test that mutliple deny rules work, and that they the cache properly
b663b9
+    stores the result
b663b9
+
b663b9
+    :id: 294c366d-850e-459e-b5a0-3cc828ec3aca
b663b9
+    :setup: Standalone Instance
b663b9
+    :steps:
b663b9
+        1. Add aci_list_A aci's and verify two searches on the same connection
b663b9
+           behave the same
b663b9
+        2. Add aci_list_B aci's and verify search fails as expected
b663b9
+    :expectedresults:
b663b9
+        1. Both searches do not return any entries
b663b9
+        2. Seaches do not return any entries
b663b9
+    """
b663b9
+
b663b9
+    if DEBUGGING:
b663b9
+        # Maybe add aci logging?
b663b9
+        pass
b663b9
+
b663b9
+    suffix = Domain(topo.standalone, DEFAULT_SUFFIX)
b663b9
+
b663b9
+    for run in range(2):
b663b9
+        topo.standalone.log.info("Pass " + str(run + 1))
b663b9
+
b663b9
+        # Test ACI List A
b663b9
+        topo.standalone.log.info("Testing two searches behave the same...")
b663b9
+        topo.standalone.simple_bind_s(DN_DM, PASSWORD)
b663b9
+        suffix.set('aci', aci_list_A, ldap.MOD_REPLACE)
b663b9
+        time.sleep(1)
b663b9
+
b663b9
+        topo.standalone.simple_bind_s(BIND_DN, PASSWORD)
b663b9
+        entries = topo.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, SRCH_FILTER)
b663b9
+        if entries and entries[0]:
b663b9
+            topo.standalone.log.fatal("Incorrectly got an entry returned from search 1")
b663b9
+            assert False
b663b9
+
b663b9
+        entries = topo.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, SRCH_FILTER)
b663b9
+        if entries and entries[0]:
b663b9
+            topo.standalone.log.fatal("Incorrectly got an entry returned from search 2")
b663b9
+            assert False
b663b9
+
b663b9
+        entries = topo.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, SRCH_FILTER2)
b663b9
+        if entries is None or len(entries) == 0:
b663b9
+            topo.standalone.log.fatal("Failed to get entry as good user")
b663b9
+            assert False
b663b9
+
b663b9
+        entries = topo.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, SRCH_FILTER2)
b663b9
+        if entries is None or len(entries) == 0:
b663b9
+            topo.standalone.log.fatal("Failed to get entry as good user")
b663b9
+            assert False
b663b9
+
b663b9
+        # Bind a different user who has rights
b663b9
+        topo.standalone.simple_bind_s(BIND_DN2, PASSWORD)
b663b9
+        entries = topo.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, SRCH_FILTER2)
b663b9
+        if entries is None or len(entries) == 0:
b663b9
+            topo.standalone.log.fatal("Failed to get entry as good user")
b663b9
+            assert False
b663b9
+
b663b9
+        entries = topo.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, SRCH_FILTER2)
b663b9
+        if entries is None or len(entries) == 0:
b663b9
+            topo.standalone.log.fatal("Failed to get entry as good user (2)")
b663b9
+            assert False
b663b9
+
b663b9
+        if run > 0:
b663b9
+            # Second pass
b663b9
+            topo.standalone.restart()
b663b9
+
b663b9
+        # Reset ACI's and do the second test
b663b9
+        topo.standalone.log.info("Testing search does not return any entries...")
b663b9
+        topo.standalone.simple_bind_s(DN_DM, PASSWORD)
b663b9
+        suffix.set('aci', aci_list_B, ldap.MOD_REPLACE)
b663b9
+        time.sleep(1)
b663b9
+
b663b9
+        topo.standalone.simple_bind_s(BIND_DN, PASSWORD)
b663b9
+        entries = topo.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, SRCH_FILTER)
b663b9
+        if entries and entries[0]:
b663b9
+            topo.standalone.log.fatal("Incorrectly got an entry returned from search 1")
b663b9
+            assert False
b663b9
+
b663b9
+        entries = topo.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, SRCH_FILTER)
b663b9
+        if entries and entries[0]:
b663b9
+            topo.standalone.log.fatal("Incorrectly got an entry returned from search 2")
b663b9
+            assert False
b663b9
+
b663b9
+        if run > 0:
b663b9
+            # Second pass
b663b9
+            topo.standalone.restart()
b663b9
+
b663b9
+        # Bind as different user who has rights
b663b9
+        topo.standalone.simple_bind_s(BIND_DN2, PASSWORD)
b663b9
+        entries = topo.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, SRCH_FILTER2)
b663b9
+        if entries is None or len(entries) == 0:
b663b9
+            topo.standalone.log.fatal("Failed to get entry as good user")
b663b9
+            assert False
b663b9
+
b663b9
+        entries = topo.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, SRCH_FILTER2)
b663b9
+        if entries is None or len(entries) == 0:
b663b9
+            topo.standalone.log.fatal("Failed to get entry as good user (2)")
b663b9
+            assert False
b663b9
+
b663b9
+        entries = topo.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, SRCH_FILTER)
b663b9
+        if entries and entries[0]:
b663b9
+            topo.standalone.log.fatal("Incorrectly got an entry returned from search 1")
b663b9
+            assert False
b663b9
+
b663b9
+        entries = topo.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, SRCH_FILTER)
b663b9
+        if entries and entries[0]:
b663b9
+            topo.standalone.log.fatal("Incorrectly got an entry returned from search 2")
b663b9
+            assert False
b663b9
+
b663b9
+        # back to user 1
b663b9
+        topo.standalone.simple_bind_s(BIND_DN, PASSWORD)
b663b9
+        entries = topo.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, SRCH_FILTER2)
b663b9
+        if entries is None or len(entries) == 0:
b663b9
+            topo.standalone.log.fatal("Failed to get entry as user1")
b663b9
+            assert False
b663b9
+
b663b9
+        entries = topo.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, SRCH_FILTER2)
b663b9
+        if entries is None or len(entries) == 0:
b663b9
+            topo.standalone.log.fatal("Failed to get entry as user1 (2)")
b663b9
+            assert False
b663b9
+
b663b9
+        entries = topo.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, SRCH_FILTER)
b663b9
+        if entries and entries[0]:
b663b9
+            topo.standalone.log.fatal("Incorrectly got an entry returned from search 1")
b663b9
+            assert False
b663b9
+
b663b9
+        entries = topo.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, SRCH_FILTER)
b663b9
+        if entries and entries[0]:
b663b9
+            topo.standalone.log.fatal("Incorrectly got an entry returned from search 2")
b663b9
+            assert False
b663b9
+
b663b9
+    topo.standalone.log.info("Test PASSED")
b663b9
+
b663b9
+
b663b9
+if __name__ == '__main__':
b663b9
+    # Run isolated
b663b9
+    # -s for DEBUG mode
b663b9
+    CURRENT_FILE = os.path.realpath(__file__)
b663b9
+    pytest.main(["-s", CURRENT_FILE])
b663b9
+
b663b9
diff --git a/ldap/servers/plugins/acl/acl.c b/ldap/servers/plugins/acl/acl.c
b663b9
index bc154c78f..6d105f4fa 100644
b663b9
--- a/ldap/servers/plugins/acl/acl.c
b663b9
+++ b/ldap/servers/plugins/acl/acl.c
b663b9
@@ -1088,9 +1088,23 @@ acl_read_access_allowed_on_entry(
b663b9
                     ** a DENY rule, then we don't have access to
b663b9
                     ** the entry ( nice trick to get in )
b663b9
                     */
b663b9
-                    if (aclpb->aclpb_state &
b663b9
-                        ACLPB_EXECUTING_DENY_HANDLES)
b663b9
+                    if (aclpb->aclpb_state & ACLPB_EXECUTING_DENY_HANDLES) {
b663b9
+                        aclEvalContext *c_ContextEval = &aclpb->aclpb_curr_entryEval_context;
b663b9
+                        AclAttrEval *c_attrEval = NULL;
b663b9
+                        /*
b663b9
+                         * The entire entry is blocked, but previously evaluated allow aci's might
b663b9
+                         * show some of the attributes as readable in the acl cache, so reset all
b663b9
+                         * the cached attributes' status to FAIL.
b663b9
+                         */
b663b9
+                        for (size_t j = 0; j < c_ContextEval->acle_numof_attrs; j++) {
b663b9
+                            c_attrEval = &c_ContextEval->acle_attrEval[j];
b663b9
+                            c_attrEval->attrEval_r_status &= ~ACL_ATTREVAL_SUCCESS;
b663b9
+                            c_attrEval->attrEval_r_status |= ACL_ATTREVAL_FAIL;
b663b9
+                            c_attrEval->attrEval_s_status &= ~ACL_ATTREVAL_SUCCESS;
b663b9
+                            c_attrEval->attrEval_s_status |= ACL_ATTREVAL_FAIL;
b663b9
+                        }
b663b9
                         return LDAP_INSUFFICIENT_ACCESS;
b663b9
+                    }
b663b9
 
b663b9
                     /* The other case is I don't have an
b663b9
                     ** explicit allow rule -- which is fine.
b663b9
@@ -2908,6 +2922,12 @@ acl__TestRights(Acl_PBlock *aclpb, int access, const char **right, const char **
b663b9
         result_reason->deciding_aci = NULL;
b663b9
         result_reason->reason = ACL_REASON_NO_MATCHED_RESOURCE_ALLOWS;
b663b9
 
b663b9
+        /* If we have deny handles we should process them */
b663b9
+        if (aclpb->aclpb_num_deny_handles > 0) {
b663b9
+            aclpb->aclpb_state &= ~ACLPB_EXECUTING_ALLOW_HANDLES;
b663b9
+            aclpb->aclpb_state |= ACLPB_EXECUTING_DENY_HANDLES;
b663b9
+        }
b663b9
+
b663b9
         TNF_PROBE_1_DEBUG(acl__TestRights_end, "ACL", "",
b663b9
                           tnf_string, no_allows, "");
b663b9
 
b663b9
-- 
b663b9
2.17.0
b663b9