From 22f1912f95e2188743525dba876cd122db27bb13 Mon Sep 17 00:00:00 2001
From: William Brown <william@blackhats.net.au>
Date: Tue, 25 Aug 2015 15:38:11 +0930
Subject: [PATCH] Add dereference request control to lib389 for testing.

---
 lib389/__init__.py        |  43 ++++++++++++++++++
 lib389/_constants.py      |   6 +++
 lib389/_controls.py       | 110 ++++++++++++++++++++++++++++++++++++++++++++++
 tests/dereference_test.py |  81 ++++++++++++++++++++++++++++++++++
 4 files changed, 240 insertions(+)
 create mode 100644 lib389/_controls.py
 create mode 100644 tests/dereference_test.py

diff --git a/lib389/__init__.py b/lib389/__init__.py
index 8dfe28f..201bf32 100644
--- a/lib389/__init__.py
+++ b/lib389/__init__.py
@@ -73,6 +73,7 @@ MAJOR, MINOR, _, _, _ = sys.version_info
 
 if MAJOR >= 3 or (MAJOR == 2 and MINOR >= 7):
     from ldap.controls.simple import GetEffectiveRightsControl
+    from lib389._controls import DereferenceControl
 
 RE_DBMONATTR = re.compile(r'^([a-zA-Z]+)-([1-9][0-9]*)$')
 RE_DBMONATTRSUN = re.compile(r'^([a-zA-Z]+)-([a-zA-Z]+)$')
@@ -2401,6 +2402,48 @@ class DirSrv(SimpleLDAPObject):
             self.set_option(ldap.OPT_SERVER_CONTROLS, [])
         return ldap_result
 
+    # Is there a better name for this function?
+    def dereference(self, deref, base=DEFAULT_SUFFIX, scope=ldap.SCOPE_SUBTREE, *args, **kwargs):
+        """
+        Perform a search which dereferences values from attributes such as member
+        or unique member.
+        For arguments to this function, please see LDAPObject.search_s. For example:
+
+        @param deref - Dereference query
+        @param base - Base DN of the suffix to check
+        @param scope - search scope
+        @param args -
+        @param kwargs -
+        @return - ldap result
+
+        LDAPObject.search_s(base, scope[, filterstr='(objectClass=*)'[, attrlist=None[, attrsonly=0]]]) -> list|None
+
+        A deref query is of the format:
+
+        "<attribute to derference>:<deref attr1>,<deref attr2>..."
+
+        "uniqueMember:dn,objectClass"
+
+        This will return the dn's and objectClasses of the dereferenced members of the group.
+        """
+        if not (MAJOR >= 3 or (MAJOR == 2 and MINOR >= 7)):
+            raise Exception("UNSUPPORTED EXTENDED OPERATION ON THIS VERSION OF PYTHON")
+        ldap_result = None
+        # This may not be thread safe. Is there a better way to do this?
+        try:
+            drc = DereferenceControl(True, deref=deref.encode('UTF-8'))
+            sctrl = [drc]
+            self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+
+            #ldap_result = self.search_s(base, scope, *args, **kwargs)
+            res = self.search(base, scope, *args, **kwargs)
+            resp_type, resp_data, resp_msgid, decoded_resp_ctrls, resp_name, resp_value = self.result4(res, add_ctrls=1, resp_ctrl_classes={CONTROL_DEREF: DereferenceControl})
+            print(resp_name)
+            print(resp_value)
+        finally:
+            self.set_option(ldap.OPT_SERVER_CONTROLS, [])
+        return resp_data, decoded_resp_ctrls
+
     def buildLDIF(self, num, ldif_file, suffix='dc=example,dc=com'):
         """Generate a simple ldif file using the dbgen.pl script, and set the
            ownership and permissions to match the user that the server runs as.
diff --git a/lib389/_constants.py b/lib389/_constants.py
index efabfc6..69c305b 100644
--- a/lib389/_constants.py
+++ b/lib389/_constants.py
@@ -114,6 +114,12 @@ RDN_REPLICA     = "cn=replica"
 
 RETROCL_SUFFIX = "cn=changelog"
 
+##################################
+###
+### Request Control OIDS
+###
+##################################
+CONTROL_DEREF = '1.3.6.1.4.1.4203.666.5.16'
 
 ##################################
 ###
diff --git a/lib389/_controls.py b/lib389/_controls.py
new file mode 100644
index 0000000..93c5fe0
--- /dev/null
+++ b/lib389/_controls.py
@@ -0,0 +1,110 @@
+"""
+Lib389 python ldap request controls.
+
+These should be upstreamed into python ldap when possible.
+"""
+
+from lib389._constants import *
+
+from ldap.controls import LDAPControl
+
+from pyasn1.type import namedtype,univ
+from pyasn1.codec.ber import encoder,decoder
+from pyasn1_modules.rfc2251 import AttributeDescription, LDAPDN, PartialAttributeList
+
+# Could use AttributeDescriptionList
+
+"""
+ controlValue ::= SEQUENCE OF derefSpec DerefSpec
+
+ DerefSpec ::= SEQUENCE {
+     derefAttr       attributeDescription,    ; with DN syntax
+     attributes      AttributeList }
+
+ AttributeList ::= SEQUENCE OF attr AttributeDescription
+
+ Needs to be matched by ber_scanf(ber, "{a{v}}", ... )
+"""
+class AttributeList(univ.SequenceOf):
+    componentType = AttributeDescription()
+
+class DerefSpec(univ.Sequence):
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('derefAttr', AttributeDescription()),
+        namedtype.NamedType('attributes', AttributeList()),
+    )
+
+class DerefControlValue(univ.SequenceOf):
+    componentType = DerefSpec()
+
+"""
+ controlValue ::= SEQUENCE OF derefRes DerefRes
+
+ DerefRes ::= SEQUENCE {
+     derefAttr       AttributeDescription,
+     derefVal        LDAPDN,
+     attrVals        [0] PartialAttributeList OPTIONAL }
+
+ PartialAttributeList ::= SEQUENCE OF
+                partialAttribute PartialAttribute
+"""
+
+class DerefRes(univ.Sequence):
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('derefAttr', AttributeDescription()),
+        namedtype.NamedType('derefVal', LDAPDN()),
+        namedtype.OptionalNamedType('attrVals', PartialAttributeList()),
+    )
+
+class DerefResultControlValue(univ.SequenceOf):
+    componentType = DerefRes()
+
+class DereferenceControl(LDAPControl):
+    """
+    Dereference Control
+    """
+
+    def __init__(self, criticality=True, deref=None):
+        LDAPControl.__init__(self, CONTROL_DEREF, criticality)
+        self.deref = deref
+
+    def encodeControlValue(self):
+        cv = DerefControlValue()
+        cvi = 0
+        for derefSpec in self.deref.split(';'):
+            derefAttr, attributes = derefSpec.split(':')
+            attributes = attributes.split(',')
+            al = AttributeList()
+            i = 0
+            while len(attributes) > 0:
+                al.setComponentByPosition(i, attributes.pop())
+                i += 1
+            ds = DerefSpec()
+            ds.setComponentByName('derefAttr', derefAttr)
+            ds.setComponentByName('attributes', al)
+            cv.setComponentByPosition(cvi, ds)
+            cvi += 1
+        return encoder.encode(cv)
+
+    def decodeControlValue(self,encodedControlValue):
+        self.entry = {}
+        #decodedValue,_ = decoder.decode(encodedControlValue,asn1Spec=DerefResultControlValue())
+        # Gets the error: TagSet(Tag(tagClass=0, tagFormat=32, tagId=16), Tag(tagClass=128, tagFormat=32, tagId=0)) not in asn1Spec: {TagSet(Tag(tagClass=0, tagFormat=32, tagId=16)): PartialAttributeList()}/{}
+        decodedValue,_ = decoder.decode(encodedControlValue)
+        print(decodedValue.prettyPrint())
+        # Pretty print yields
+        #Sequence:  <-- Sequence of
+        # <no-name>=Sequence:  <-- derefRes
+        #  <no-name>=uniqueMember <-- derefAttr
+        #  <no-name>=uid=test,dc=example,dc=com <-- derefVal
+        #  <no-name>=Sequence: <-- attrVals
+        #   <no-name>=uid
+        #   <no-name>=Set:
+        #    <no-name>=test
+        # For now, while asn1spec is sad, we'll just rely on it being well formed
+        # However, this isn't good, as without the asn1spec, we seem to actually be dropping values ....
+        for result in decodedValue:
+            derefAttr, derefVal, _ = result
+            self.entry[str(derefAttr)] = str(derefVal)
+        return self.entry
+
diff --git a/tests/dereference_test.py b/tests/dereference_test.py
new file mode 100644
index 0000000..86c2b31
--- /dev/null
+++ b/tests/dereference_test.py
@@ -0,0 +1,81 @@
+'''
+Created on Aug 1, 2015
+
+@author: William Brown
+'''
+from lib389._constants import *
+from lib389 import DirSrv,Entry
+import ldap
+
+INSTANCE_PORT     = 54321
+INSTANCE_SERVERID = 'dereferenceds'
+INSTANCE_PREFIX   = '/srv'
+
+class Test_dereference():
+    def setUp(self):
+        instance = DirSrv(verbose=False)
+        instance.log.debug("Instance allocated")
+        args = {SER_HOST:          LOCALHOST,
+                SER_PORT:          INSTANCE_PORT,
+                SER_DEPLOYED_DIR:  INSTANCE_PREFIX,
+                SER_SERVERID_PROP: INSTANCE_SERVERID
+                }
+        instance.allocate(args)
+        if instance.exists():
+            instance.delete()
+        instance.create()
+        instance.open()
+        self.instance = instance
+
+    def tearDown(self):
+        if self.instance.exists():
+            #self.instance.db2ldif(bename='userRoot', suffixes=[DEFAULT_SUFFIX], excludeSuffixes=[], encrypt=False, \
+            #repl_data=False, outputfile='%s/ldif/%s.ldif' % (self.instance.dbdir,INSTANCE_SERVERID ))
+            #self.instance.clearBackupFS()
+            #self.instance.backupFS()
+            self.instance.delete()
+
+    def add_user(self):
+        # Create a user entry
+        for i in range(0,2):
+            uentry = Entry('uid=test%s,%s' % (i, DEFAULT_SUFFIX))
+            uentry.setValues('objectclass', 'top', 'extensibleobject')
+            uentry.setValues('uid', 'test')
+            self.instance.add_s(uentry)
+        #self.instance.log.debug("Created user entry as:" ,uentry.dn)
+
+    def add_group(self):
+        # Create a group for the user to have some rights to
+        gentry = Entry('cn=testgroup,%s' % DEFAULT_SUFFIX)
+        gentry.setValues('objectclass', 'top', 'extensibleobject')
+        gentry.setValues('cn', 'testgroup')
+        for i in range(0,2):
+            gentry.setValues('uniqueMember', 'uid=test%s,%s' % (i,DEFAULT_SUFFIX))
+        self.instance.add_s(gentry)
+
+    def test_dereference(self):
+        # Run an effective rights search
+        try:
+            result, control_response = self.instance.dereference('uniqueMember:dn,uid;uniqueMember:dn,uid', filterstr='(cn=testgroup)')
+            assert False
+        except ldap.UNAVAILABLE_CRITICAL_EXTENSION:
+            # This is a good thing! It means our deref Control Value is correctly formatted.
+            pass
+        result, control_response = self.instance.dereference('uniqueMember:cn,uid,objectclass', filterstr='(cn=testgroup)')
+
+        #rights = result[0]
+        print(result[0])
+        print(control_response)
+        # This isn't meant to be in the result, it should be in control response
+        assert result[0][2][0].entry == {'uniqueMember': 'uid=test1,dc=example,dc=com'}
+        #assert rights.getValue('attributeLevelRights') == 'cn:rsc'
+        #assert rights.getValue('entryLevelRights') == 'v'
+
+if __name__ == "__main__":
+    test = Test_dereference()
+    test.setUp()
+    test.add_user()
+    test.add_group()
+    test.test_dereference()
+    test.tearDown()
+
-- 
2.4.3

