From 2dc26d9b7d630fd8d57c28ebf5d7e1607ad4e913 Mon Sep 17 00:00:00 2001
From: Amita Sharma <amsharma@redhat.com>
Date: Aug 29 2018 15:43:54 +0000
Subject: Issue 48053 - Add attribute encryption test cases


Description: Added test cases for attribute encryption

https://pagure.io/389-ds-base/issue/48053

Reviewed by: Simon and Viktor

---

diff --git a/dirsrvtests/tests/suites/attr_encryption/attr_encryption_test.py b/dirsrvtests/tests/suites/attr_encryption/attr_encryption_test.py
new file mode 100644
index 0000000..ccf7eac
--- /dev/null
+++ b/dirsrvtests/tests/suites/attr_encryption/attr_encryption_test.py
@@ -0,0 +1,453 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+
+import logging
+import pytest
+from lib389.tasks import *
+from lib389.topologies import topology_st as topo
+from lib389.utils import *
+from lib389._constants import DEFAULT_SUFFIX
+from lib389.idm.user import UserAccounts, TEST_USER_PROPERTIES
+from lib389.backend import Backends
+from lib389.idm.domain import Domain
+
+USER_DN = 'uid=test_user,%s' % DEFAULT_SUFFIX
+
+logging.getLogger(__name__).setLevel(logging.INFO)
+log = logging.getLogger(__name__)
+
+
+@pytest.fixture(scope="module")
+def enable_user_attr_encryption(topo, request):
+    """ Enables attribute encryption for various attributes
+        Adds a test user with encrypted attributes
+    """
+
+    log.info("Enable TLS for attribute encryption")
+    topo.standalone.enable_tls()
+
+    log.info("Enables attribute encryption")
+    backends = Backends(topo.standalone)
+    backend = backends.list()[0]
+    encrypt_attrs = backend.get_encrypted_attrs()
+
+    log.info("Enables attribute encryption for employeeNumber and telephoneNumber")
+    emp_num_encrypt = encrypt_attrs.create(properties={'cn': 'employeeNumber', 'nsEncryptionAlgorithm': 'AES'})
+    telephone_encrypt = encrypt_attrs.create(properties={'cn': 'telephoneNumber', 'nsEncryptionAlgorithm': '3DES'})
+
+    log.info("Add a test user with encrypted attributes")
+    users = UserAccounts(topo.standalone, DEFAULT_SUFFIX)
+    test_user = users.create(properties=TEST_USER_PROPERTIES)
+    test_user.replace('employeeNumber', '1000')
+    test_user.replace('telephonenumber', '1234567890')
+
+    def fin():
+        log.info("Remove attribute encryption for various attributes")
+        emp_num_encrypt.delete()
+        telephone_encrypt.delete()
+
+    request.addfinalizer(fin)
+    return test_user
+
+
+def test_basic(topo, enable_user_attr_encryption):
+    """Tests encrypted attributes with a test user entry
+    :id: d767d5c8-b934-4b14-9774-bd13480d81b3
+    :setup: Standalone instance
+            Enable AES encryption config on employeenumber
+            Enable 3DES encryption config on telephoneNumber
+            Add a test user with with encrypted attributes
+    :steps:
+         1. Restart the server
+         2. Check employeenumber encryption enabled
+         3. Check telephoneNumber encryption enabled
+         4. Check that encrypted attribute is present for user i.e. telephonenumber
+     :expectedresults:
+         1. This should be successful
+         2. This should be successful
+         3. This should be successful
+         4. This should be successful
+    """
+
+    log.info("Restart the server")
+    topo.standalone.restart()
+    backends = Backends(topo.standalone)
+    backend = backends.list()[0]
+    encrypt_attrs = backend.get_encrypted_attrs()
+
+    log.info("Extracting values of cn from the list of objects in encrypt_attrs")
+    log.info("And appending the cn values in a list")
+    enc_attrs_cns = []
+    for enc_attr in encrypt_attrs.list():
+        enc_attrs_cns.append(enc_attr.rdn)
+
+    log.info("Check employeenumber encryption is enabled")
+    assert "employeeNumber" in enc_attrs_cns
+
+    log.info("Check telephoneNumber encryption is enabled")
+    assert "telephoneNumber" in enc_attrs_cns
+
+    log.info("Check that encrypted attribute is present for user i.e. telephonenumber")
+    assert enable_user_attr_encryption.present('telephoneNumber')
+
+
+def test_export_import_ciphertext(topo, enable_user_attr_encryption):
+    """Configure attribute encryption, store some data, check that we can export the ciphertext
+    :id: b433e215-2926-48a5-818f-c21abc40fc2d
+    :setup: Standalone instance
+            Enable AES encryption config on employeenumber
+            Enable 3DES encryption config on telephoneNumber
+            Add a test user with encrypted attributes
+    :steps:
+         1. Export data as ciphertext
+         2. Check that the attribute is present in the exported file
+         3. Check that the encrypted value of attribute is not present in the exported file
+         4. Delete the test user entry with encrypted data
+         5. Import the previously exported data as ciphertext
+         6. Check attribute telephoneNumber should be imported
+    :expectedresults:
+         1. This should be successful
+         2. This should be successful
+         3. This should be successful
+         4. This should be successful
+         5. This should be successful
+         6. This should be successful
+    """
+
+    log.info("Export data as ciphertext")
+    export_ldif = os.path.join(topo.standalone.ds_paths.ldif_dir, "export_ciphertext.ldif")
+
+    # Offline export
+    topo.standalone.stop()
+    if not topo.standalone.db2ldif(bename=DEFAULT_BENAME, suffixes=(DEFAULT_SUFFIX,),
+                                   excludeSuffixes=None, encrypt=False, repl_data=None, outputfile=export_ldif):
+        log.fatal('Failed to run offline db2ldif')
+        assert False
+    topo.standalone.start()
+
+    log.info("Check that the attribute is present in the exported file")
+    log.info("Check that the encrypted value of attribute is not present in the exported file")
+    with open(export_ldif, 'r') as ldif_file:
+        ldif = ldif_file.read()
+        assert 'telephonenumber' in ldif
+        assert 'telephonenumber: 1234567890' not in ldif
+
+    log.info("Delete the test user entry with encrypted data")
+    enable_user_attr_encryption.delete()
+
+    log.info("Import data as ciphertext, which was exported previously")
+    import_ldif = os.path.join(topo.standalone.ds_paths.ldif_dir, "export_ciphertext.ldif")
+
+    # Offline export
+    topo.standalone.stop()
+    if not topo.standalone.ldif2db(bename=DEFAULT_BENAME, suffixes=(DEFAULT_SUFFIX,),
+                                   excludeSuffixes=None,  encrypt=False, import_file=import_ldif):
+        log.fatal('Failed to run offline ldif2db')
+        assert False
+    topo.standalone.start()
+
+    log.info("Check that the data with encrypted attribute is imported properly")
+    users = UserAccounts(topo.standalone, DEFAULT_SUFFIX)
+    user = users.get('testuser')
+    assert user.present("telephoneNumber")
+
+
+
+def test_export_import_plaintext(topo, enable_user_attr_encryption):
+    """Configure attribute encryption, store some data, check that we can export the plain text
+     :id: b171e215-0456-48a5-245f-c21abc40fc2d
+     :setup: Standalone instance
+             Enable AES encryption config on employeenumber
+             Enable 3DES encryption config on telephoneNumber
+             Add a test user with encrypted attributes
+     :steps:
+          1. Export data as plain text
+          2. Check that the attribute is present in the exported file
+          3. Check that the encrypted value of attribute is also present in the exported file
+          4. Delete the test user entry with encrypted data
+          5. Import data as plaintext
+          6. Check attribute value of telephoneNumber
+     :expectedresults:
+          1. This should be successful
+          2. This should be successful
+          3. This should be successful
+          4. This should be successful
+          5. This should be successful
+          6. This should be successful
+     """
+
+    log.info("Export data as plain text")
+    export_ldif = os.path.join(topo.standalone.ds_paths.ldif_dir, "export_plaintext.ldif")
+
+    # Offline export
+    topo.standalone.stop()
+    if not topo.standalone.db2ldif(bename=DEFAULT_BENAME, suffixes=(DEFAULT_SUFFIX,),
+                                   excludeSuffixes=None, encrypt=True, repl_data=None, outputfile=export_ldif):
+        log.fatal('Failed to run offline db2ldif')
+        assert False
+    topo.standalone.start()
+
+    log.info("Check that the attribute is present in the exported file")
+    log.info("Check that the plain text value of the encrypted attribute is present in the exported file")
+    with open(export_ldif, 'r') as ldif_file:
+        assert 'telephoneNumber: 1234567890' in ldif_file.read()
+
+    log.info("Delete the test user entry with encrypted data")
+    enable_user_attr_encryption.delete()
+
+    log.info("Import data as plain text, which was exported previously")
+    import_ldif = os.path.join(topo.standalone.ds_paths.ldif_dir, "export_plaintext.ldif")
+
+    # Offline export
+    topo.standalone.stop()
+    if not topo.standalone.ldif2db(bename=DEFAULT_BENAME, suffixes=(DEFAULT_SUFFIX,),
+                                   excludeSuffixes=None,  encrypt=True, import_file=import_ldif):
+        log.fatal('Failed to run offline ldif2db')
+        assert False
+    topo.standalone.start()
+
+    log.info("Check that the attribute is imported properly")
+    users = UserAccounts(topo.standalone, DEFAULT_SUFFIX)
+    user = users.get('testuser')
+    assert user.present("telephoneNumber")
+
+
+def test_attr_encryption_unindexed(topo, enable_user_attr_encryption):
+    """Configure attribute encryption for an un-indexed attribute, check that we can export encrypted data
+    :id: d3ef38e1-bb5a-44d8-a3a4-4a25a57e3454
+    :setup: Standalone instance
+            Enable AES encryption config on employeenumber
+            Enable 3DES encryption config on telephoneNumber
+            Add a test user with encrypted attributes
+    :steps:
+         1. Export data as cipher text
+         2. Check that the unindexed attribute employeenumber is present in exported ldif file
+         3. Check that the unindexed attribute employeenumber value is not present in exported ldif file
+    :expectedresults:
+         1. This should be successful
+         2. This should be successful
+         3. This should be successful
+    """
+    log.info("Export data as cipher text")
+    export_ldif = os.path.join(topo.standalone.ds_paths.ldif_dir, "emp_num_ciphertext.ldif")
+
+    # Offline export
+    topo.standalone.stop()
+    if not topo.standalone.db2ldif(bename=DEFAULT_BENAME, suffixes=(DEFAULT_SUFFIX,),
+                                   excludeSuffixes=None, encrypt=False, repl_data=None, outputfile=export_ldif):
+        log.fatal('Failed to run offline db2ldif')
+        assert False
+    topo.standalone.start()
+
+    log.info("Check that the attribute is present in the exported file")
+    log.info("Check that the encrypted value of attribute is not present in the exported file")
+    with open(export_ldif, 'r') as ldif_file:
+        ldif = ldif_file.read()
+        assert 'employeeNumber' in ldif
+        assert 'employeeNumber: 1000' not in ldif
+
+
+def test_attr_encryption_multiple_backends(topo, enable_user_attr_encryption):
+    """Tests Configuration of attribute encryption for multiple backends
+       Where both the backends have attribute encryption
+    :id: f3ef40e1-17d6-44d8-a3a4-4a25a57e9064
+    :setup: Standalone instance
+            SSL Enabled
+    :steps:
+         1. Add two test backends
+         2. Configure attribute encryption for telephonenumber in one test backend
+         3. Configure attribute encryption for employeenumber in another test backend
+         4. Add a test user in both backends with encrypted attributes
+         5. Export data as ciphertext from both backends
+         6. Check that telephoneNumber is encrypted in the ldif file of db1
+         7. Check that employeeNumber is encrypted in the ldif file of db2
+         8. Delete both test backends
+    :expectedresults:
+         1. This should be successful
+         2. This should be successful
+         3. This should be successful
+         4. This should be successful
+         5. This should be successful
+         6. This should be successful
+         7. This should be successful
+         8. This should be successful
+    """
+    log.info("Add two test backends")
+    test_suffix1 = 'dc=test1,dc=com'
+    test_db1 = 'test_db1'
+    test_suffix2 = 'dc=test2,dc=com'
+    test_db2 = 'test_db2'
+
+    # Create backends
+    backends = Backends(topo.standalone)
+    test_backend1 = backends.create(properties={'cn': test_db1,
+                                               'nsslapd-suffix': test_suffix1})
+    test_backend2 = backends.create(properties={'cn': test_db2,
+                                                'nsslapd-suffix': test_suffix2})
+
+    # Create the top of the tree
+    suffix1 = Domain(topo.standalone, test_suffix1)
+    test1 = suffix1.create(properties={'dc': 'test1'})
+    suffix2 = Domain(topo.standalone, test_suffix2)
+    test2 = suffix2.create(properties={'dc': 'test2'})
+
+    log.info("Enables attribute encryption for telephoneNumber in test_backend1")
+    backend1_encrypt_attrs = test_backend1.get_encrypted_attrs()
+    b1_encrypt = backend1_encrypt_attrs.create(properties={'cn': 'telephoneNumber',
+                                                           'nsEncryptionAlgorithm': 'AES'})
+
+    log.info("Enables attribute encryption for employeeNumber in test_backend2")
+    backend2_encrypt_attrs = test_backend2.get_encrypted_attrs()
+    b2_encrypt = backend2_encrypt_attrs.create(properties={'cn': 'employeeNumber',
+                                                                 'nsEncryptionAlgorithm': 'AES'})
+
+    log.info("Add a test user with encrypted attributes in both backends")
+    users = UserAccounts(topo.standalone, test1.dn, None)
+    test_user = users.create(properties=TEST_USER_PROPERTIES)
+    test_user.replace('telephoneNumber', '1234567890')
+
+    users = UserAccounts(topo.standalone, test2.dn, None)
+    test_user = users.create(properties=TEST_USER_PROPERTIES)
+    test_user.replace('employeeNumber', '1000')
+
+    log.info("Export data as ciphertext from both backends")
+    export_db1 = os.path.join(topo.standalone.ds_paths.ldif_dir, "export_db1.ldif")
+    export_db2 = os.path.join(topo.standalone.ds_paths.ldif_dir, "export_db2.ldif")
+
+    # Offline export
+    topo.standalone.stop()
+    if not topo.standalone.db2ldif(bename=test_db1, suffixes=(test_suffix1,),
+                                   excludeSuffixes=None, encrypt=False, repl_data=None, outputfile=export_db1):
+        log.fatal('Failed to run offline db2ldif')
+        assert False
+
+    if not topo.standalone.db2ldif(bename=test_db2, suffixes=(test_suffix2,),
+                                   excludeSuffixes=None, encrypt=False, repl_data=None, outputfile=export_db2):
+        log.fatal('Failed to run offline db2ldif')
+        assert False
+    topo.standalone.start()
+
+    log.info("Check that the attribute is present in the exported file in db1")
+    log.info("Check that the encrypted value of attribute is not present in the exported file in db1")
+    with open(export_db1, 'r') as ldif_file:
+        ldif = ldif_file.read()
+        assert 'telephoneNumber' in ldif
+        assert 'telephoneNumber: 1234567890' not in ldif
+
+    log.info("Check that the attribute is present in the exported file in db2")
+    log.info("Check that the encrypted value of attribute is not present in the exported file in db2")
+    with open(export_db2, 'r') as ldif_file:
+        ldif = ldif_file.read()
+        assert 'employeeNumber' in ldif
+        assert 'employeeNumber: 1000' not in ldif
+
+    log.info("Delete test backends")
+    test_backend1.delete()
+    test_backend2.delete()
+
+
+def test_attr_encryption_backends(topo, enable_user_attr_encryption):
+    """Tests Configuration of attribute encryption for single backend
+       where more backends are present
+    :id: f3ef40e1-17d6-44d8-a3a4-4a25a57e9064
+    :setup: Standalone instance
+            SSL Enabled
+    :steps:
+         1. Add two test backends
+         2. Configure attribute encryption for telephoneNumber in one test backend
+         3. Add a test user in both backends with telephoneNumber
+         4. Export ldif from both test backends
+         5. Check that telephonenumber is encrypted in the ldif file of db1
+         6. Check that telephonenumber is not encrypted in the ldif file of db2
+         7. Delete both test backends
+    :expectedresults:
+         1. This should be successful
+         2. This should be successful
+         3. This should be successful
+         4. This should be successful
+         5. This should be successful
+         6. This should be successful
+         7. This should be successful
+    """
+    log.info("Add two test backends")
+    test_suffix1 = 'dc=test1,dc=com'
+    test_db1 = 'test_db1'
+    test_suffix2 = 'dc=test2,dc=com'
+    test_db2 = 'test_db2'
+
+    # Create backends
+    backends = Backends(topo.standalone)
+    test_backend1 = backends.create(properties={'cn': test_db1,
+                                               'nsslapd-suffix': test_suffix1})
+    test_backend2 = backends.create(properties={'cn': test_db2,
+                                                'nsslapd-suffix': test_suffix2})
+
+    # Create the top of the tree
+    suffix1 = Domain(topo.standalone, test_suffix1)
+    test1 = suffix1.create(properties={'dc': 'test1'})
+    suffix2 = Domain(topo.standalone, test_suffix2)
+    test2 = suffix2.create(properties={'dc': 'test2'})
+
+    log.info("Enables attribute encryption for telephoneNumber in test_backend1")
+    backend1_encrypt_attrs = test_backend1.get_encrypted_attrs()
+    b1_encrypt = backend1_encrypt_attrs.create(properties={'cn': 'telephoneNumber',
+                                                           'nsEncryptionAlgorithm': 'AES'})
+
+    log.info("Add a test user with telephoneNumber in both backends")
+    users = UserAccounts(topo.standalone, test1.dn, None)
+    test_user = users.create(properties=TEST_USER_PROPERTIES)
+    test_user.replace('telephoneNumber', '1234567890')
+
+    users = UserAccounts(topo.standalone, test2.dn, None)
+    test_user = users.create(properties=TEST_USER_PROPERTIES)
+    test_user.replace('telephoneNumber', '1234567890')
+
+    log.info("Export data as ciphertext from both backends")
+    export_db1 = os.path.join(topo.standalone.ds_paths.ldif_dir, "export_db1.ldif")
+    export_db2 = os.path.join(topo.standalone.ds_paths.ldif_dir, "export_db2.ldif")
+
+    # Offline export
+    topo.standalone.stop()
+    if not topo.standalone.db2ldif(bename=test_db1, suffixes=(test_suffix1,),
+                                   excludeSuffixes=None, encrypt=False, repl_data=None, outputfile=export_db1):
+        log.fatal('Failed to run offline db2ldif')
+        assert False
+
+    if not topo.standalone.db2ldif(bename=test_db2, suffixes=(test_suffix2,),
+                                   excludeSuffixes=None, encrypt=False, repl_data=None, outputfile=export_db2):
+        log.fatal('Failed to run offline db2ldif')
+        assert False
+    topo.standalone.start()
+
+    log.info("Check that the attribute is present in the exported file in db1")
+    log.info("Check that the encrypted value of attribute is not present in the exported file in db1")
+    with open(export_db1, 'r') as ldif_file:
+        ldif = ldif_file.read()
+        assert 'telephoneNumber' in ldif
+        assert 'telephoneNumber: 1234567890' not in ldif
+
+    log.info("Check that the attribute is present in the exported file in db2")
+    log.info("Check that the value of attribute is also present in the exported file in db2")
+    with open(export_db2, 'r') as ldif_file:
+        ldif = ldif_file.read()
+        assert 'telephoneNumber' in ldif
+        assert 'telephoneNumber: 1234567890' in ldif
+
+    log.info("Delete test backends")
+    test_backend1.delete()
+    test_backend2.delete()
+
+
+if __name__ == '__main__':
+    # Run isolated
+    # -s for DEBUG mode
+    CURRENT_FILE = os.path.realpath(__file__)
+    pytest.main("-s %s" % CURRENT_FILE)
+
+
diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py
index 1fd57ba..64ffb48 100644
--- a/src/lib389/lib389/backend.py
+++ b/src/lib389/lib389/backend.py
@@ -23,6 +23,7 @@ from lib389.exceptions import NoSuchEntryError, InvalidArgumentError
 from lib389.monitor import MonitorBackend
 from lib389.index import Indexes
 from lib389.tasks import ImportTask, ExportTask
+from lib389.encrypted_attributes import EncryptedAttrs
 
 # This is for sample entry creation.
 from lib389.configurations import get_sample_entries
@@ -581,6 +582,12 @@ class Backend(DSLdapObject):
                               replication, not_folded, no_seq_num, include_suffixes, exclude_suffixes)
         return task
 
+    def get_encrypted_attrs(self):
+        """Get Encrypted Attributes(DSLdapObject) for the backend"""
+
+        encrypt_attrs = EncryptedAttrs(instance=self._instance, basedn="cn=encrypted attributes,%s" % self._dn)
+        return encrypt_attrs
+
 
 class Backends(DSLdapObjects):
     """DSLdapObjects that represents DN_LDBM base DN
diff --git a/src/lib389/lib389/encrypted_attributes.py b/src/lib389/lib389/encrypted_attributes.py
new file mode 100644
index 0000000..0dcece2
--- /dev/null
+++ b/src/lib389/lib389/encrypted_attributes.py
@@ -0,0 +1,50 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import ldap
+import sys
+from lib389._constants import *
+from lib389.properties import *
+from lib389._mapped_object import DSLdapObjects, DSLdapObject
+
+
+class EncryptedAttr(DSLdapObject):
+    """Encrypted Attribute DSLdapObject with:
+    - must attributes = ['cn', 'nsEncryptionAlgorithm']
+    - RDN attribute is 'cn'
+
+    :param instance: An instance
+    :type instance: lib389.DirSrv
+    :param dn: Encrypted Attribute DN
+    :type dn: str
+    """
+
+    def __init__(self, instance, dn=None):
+        super(EncryptedAttr, self).__init__(instance, dn)
+        self._rdn_attribute = 'cn'
+        self._must_attributes = ['cn', 'nsEncryptionAlgorithm']
+        self._create_objectclasses = ['top', 'nsAttributeEncryption']
+        self._protected = False
+        self._lint_functions = []
+
+
+class EncryptedAttrs(DSLdapObjects):
+    """DSLdapObjects that represents Encrypted Attribute
+
+    :param instance: An instance
+    :type instance: lib389.DirSrv
+    :param basedn: DN of suffix container.
+    :type basedn: str
+    """
+
+    def __init__(self, instance, basedn):
+        super(EncryptedAttrs, self).__init__(instance=instance)
+        self._objectclasses = ['nsAttributeEncryption']
+        self._filterattrs = ['cn']
+        self._childobject = EncryptedAttr
+        self._basedn = basedn
\ No newline at end of file

