diff --git a/CHANGELOG.md b/CHANGELOG.md
index 99ddc612c19d31460479524065a7f800a2fef1ea..0d5a5ac2ef93edca05c8e977b4ebb99f0dd3008e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,10 +5,12 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
-## [Unreleased] ##
+## [Unreleased]
 
 ### Added ###
 
+- New function in apiutils that copies an Entity.
+
 ### Changed ###
 
 - Added additional customization options to the plantuml module.
@@ -20,8 +22,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Fixed ###
 
+* [#75](https://gitlab.indiscale.com/caosdb/src/caosdb-pylib/-/issues/75), [#103](https://gitlab.indiscale.com/caosdb/src/caosdb-pylib/-/issues/103) Fixed JSON schema to allow more sections, and correct requirements for
+  password method.
+
 ### Security ###
 
+### Documentation ###
+
+
+## [0.7.2] - 2022-03-25 ##
+(Timm Fitschen)
+
+### Added ###
+
+### Changed ###
+
+### Deprecated ###
+
+* In module `caosdb.apiutils`:
+  * `CaosDBPythonEntity` class
+  * `convert_to_entity` function
+  * `convert_to_python_object` function
+
+### Removed ###
+
+### Fixed ###
+
+* [caosdb-pylib#106](https://gitlab.indiscale.com/caosdb/src/caosdb-pylib/-/issues/106)
+  Parsing Error in class caosdb.common.models.ACL. This may lead to the
+  unintentional revocation of permissions for some users or roles during
+  updates. However, no additional permissions are being granted.
+
+### Security ###
+
+### Documentation ###
+
+## [0.7.1] - 2022-03-11 ##
+(Daniel Hornung)
+
+### Documentation ###
+
+- `timeout` option in example pycaosdb.ini
+
 ## [0.7.0] - 2022-01-21 ##
 
 ### Added ###
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
index 04e783a7fa31b1d5c3a600a1009c8f040db1620d..d6f91cb9e15982bd1b7b0366f94bd9b37286f85a 100644
--- a/DEPENDENCIES.md
+++ b/DEPENDENCIES.md
@@ -1,6 +1,5 @@
-* caosdb-server == 0.3
-* Python >= 3.5
+* caosdb-server >= 0.7.2
+* Python >= 3.6
 * pip >= 20.0.2
 
-
-Any other dependencies are being installed via pip
+Any other dependencies are defined in the setup.py and are being installed via pip
diff --git a/README_SETUP.md b/README_SETUP.md
index e58f934ceba176e4b5ba42239565f8e3bd48171a..dc667da8aa5877132c1212d2ddd2827e85992118 100644
--- a/README_SETUP.md
+++ b/README_SETUP.md
@@ -82,60 +82,8 @@ pip3 install --user .[jsonschema]
 
 ## Configuration ##
 
-The  configuration is done using `ini` configuration files.
-PyCaosDB tries to read from the inifile specified in the environment variable `PYCAOSDBINI` or
-alternatively in `~/.pycaosdb.ini` upon import.  After that, the ini file `pycaosdb.ini` in the
-current working directory will be read additionally, if it exists.
-
-Here, we will look at the most common configuration options. For a full and 
-comprehensive description please check out 
-[pycaosdb.ini file](https://gitlab.com/caosdb/caosdb-pylib/-/blob/main/examples/pycaosdb.ini) 
-You can download this file and use it as a starting point.
-
-
-Typically, you need to change at least the `url` and `username` fields as required. 
-(Ask your CaosDB administrator or IT crowd if
-you do not know what to put there, but for the demo instances https://demo.indiscale.com, `username=admin`
-and `password=caosdb` should work).
-
-### Authentication ###
-
-The default configuration (that your are asked for your password when ever a connection is created
-can be changed by setting `password_method`:
-
-* with `password_method=input` password (and possibly user) will be queried on demand (**default**)
-* use the password manager [pass](https://www.passwordstore.org) by using `pass` as value, see also the [ArchWiki
-  entry](https://wiki.archlinux.org/index.php/Pass#Basic_usage). This also requires `password_identifier` which refers to the identifier within pass
-  for the desired password.
-* install the python package [keyring](https://pypi.org/project/keyring), to use the system keyring/wallet (macOS, GNOME, KDE,
-  Windows). The password will be queried on first usage.
-* with `password_method=plain` (**strongly discouraged**)
-
-The following illustrates the recommended options:
-
-```ini
-[Connection]
-# using "pass" password manager
-#password_method=pass
-#password_identifier=...
-
-# using the system keyring/wallet (macOS, GNOME, KDE, Windows)
-#password_method=keyring
-```
-
-### SSL Certificate ###
-In some cases (especially if you are testing CaosDB) you might need to supply 
-an SSL certificate to allow SSL encryption.
-
-```ini
-[Connection]
-cacert=/path/to/caosdb.ca.pem
-```
-
-### Further Settings ###
-As mentioned above, a complete list of options can be found in the 
-[pycaosdb.ini file](https://gitlab.com/caosdb/caosdb-pylib/-/blob/main/examples/pycaosdb.ini) in 
-the examples folder of the source code.
+The configuration is done using `ini` configuration files.  The content of these configuration files
+is described in detail in the [configuration section of the documentation](https://docs.indiscale.com/caosdb-pylib/configuration.html).
 
 ## Try it out ##
 
@@ -155,7 +103,10 @@ like this, check out the "Authentication" section in the [configuration document
 Now would be a good time to continue with the [tutorials](tutorials/index).
 
 ## Run Unit Tests
-tox
+
+- Run all tests: `tox` or `make unittest`
+- Run a specific test file: e.g. `tox -- unittests/test_schema.py`
+- Run a specific test function: e.g. `tox -- unittests/test_schema.py::test_config_files`
 
 ## Documentation ##
 
diff --git a/RELEASE_GUIDELINES.md b/RELEASE_GUIDELINES.md
index e71234b8e2bc95f954ffbebdc26acf6edd8e0b2d..b4e38d643756798f0ba8b07d6eceec529cbb3054 100644
--- a/RELEASE_GUIDELINES.md
+++ b/RELEASE_GUIDELINES.md
@@ -36,8 +36,10 @@ guidelines of the CaosDB Project
 9. Publish the release by executing `./release.sh` with uploads the caosdb
    module to the Python Package Index [pypi.org](https://pypi.org).
 
-10. Merge the main branch back into the dev branch.
+10. Create a gitlab release on gitlab.indiscale.com and gitlab.com
 
-11. After the merge of main to dev, start a new development version by
+11. Merge the main branch back into the dev branch.
+
+12. After the merge of main to dev, start a new development version by
     setting `ISRELEASED` to `False` and by increasing at least the `MICRO`
     version in [setup.py](./setup.py) and preparing CHANGELOG.md.
diff --git a/examples/pycaosdb.ini b/examples/pycaosdb.ini
index edc32195fbb364bb355d67b8733e8c7bccbb0d34..8cf74e43c5db32ed139c4fe371a6c2b3831b2ee1 100644
--- a/examples/pycaosdb.ini
+++ b/examples/pycaosdb.ini
@@ -67,3 +67,6 @@
 
 # This option is used internally and for testing. Do not override.
 # implementation=_DefaultCaosDBServerConnection
+
+# The timeout for requests to the server.
+# timeout=1000
diff --git a/setup.py b/setup.py
index 310d77786949a9bc2982936457a90d73047f2b0c..def734ac1b5b4a648df58a6904d51835229dbb86 100755
--- a/setup.py
+++ b/setup.py
@@ -47,9 +47,13 @@ from setuptools import find_packages, setup
 
 ISRELEASED = False
 MAJOR = 0
-MINOR = 7
-MICRO = 1
-PRE = ""  # e.g. rc0, alpha.1, 0.beta-23
+MINOR = 8
+MICRO = 0
+# Do not tag as pre-release until this commit
+# https://github.com/pypa/packaging/pull/515
+# has made it into a release. Probably we should wait for pypa/packaging>=21.4
+# https://github.com/pypa/packaging/releases
+PRE = "" # "dev"  # e.g. rc0, alpha.1, 0.beta-23
 
 if PRE:
     VERSION = "{}.{}.{}-{}".format(MAJOR, MINOR, MICRO, PRE)
@@ -166,12 +170,13 @@ def setup_package():
         packages=find_packages('src'),
         python_requires='>=3.6',
         package_dir={'': 'src'},
-        install_requires=['lxml>=3.6.4',
-                          'PyYaml>=3.12', 'future', 'PySocks>=1.6.7'],
+        install_requires=['lxml>=4.6.3',
+                          'PyYAML>=6.0', 'future', 'PySocks>=1.6.7'],
         extras_require={'keyring': ['keyring>=13.0.0'],
-                        'jsonschema': ['jsonschema==4.0.1']},
+                        'jsonschema': ['jsonschema>=4.4.0']},
         setup_requires=["pytest-runner>=2.0,<3dev"],
-        tests_require=["pytest", "pytest-cov", "coverage>=4.4.2", "jsonschema==4.0.1"],
+        tests_require=["pytest", "pytest-cov", "coverage>=4.4.2",
+                       "jsonschema>=4.4.0"],
         package_data={
             'caosdb': ['cert/indiscale.ca.crt', 'schema-pycaosdb-ini.yml'],
         },
diff --git a/src/caosdb/apiutils.py b/src/caosdb/apiutils.py
index dbab28f963d4d167c4dfc097e25527dfc0baad50..08f31daad56c0ab471322197cadc1a1378267f35 100644
--- a/src/caosdb/apiutils.py
+++ b/src/caosdb/apiutils.py
@@ -37,7 +37,9 @@ from caosdb.common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER,
                                     REFERENCE, TEXT, is_reference)
 from caosdb.common.models import (Container, Entity, File, Property, Query,
                                   Record, RecordType, execute_query,
-                                  get_config)
+                                  get_config, SPECIAL_ATTRIBUTES)
+
+import logging
 
 
 def new_record(record_type, name=None, description=None,
@@ -141,6 +143,9 @@ class CaosDBPythonEntity(object):
     _last_id = 0
 
     def __init__(self):
+        warnings.warn("The CaosDBPythonEntity class is deprecated, replacements will be provided by"
+                      " the high_level_api module.",
+                      DeprecationWarning, stacklevel=2)
         # Save a copy of the dry state
         # of this object in order to be
         # able to detect conflicts.
@@ -381,10 +386,7 @@ def _single_convert_to_python_object(robj, entity):
     return robj
 
 
-def _single_convert_to_entity(entity, robj, recursive_depth, **kwargs):
-    """
-    recursive_depth: disabled if 0
-    """
+def _single_convert_to_entity(entity, robj, **kwargs):
     if robj._id is not None:
         entity.id = robj._id
 
@@ -410,16 +412,16 @@ def _single_convert_to_entity(entity, robj, recursive_depth, **kwargs):
             else:
                 entity.add_parent(id=parent)
 
-    def add_property(entity, prop, name, _recursive=False, datatype=None):
+    def add_property(entity, prop, name, recursive=False, datatype=None):
         if datatype is None:
-            raise RuntimeError("Datatype must not be None.")
+            raise ArgumentError("datatype must not be None")
 
         if isinstance(prop, CaosDBPythonEntity):
             entity.add_property(name=name, value=str(
                 prop._id), datatype=datatype)
 
-            if _recursive and not prop.do_not_expand:
-                return convert_to_entity(prop, recursive=_recursive)
+            if recursive and not prop.do_not_expand:
+                return convert_to_entity(prop, recursive=recursive)
             else:
                 return []
         else:
@@ -429,11 +431,6 @@ def _single_convert_to_entity(entity, robj, recursive_depth, **kwargs):
 
             return []
 
-    if recursive_depth == 0:
-        recursive = False
-    else:
-        recursive = True
-
     for prop in robj._properties:
         value = robj.__getattribute__(prop)
 
@@ -447,7 +444,7 @@ def _single_convert_to_entity(entity, robj, recursive_depth, **kwargs):
 
                         if recursive and not v.do_not_expand:
                             children.append(convert_to_entity(
-                                v, recursive=recursive_depth-1))
+                                v, recursive=recursive))
                     else:
                         if isinstance(v, float) or isinstance(v, int):
                             lst.append(str(v))
@@ -477,6 +474,9 @@ def _single_convert_to_entity(entity, robj, recursive_depth, **kwargs):
 
 
 def convert_to_entity(python_object, **kwargs):
+    warnings.warn("The convert_to_entity function is deprecated, replacement will be provided by "
+                  "the high_level_api module.", DeprecationWarning, stacklevel=2)
+
     if isinstance(python_object, Container):
         # Create a list of objects:
 
@@ -498,6 +498,8 @@ def convert_to_entity(python_object, **kwargs):
 def convert_to_python_object(entity):
     """"""
 
+    warnings.warn("The convert_to_python_object function is deprecated, replacement will be "
+                  "provided by the high_level_api module.", DeprecationWarning, stacklevel=2)
     if isinstance(entity, Container):
         # Create a list of objects:
 
@@ -565,10 +567,6 @@ def getCommitIn(folder):
         return t.readline().strip()
 
 
-COMPARED = ["name", "role", "datatype", "description", "importance",
-            "id", "path", "checksum", "size"]
-
-
 def compare_entities(old_entity: Entity, new_entity: Entity):
     """
     Compare two entites.
@@ -592,7 +590,7 @@ def compare_entities(old_entity: Entity, new_entity: Entity):
     if old_entity is new_entity:
         return (olddiff, newdiff)
 
-    for attr in COMPARED:
+    for attr in SPECIAL_ATTRIBUTES:
         try:
             oldattr = old_entity.__getattribute__(attr)
             old_entity_attr_exists = True
@@ -681,10 +679,77 @@ def compare_entities(old_entity: Entity, new_entity: Entity):
     return (olddiff, newdiff)
 
 
+def merge_entities(entity_a: Entity, entity_b: Entity):
+    """
+    Merge entity_b into entity_a such that they have the same parents and properties.
+
+    datatype, unit, value, name and description will only be changed in entity_a if they
+    are None for entity_a and set for entity_b. If there is a corresponding value
+    for entity_a different from None a RuntimeError will be raised informing of an
+    unresolvable merge conflict.
+
+    The merge operation is done in place.
+
+    Returns entity_a.
+
+    WARNING: This function is currently experimental and insufficiently tested. Use with care.
+    """
+
+    logging.warning(
+        "This function is currently experimental and insufficiently tested. Use with care.")
+
+    # Compare both entities:
+    diff_r1, diff_r2 = compare_entities(entity_a, entity_b)
+
+    # Go through the comparison and try to apply changes to entity_a:
+    for key in diff_r2["parents"]:
+        entity_a.add_parent(entity_b.get_parent(key))
+
+    for key in diff_r2["properties"]:
+        if key in diff_r1["properties"]:
+            if ("importance" in diff_r1["properties"][key] and
+                    "importance" in diff_r2["properties"][key]):
+                if (diff_r1["properties"][key]["importance"] !=
+                        diff_r2["properties"][key]["importance"]):
+                    raise NotImplementedError()
+            elif ("importance" in diff_r1["properties"][key] or
+                  "importance" in diff_r2["properties"][key]):
+                raise NotImplementedError()
+
+            for attribute in ("datatype", "unit", "value"):
+                if diff_r1["properties"][key][attribute] is None:
+                    setattr(entity_a.get_property(key), attribute,
+                            diff_r2["properties"][key][attribute])
+                else:
+                    raise RuntimeError("Merge conflict.")
+        else:
+            # TODO: This is a temporary FIX for
+            #       https://gitlab.indiscale.com/caosdb/src/caosdb-pylib/-/issues/105
+            entity_a.add_property(id=entity_b.get_property(key).id,
+                                  name=entity_b.get_property(key).name,
+                                  datatype=entity_b.get_property(key).datatype,
+                                  value=entity_b.get_property(key).value,
+                                  unit=entity_b.get_property(key).unit,
+                                  importance=entity_b.get_importance(key))
+            # entity_a.add_property(
+            #     entity_b.get_property(key),
+            #     importance=entity_b.get_importance(key))
+
+    for special_attribute in ("name", "description"):
+        sa_a = getattr(entity_a, special_attribute)
+        sa_b = getattr(entity_b, special_attribute)
+        if sa_a != sa_b:
+            if sa_a is None:
+                setattr(entity_a, special_attribute, sa_b)
+            else:
+                raise RuntimeError("Merge conflict.")
+    return entity_a
+
+
 def describe_diff(olddiff, newdiff, name=None, as_update=True):
     description = ""
 
-    for attr in list(set(list(olddiff.keys())+list(newdiff.keys()))):
+    for attr in list(set(list(olddiff.keys()) + list(newdiff.keys()))):
         if attr == "parents" or attr == "properties":
             continue
         description += "{} differs:\n".format(attr)
diff --git a/src/caosdb/common/models.py b/src/caosdb/common/models.py
index 80a6ee11e707fb3776fc96b42a16b649ac575f66..6475bc99ec825e102d5eac1b38d506247c11ebcb 100644
--- a/src/caosdb/common/models.py
+++ b/src/caosdb/common/models.py
@@ -5,9 +5,9 @@
 #
 # Copyright (C) 2018 Research Group Biomedical Physics,
 # Max-Planck-Institute for Dynamics and Self-Organization Göttingen
-# Copyright (C) 2020 Indiscale GmbH <info@indiscale.com>
+# Copyright (C) 2020-2022 Indiscale GmbH <info@indiscale.com>
 # Copyright (C) 2020 Florian Spreckelsen <f.spreckelsen@indiscale.com>
-# Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+# Copyright (C) 2020-2022 Timm Fitschen <t.fitschen@indiscale.com>
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as
@@ -25,7 +25,14 @@
 # ** end header
 #
 
-"""missing docstring."""
+"""
+Collection of the central classes of the CaosDB client, namely the Entity class
+and all of its subclasses and the Container class which is used to carry out
+transactions.
+
+All additional classes are either important for the entities or the
+transactions.
+"""
 from __future__ import print_function, unicode_literals
 
 import re
@@ -72,6 +79,10 @@ ALL = "ALL"
 NONE = "NONE"
 
 
+SPECIAL_ATTRIBUTES = ["name", "role", "datatype", "description",
+                      "id", "path", "checksum", "size"]
+
+
 class Entity(object):
 
     """Entity is a generic CaosDB object.
@@ -114,6 +125,48 @@ class Entity(object):
         self.id = id
         self.state = None
 
+
+    def copy(self):
+        """
+        Return a copy of entity.
+
+        If deep == True return a deep copy, recursively copying all sub entities.
+
+        Standard properties are copied using add_property.
+        Special attributes, as defined by the global variable SPECIAL_ATTRIBUTES and additionaly
+        the "value" are copied using setattr.
+        """
+        if self.role == "File":
+            new = File()
+        elif self.role == "Property":
+            new = Property()
+        elif self.role == "RecordType":
+            new = RecordType()
+        elif self.role == "Record":
+            new = Record()
+        elif self.role == "Entity":
+            new = Entity()
+        else:
+            raise RuntimeError("Unkonwn role.")
+
+        # Copy special attributes:
+        # TODO: this might rise an exception when copying
+        #       special file attributes like checksum and size.
+        for attribute in SPECIAL_ATTRIBUTES + ["value"]:
+            val = getattr(self, attribute)
+            if val is not None:
+                setattr(new, attribute, val)
+
+        # Copy parents:
+        for p in self.parents:
+            new.add_parent(p)
+
+        # Copy properties:
+        for p in self.properties:
+            new.add_property(p, importance=self.get_importance(p))
+
+        return new
+
     @property
     def version(self):
         if self._version is not None or self._wrapped_entity is None:
@@ -269,14 +322,74 @@ class Entity(object):
         self.__pickup = new_pickup
 
     def grant(self, realm=None, username=None, role=None,
-              permission=None, priority=False):
+              permission=None, priority=False, revoke_denial=True):
+        """Grant a permission to a user or role for this entity.
+
+        You must specify either only the username and the realm, or only the
+        role.
+
+        By default a previously existing denial rule would be revoked, because
+        otherwise this grant wouldn't have any effect. However, for keeping
+        contradicting rules pass revoke_denial=False.
+
+        Parameters
+        ----------
+        permission: str
+            The permission to be granted.
+        username : str, optional
+            The username. Exactly one is required, either the `username` or the
+            `role`.
+        realm: str, optional
+            The user's realm. Required when username is not None.
+        role: str, optional
+            The role (as in Role-Based Access Control). Exactly one is
+            required, either the `username` or the `role`.
+        priority: bool, default False
+            Whether this permission is granted with priority over non-priority
+            rules.
+        revoke_denial: bool, default True
+            Whether a contradicting denial (with same priority flag) in this
+            ACL will be revoked.
+        """
+        # @review Florian Spreckelsen 2022-03-17
         self.acl.grant(realm=realm, username=username, role=role,
-                       permission=permission, priority=priority)
+                       permission=permission, priority=priority,
+                       revoke_denial=revoke_denial)
 
     def deny(self, realm=None, username=None, role=None,
-             permission=None, priority=False):
+             permission=None, priority=False, revoke_grant=True):
+        """Deny a permission to a user or role for this entity.
+
+        You must specify either only the username and the realm, or only the
+        role.
+
+        By default a previously existing grant rule would be revoked, because
+        otherwise this denial would override the grant rules anyways. However,
+        for keeping contradicting rules pass revoke_grant=False.
+
+        Parameters
+        ----------
+        permission: str
+            The permission to be denied.
+        username : str, optional
+            The username. Exactly one is required, either the `username` or the
+            `role`.
+        realm: str, optional
+            The user's realm. Required when username is not None.
+        role: str, optional
+            The role (as in Role-Based Access Control). Exactly one is
+            required, either the `username` or the `role`.
+        priority: bool, default False
+            Whether this permission is denied with priority over non-priority
+            rules.
+        revoke_grant: bool, default True
+            Whether a contradicting grant (with same priority flag) in this
+            ACL will be revoked.
+        """
+        # @review Florian Spreckelsen 2022-03-17
         self.acl.deny(realm=realm, username=username, role=role,
-                      permission=permission, priority=priority)
+                      permission=permission, priority=priority,
+                      revoke_grant=revoke_grant)
 
     def revoke_denial(self, realm=None, username=None,
                       role=None, permission=None, priority=False):
@@ -3636,13 +3749,15 @@ class ACI():
         self.permission = permission
 
     def __hash__(self):
-        return hash(str(self.realm) + ":" + str(self.username) +
-                    ":" + str(self.role) + ":" + str(self.permission))
+        return hash(self.__repr__())
 
     def __eq__(self, other):
         return isinstance(other, ACI) and (self.role is None and self.username == other.username and self.realm ==
                                            other.realm) or self.role == other.role and self.permission == other.permission
 
+    def __repr__(self):
+        return str(self.realm) + ":" + str(self.username) + ":" + str(self.role) + ":" + str(self.permission)
+
     def add_to_element(self, e):
         if self.role is not None:
             e.set("role", self.role)
@@ -3667,10 +3782,35 @@ class ACL():
             self.clear()
 
     def parse_xml(self, xml):
+        """Clear this ACL and parse the xml.
+
+        Iterate over the rules in the xml and add each rule to this ACL.
+
+        Contradicting rules will both be kept.
+
+        Parameters
+        ----------
+        xml : lxml.etree.Element
+            The xml element containing the ACL rules, i.e. <Grant> and <Deny>
+            rules.
+        """
         self.clear()
         self._parse_xml(xml)
 
     def _parse_xml(self, xml):
+        """Parse the xml.
+
+        Iterate over the rules in the xml and add each rule to this ACL.
+
+        Contradicting rules will both be kept.
+
+        Parameters
+        ----------
+        xml : lxml.etree.Element
+            The xml element containing the ACL rules, i.e. <Grant> and <Deny>
+            rules.
+        """
+        # @review Florian Spreckelsen 2022-03-17
         for e in xml:
             role = e.get("role")
             username = e.get("username")
@@ -3683,10 +3823,12 @@ class ACL():
 
                     if e.tag == "Grant":
                         self.grant(username=username, realm=realm, role=role,
-                                   permission=permission, priority=priority)
+                                   permission=permission, priority=priority,
+                                   revoke_denial=False)
                     elif e.tag == "Deny":
                         self.deny(username=username, realm=realm, role=role,
-                                  permission=permission, priority=priority)
+                                  permission=permission, priority=priority,
+                                  revoke_grant=False)
 
     def combine(self, other):
         """ Combine and return new instance."""
@@ -3764,12 +3906,42 @@ class ACL():
         if item in self._denials:
             self._denials.remove(item)
 
-    def grant(self, username=None, realm=None, role=None,
-              permission=None, priority=False):
+    def grant(self, permission, username=None, realm=None, role=None,
+              priority=False, revoke_denial=True):
+        """Grant a permission to a user or role.
+
+        You must specify either only the username and the realm, or only the
+        role.
+
+        By default a previously existing denial rule would be revoked, because
+        otherwise this grant wouldn't have any effect. However, for keeping
+        contradicting rules pass revoke_denial=False.
+
+        Parameters
+        ----------
+        permission: str
+            The permission to be granted.
+        username : str, optional
+            The username. Exactly one is required, either the `username` or the
+            `role`.
+        realm: str, optional
+            The user's realm. Required when username is not None.
+        role: str, optional
+            The role (as in Role-Based Access Control). Exactly one is
+            required, either the `username` or the `role`.
+        priority: bool, default False
+            Whether this permission is granted with priority over non-priority
+            rules.
+        revoke_denial: bool, default True
+            Whether a contradicting denial (with same priority flag) in this
+            ACL will be revoked.
+        """
+        # @review Florian Spreckelsen 2022-03-17
         priority = self._get_boolean_priority(priority)
         item = ACI(role=role, username=username,
                    realm=realm, permission=permission)
-        self._remove_item(item, priority)
+        if revoke_denial:
+            self._remove_item(item, priority)
 
         if priority is True:
             self._priority_grants.add(item)
@@ -3777,11 +3949,41 @@ class ACL():
             self._grants.add(item)
 
     def deny(self, username=None, realm=None, role=None,
-             permission=None, priority=False):
+             permission=None, priority=False, revoke_grant=True):
+        """Deny a permission to a user or role for this entity.
+
+        You must specify either only the username and the realm, or only the
+        role.
+
+        By default a previously existing grant rule would be revoked, because
+        otherwise this denial would override the grant rules anyways. However,
+        for keeping contradicting rules pass revoke_grant=False.
+
+        Parameters
+        ----------
+        permission: str
+            The permission to be denied.
+        username : str, optional
+            The username. Exactly one is required, either the `username` or the
+            `role`.
+        realm: str, optional
+            The user's realm. Required when username is not None.
+        role: str, optional
+            The role (as in Role-Based Access Control). Exactly one is
+            required, either the `username` or the `role`.
+        priority: bool, default False
+            Whether this permission is denied with priority over non-priority
+            rules.
+        revoke_grant: bool, default True
+            Whether a contradicting grant (with same priority flag) in this
+            ACL will be revoked.
+        """
+        # @review Florian Spreckelsen 2022-03-17
         priority = self._get_boolean_priority(priority)
         item = ACI(role=role, username=username,
                    realm=realm, permission=permission)
-        self._remove_item(item, priority)
+        if revoke_grant:
+            self._remove_item(item, priority)
 
         if priority is True:
             self._priority_denials.add(item)
diff --git a/src/caosdb/configuration.py b/src/caosdb/configuration.py
index 51e3749aaca3045afec9334ef987a174d5d19f26..75827df0d00d6c82251c2c04fa47413ac2801928 100644
--- a/src/caosdb/configuration.py
+++ b/src/caosdb/configuration.py
@@ -84,23 +84,28 @@ def config_to_yaml(config):
 
 
 def validate_yaml_schema(valobj):
-    # TODO: Re-enable warning once the schema has been extended to also cover
-    # SSS pycaosdb.inis and integration tests.
     if optional_jsonschema_validate:
         with open(os.path.join(os.path.dirname(__file__), "schema-pycaosdb-ini.yml")) as f:
             schema = yaml.load(f, Loader=yaml.SafeLoader)
         optional_jsonschema_validate(instance=valobj, schema=schema["schema-pycaosdb-ini"])
-    # else:
-    #     warnings.warn("""
-    #                     Warning: The validation could not be performed because `jsonschema` is not installed.
-    #                 """)
+    else:
+        warnings.warn("""
+            Warning: The validation could not be performed because `jsonschema` is not installed.
+        """)
 
 
 def _read_config_files():
-    """Function to read config files from different paths. Checks for path in $PYCAOSDBINI or home directory (.pycaosdb.ini) and in the current working directory (pycaosdb.ini).
+    """Function to read config files from different paths.
+
+    Checks for path either in ``$PYCAOSDBINI`` or home directory (``.pycaosdb.ini``), and
+    additionally in the current working directory (``pycaosdb.ini``).
+
+    Returns
+    -------
+
+    ini files: list
+      The successfully parsed ini-files. Order: env_var or home directory, cwd. Used for testing the function.
 
-    Returns:
-        [list]: list with successfully parsed ini-files. Order: env_var or home directory, cwd. Used for testing the function.
     """
     return_var = []
     if "PYCAOSDBINI" in environ:
diff --git a/src/caosdb/schema-pycaosdb-ini.yml b/src/caosdb/schema-pycaosdb-ini.yml
index bfe8fe7c63679507bba795bb45d7afa2b097f07b..5dabdd89795e19a757209e03cc843776be705777 100644
--- a/src/caosdb/schema-pycaosdb-ini.yml
+++ b/src/caosdb/schema-pycaosdb-ini.yml
@@ -65,26 +65,39 @@ schema-pycaosdb-ini:
             properties:
               password_method:
                 const: input
+            required: [password_method]
           then:
             required: [url]
         - if:
             properties:
               password_method:
                 const: plain
+            required: [password_method]
           then:
             required: [url, username, password]
         - if:
             properties:
               password_method:
                 const: pass
+            required: [password_method]
           then:
             required: [url, username, password_identifier]
         - if:
             properties:
               password_method:
                 const: keyring
+            required: [password_method]
           then:
             required: [url, username]
     IntegrationTests:
       description: "Used by the integration test suite from the caosdb-pyinttest repo."
       additionalProperties: true
+    Misc:
+      description: "Some additional configuration settings."
+      additionalProperties: true
+    advancedtools:
+      description: "Configuration settings for the caosadvancedtools."
+      additionalProperties: true
+    sss_helper:
+      description: "Configuration settings for server-side scripting."
+      additionalProperties: true
diff --git a/src/doc/conf.py b/src/doc/conf.py
index b05fa1c71c1dcd0b59916594818449d2ebc574bd..ce1cfd261cb8b8d5aac8022d969c765b1c45fae3 100644
--- a/src/doc/conf.py
+++ b/src/doc/conf.py
@@ -25,14 +25,14 @@ import sphinx_rtd_theme  # noqa: E402
 # -- Project information -----------------------------------------------------
 
 project = 'pycaosdb'
-copyright = '2020, IndiScale GmbH'
+copyright = '2022, IndiScale GmbH'
 author = 'Daniel Hornung'
 
 # The short X.Y version
-version = '0.5.2'
+version = '0.8.0'
 # The full version, including alpha/beta/rc tags
 # release = '0.5.2-rc2'
-release = '0.5.2'
+release = '0.8.0-dev'
 
 
 # -- General configuration ---------------------------------------------------
diff --git a/src/doc/configuration.md b/src/doc/configuration.md
index 6e53542f661dcae94622fef24a67cecf7491df9c..02cbbd7b13d916a676ad26c277e370ae76bf3725 100644
--- a/src/doc/configuration.md
+++ b/src/doc/configuration.md
@@ -4,6 +4,15 @@ PyCaosDB tries to read from the inifile specified in the environment variable `P
 alternatively in `~/.pycaosdb.ini` upon import.  After that, the ini file `pycaosdb.ini` in the
 current working directory will be read additionally, if it exists.
 
+Here, we will look at the most common configuration options. For a full and comprehensive
+description please check out the [example pycaosdb.ini file](https://gitlab.com/caosdb/caosdb-pylib/-/blob/main/examples/pycaosdb.ini).  You can download this file and use
+it as a starting point.
+
+
+Typically, you need to change at least the `url` and `username` fields as required.  (Ask your
+CaosDB administrator or IT crowd if you do not know what to put there, but for the demo instance at
+https://demo.indiscale.com, `username=admin` and `password=caosdb` should work).
+
 ## Authentication ##
 
 The default configuration (that your are asked for your password when ever a connection is created
@@ -17,6 +26,8 @@ can be changed by setting `password_method`:
   Windows). The password will be queried on first usage.
 * with `password_method=plain` (**strongly discouraged**)
 
+The following illustrates the recommended options:
+
 ```ini
 [Connection]
 username=YOUR_USERNAME
@@ -35,7 +46,10 @@ username=YOUR_USERNAME
 
 ## SSL Certificate ##
 
-You can set the pass to the ssl certificate to be used:
+In some cases (especially if you are testing CaosDB) you might need to supply an SSL certificate to
+allow SSL encryption.
+
+The `cacert` option sets the path to the ssl certificate for the connection:
 
 ```ini
 [Connection]
@@ -49,6 +63,8 @@ with CaosDB which makes the experience much less verbose. Set it to 1 or 2 in ca
 debugging (which I hope will not be necessary for this tutorial) or if you want to learn more about
 the internals of the protocol. 
 
+`timeout` sets the timeout for requests to the server.
+
 A complete list of options can be found in the 
 [pycaosdb.ini file](https://gitlab.com/caosdb/caosdb-pylib/-/blob/main/examples/pycaosdb.ini) in 
 the examples folder of the source code.
diff --git a/tox.ini b/tox.ini
index b1061a57c6a136cb29f77a1d0c03383ab82ecf8b..0d245e03ef9c8fe2151e173cd1a10964d47ef82b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,3 +9,6 @@ deps = .
     pytest-cov
     jsonschema==4.0.1
 commands=py.test --cov=caosdb -vv {posargs}
+
+[flake8]
+max-line-length=100
diff --git a/unittests/test_acl.py b/unittests/test_acl.py
new file mode 100644
index 0000000000000000000000000000000000000000..633c25ad5c4046c0fa41b66049bdf56aa695f482
--- /dev/null
+++ b/unittests/test_acl.py
@@ -0,0 +1,55 @@
+# -*- encoding: utf-8 -*-
+#
+# This file is a part of the CaosDB Project.
+#
+# Copyright (C) 2022 Indiscale GmbH <info@indiscale.com>
+# Copyright (C) 2022 Timm Fitschen <f.fitschen@indiscale.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+import caosdb as db
+from lxml import etree
+
+
+def test_parse_xml():
+    # @review Florian Spreckelsen 2022-03-17
+    xml_str = """
+        <EntityACL>
+          <Grant priority="False" role="role1">
+            <Permission name="RETRIEVE:ENTITY"/>
+          </Grant>
+          <Deny priority="False" role="role1">
+            <Permission name="RETRIEVE:ENTITY"/>
+          </Deny>
+          <Grant priority="True" role="role1">
+            <Permission name="RETRIEVE:ENTITY"/>
+          </Grant>
+          <Deny priority="True" role="role1">
+            <Permission name="RETRIEVE:ENTITY"/>
+          </Deny>
+        </EntityACL>"""
+    xml = etree.fromstring(xml_str)
+    left_acl = db.ACL(xml)
+
+    right_acl = db.ACL()
+    right_acl.grant(role="role1", permission="RETRIEVE:ENTITY",
+                    revoke_denial=False)
+    right_acl.deny(role="role1", permission="RETRIEVE:ENTITY",
+                   revoke_grant=False)
+    right_acl.grant(role="role1", permission="RETRIEVE:ENTITY",
+                    priority=True, revoke_denial=False)
+    right_acl.deny(role="role1", permission="RETRIEVE:ENTITY",
+                   priority=True, revoke_grant=False)
+
+    assert left_acl == right_acl
diff --git a/unittests/test_apiutils.py b/unittests/test_apiutils.py
index 0294646f6c526230a8e9fb722d56aa23a8f9285c..13603f4caae8b1212ffa041f37d9be4b462223ff 100644
--- a/unittests/test_apiutils.py
+++ b/unittests/test_apiutils.py
@@ -31,10 +31,14 @@ import tempfile
 import caosdb as db
 import caosdb.apiutils
 from caosdb.apiutils import (apply_to_ids, compare_entities, create_id_query,
-                             resolve_reference)
+                             resolve_reference, merge_entities)
+
+from caosdb.common.models import SPECIAL_ATTRIBUTES
 
 from .test_property import testrecord
 
+import pytest
+
 
 def test_convert_object():
     r2 = db.apiutils.convert_to_python_object(testrecord)
@@ -230,3 +234,92 @@ def test_compare_special_properties():
             assert diff_r2[key] == 2
         assert len(diff_r1["properties"]) == 0
         assert len(diff_r2["properties"]) == 0
+
+
+def test_copy_entities():
+    r = db.Record(name="A")
+    r.add_parent(name="B")
+    r.add_property(name="C", value=4, importance="OBLIGATORY")
+    r.add_property(name="D", value=[3, 4, 7], importance="OBLIGATORY")
+    r.description = "A fancy test record"
+
+    c = r.copy()
+
+    assert c is not r
+    assert c.name == "A"
+    assert c.role == r.role
+    assert c.parents[0].name == "B"
+    # parent and property objects are not shared among copy and original:
+    assert c.parents[0] is not r.parents[0]
+
+    for i in [0, 1]:
+        assert c.properties[i] is not r.properties[i]
+        for special in SPECIAL_ATTRIBUTES:
+            assert getattr(c.properties[i], special) == getattr(r.properties[i], special)
+        assert c.get_importance(c.properties[i]) == r.get_importance(r.properties[i])
+
+
+def test_merge_entities():
+    r = db.Record(name="A")
+    r.add_parent(name="B")
+    r.add_property(name="C", value=4, importance="OBLIGATORY")
+    r.add_property(name="D", value=[3, 4, 7], importance="OBLIGATORY")
+    r.description = "A fancy test record"
+
+    r2 = db.Record()
+    r2.add_property(name="F", value="text")
+    merge_entities(r2, r)
+    assert r2.get_parents()[0].name == "B"
+    assert r2.get_property("C").name == "C"
+    assert r2.get_property("C").value == 4
+    assert r2.get_property("D").name == "D"
+    assert r2.get_property("D").value == [3, 4, 7]
+
+    assert r2.get_property("F").name == "F"
+    assert r2.get_property("F").value == "text"
+
+
+def test_merge_bug_109():
+    rt = db.RecordType(name="TestBug")
+    p = db.Property(name="test_bug_property", datatype=db.LIST(db.INTEGER))
+
+    r_b = db.Record(name="TestRecord")
+    r_b.add_parent(rt)
+    r_b.add_property(p, value=[18, 19])
+
+    r_a = db.Record(name="TestRecord")
+    r_a.add_parent(rt)
+
+    merge_entities(r_a, r_b)
+
+    assert r_b.get_property("test_bug_property").value == [18, 19]
+    assert r_a.get_property("test_bug_property").value == [18, 19]
+
+    assert "<Value>18</Value>\n    <Value>19</Value>" in str(r_b)
+    assert "<Value>18</Value>\n    <Value>19</Value>\n    <Value>18</Value>\n    <Value>19</Value>" not in str(r_b)
+
+    assert "<Value>18</Value>\n    <Value>19</Value>" in str(r_a)
+    assert "<Value>18</Value>\n    <Value>19</Value>\n    <Value>18</Value>\n    <Value>19</Value>" not in str(r_a)
+
+
+@pytest.mark.xfail
+def test_bug_109():
+    rt = db.RecordType(name="TestBug")
+    p = db.Property(name="test_bug_property", datatype=db.LIST(db.INTEGER))
+
+    r_b = db.Record(name="TestRecord")
+    r_b.add_parent(rt)
+    r_b.add_property(p, value=[18, 19])
+
+    r_a = db.Record(name="TestRecord")
+    r_a.add_parent(rt)
+    r_a.add_property(r_b.get_property("test_bug_property"))
+
+    assert r_b.get_property("test_bug_property").value == [18, 19]
+    assert r_a.get_property("test_bug_property").value == [18, 19]
+
+    assert "<Value>18</Value>\n    <Value>19</Value>" in str(r_b)
+    assert "<Value>18</Value>\n    <Value>19</Value>\n    <Value>18</Value>\n    <Value>19</Value>" not in str(r_b)
+
+    assert "<Value>18</Value>\n    <Value>19</Value>" in str(r_a)
+    assert "<Value>18</Value>\n    <Value>19</Value>\n    <Value>18</Value>\n    <Value>19</Value>" not in str(r_a)
diff --git a/unittests/test_configs/pycaosdb-IntegrationTests.ini b/unittests/test_configs/pycaosdb-IntegrationTests.ini
new file mode 100644
index 0000000000000000000000000000000000000000..cb9871708f7f23c489de0cbc8f4fbda15dfa6ad0
--- /dev/null
+++ b/unittests/test_configs/pycaosdb-IntegrationTests.ini
@@ -0,0 +1,37 @@
+# -*- mode:conf; -*-
+## This sections needs to exist in addition to the usual section
+[IntegrationTests]
+# test_server_side_scripting.bin_dir.local=/path/to/scripting/bin
+test_server_side_scripting.bin_dir.local=/home/myself/test/caosdb-server/scripting/bin
+# test_server_side_scripting.bin_dir.server=/opt/caosdb/git/caosdb-server/scripting/bin
+
+# # location of the files from the pyinttest perspective
+# test_files.test_insert_files_in_dir.local=/extroot/test_insert_files_in_dir/
+test_files.test_insert_files_in_dir.local=/home/myself/test/debug_advanced/paths/extroot/test_insert_files_in_dir
+# # location of the files from the caosdb_servers perspective
+test_files.test_insert_files_in_dir.server=/opt/caosdb/mnt/extroot/test_insert_files_in_dir/
+
+########## Files ##################
+## Used by tests of file handling. Specify the path to an existing
+## directory in which file tests are performed, once as seen by the
+## host and once as seen by the server.
+
+# location of the files from the pyinttest (i.e. host) perspective
+#test_files.test_insert_files_in_dir.local=/extroot/test_insert_files_in_dir/
+
+# location of the files from the caosdb server's perspective
+#test_files.test_insert_files_in_dir.server=/opt/caosdb/mnt/extroot/test_insert_files_in_dir/
+
+# # location of the one-time tokens from the pyinttest's perspective
+# test_authentication.admin_token_crud = /authtoken/admin_token_crud.txt
+# test_authentication.admin_token_expired = /authtoken/admin_token_expired.txt
+# test_authentication.admin_token_3_attempts = /authtoken/admin_token_3_attempts.txt
+
+
+## Insert your usual settings here
+[Connection]
+url=https://localhost:10443/
+username=admin
+password_method=plain
+password=caosdb
+
diff --git a/unittests/test_configs/pycaosdb-empty.ini b/unittests/test_configs/pycaosdb-empty.ini
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/unittests/test_configs/pycaosdb-real-world-1.ini b/unittests/test_configs/pycaosdb-real-world-1.ini
new file mode 100644
index 0000000000000000000000000000000000000000..e524f1d3465c61d89ae4a4dda54536a722f99837
--- /dev/null
+++ b/unittests/test_configs/pycaosdb-real-world-1.ini
@@ -0,0 +1,17 @@
+[Connection]
+url = https://localhost:10443
+cacert = /opt/caosdb/cert/caosdb.cert.pem
+debug = 0
+timeout = 5000
+
+[Misc]
+sendmail = /usr/local/bin/sendmail_to_file
+entity_loan.curator_mail_from=crawler-test@example.com
+entity_loan.curator_mail_to=crawler-test@example.com
+
+[sss_helper]
+external_uri = https://caosdb.example.com:443
+
+[advancedtools]
+crawler.from_mail=admin@example.com
+crawler.to_mail=admin@example.com
diff --git a/unittests/test_configs/pycaosdb-real-world-2.ini b/unittests/test_configs/pycaosdb-real-world-2.ini
new file mode 100644
index 0000000000000000000000000000000000000000..5ebd115a4a4de189d22180130acca2a4b78b6daf
--- /dev/null
+++ b/unittests/test_configs/pycaosdb-real-world-2.ini
@@ -0,0 +1,15 @@
+[Connection]
+url = https://samplemanager.example.com:443
+cacert = /opt/caosdb/cert/caosdb.cert.pem
+debug = 0
+timeout = 5000
+[Misc]
+sendmail = /usr/local/bin/sendmail_to_file
+entity_loan.curator_mail_from=crawler-test@example.com
+entity_loan.curator_mail_to=crawler-test@example.com
+[sss_helper]
+external_uri = https://localhost:10443
+[advancedtools]
+crawler.from_mail=crawler-test@example.com
+crawler.to_mail=crawler-test@example.com           
+
diff --git a/unittests/test_configs/pycaosdb-server-side-scripting.ini b/unittests/test_configs/pycaosdb-server-side-scripting.ini
new file mode 100644
index 0000000000000000000000000000000000000000..de2867f8dc66b3e81f10f35e40c36f9cb8591604
--- /dev/null
+++ b/unittests/test_configs/pycaosdb-server-side-scripting.ini
@@ -0,0 +1,9 @@
+; this is the pycaosdb.ini for the server-side-scripting home.
+[Connection]
+url = https://caosdb-server:10443
+cacert = /opt/caosdb/cert/caosdb.cert.pem
+debug = 0
+timeout = 5000
+
+[Misc]
+sendmail = /usr/local/bin/sendmail_to_file
diff --git a/unittests/test_configs/pycaosdb4.ini b/unittests/test_configs/pycaosdb4.ini
new file mode 100644
index 0000000000000000000000000000000000000000..ddbc7ca6f969e55ea6131d96f091177a13687ece
--- /dev/null
+++ b/unittests/test_configs/pycaosdb4.ini
@@ -0,0 +1,4 @@
+[Connection]
+url=https://localhost:10443/
+username=admin
+password_method=input
diff --git a/unittests/test_configs/pycaosdb5.ini b/unittests/test_configs/pycaosdb5.ini
new file mode 100644
index 0000000000000000000000000000000000000000..3f365efdd92641a39b742e22f825033a69e12dc5
--- /dev/null
+++ b/unittests/test_configs/pycaosdb5.ini
@@ -0,0 +1,4 @@
+[Connection]
+url=https://localhost:10443/
+username=admin
+# No password method: should be "input" by default
diff --git a/unittests/test_schema.py b/unittests/test_schema.py
index 1552179a3e43dacb3ecca705466bb7ff84d330cf..fc3f63a4cbaeadcac3c1cb9be2d861a0688fe4b0 100644
--- a/unittests/test_schema.py
+++ b/unittests/test_schema.py
@@ -2,7 +2,9 @@
 #
 # This file is a part of the CaosDB Project.
 #
+# Copyright (C) 2022 Indiscale GmbH <info@indiscale.com>
 # Copyright (C) 2021 Alexander Schlemmer
+# Copyright (C) 2022 Daniel Hornung <d.hornung@indiscale.com>
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as
@@ -31,15 +33,18 @@ from configparser import ConfigParser
 
 def test_config_files():
     for fn in glob(os.path.join(os.path.dirname(__file__), "test_configs", "*.ini")):
+        print(f"Testing {fn}.")
         c = ConfigParser()
         c.read(fn)
+        print(config_to_yaml(c))
         validate_yaml_schema(config_to_yaml(c))
 
 
 def test_broken_config_files():
     for fn in glob(os.path.join(os.path.dirname(__file__), "broken_configs", "*.ini")):
-        print(fn)
+        print(f"Testing {fn}.")
         with raises(ValidationError):
             c = ConfigParser()
             c.read(fn)
+            print(config_to_yaml(c))
             validate_yaml_schema(config_to_yaml(c))