diff --git a/src/python/PyBis/pybis/pybis.py b/src/python/PyBis/pybis/pybis.py
index b245e48a911991447e4b5f30348c2fa8ee5e2ebf..c38f1e236745367d4b5b81e2c15449d4e7ce3140 100644
--- a/src/python/PyBis/pybis/pybis.py
+++ b/src/python/PyBis/pybis/pybis.py
@@ -52,6 +52,17 @@ except Exception:
 else:
     VERBOSE = True
 
+LOG_NONE    = 0
+LOG_SEVERE  = 1
+LOG_ERROR   = 2
+LOG_WARNING = 3
+LOG_INFO    = 4
+LOG_ENTRY   = 5
+LOG_PARM    = 6
+LOG_DEBUG   = 7
+
+DEBUG_LEVEL = LOG_NONE
+
 
 
 def _definitions(entity):
@@ -136,10 +147,11 @@ def _definitions(entity):
             "identifier": "userId",
         },
         "AuthorizationGroup" : {
-            "attrs": "permId code description users registrator registrationDate modificationDate".split(),
             "attrs_new": "code description userIds".split(),
-            "multi": "users".split()
-
+            "attrs_up": "code description userIds".split(),
+            "attrs": "permId code description registrator registrationDate modificationDate users".split(),
+            "multi": "users".split(),
+            "identifier": "groupId",
         },
         "RoleAssignment" : {
             "attrs": "id user authorizationGroup role roleLevel space project registrator registrationDate".split(),
@@ -320,8 +332,8 @@ def get_search_criteria(entity, **search_args):
 
 def extract_code(obj):
     if not isinstance(obj, dict):
-        return str(obj)
-    return obj['code']
+        return '' if obj is None else str(obj)
+    return '' if obj['code'] is None else obj['code']
 
 
 def extract_deletion(obj):
@@ -355,8 +367,8 @@ def extract_permid(permid):
 
 def extract_nested_permid(permid):
     if not isinstance(permid, dict):
-        return str(permid)
-    return permid['permId']['permId']
+        return '' if permid is None else str(permid)
+    return '' if permid['permId']['permId'] is None else permid['permId']['permId'] 
 
 
 def extract_property_assignments(pas):
@@ -434,19 +446,6 @@ def _create_tagIds(tags=None):
         })
     return tagIds
     
-def _create_userIds(users=None):
-    if users is None:
-        return None
-    if not isinstance(users, list):
-        users = [users]
-    userIds = []
-    for user in users:
-        userIds.append({
-            "permId": user, 
-            "@type": "as.dto.person.id.PersonPermId"
-        })
-    return userIds
-
 
 def _tagIds_for_tags(tags=None, action='Add'):
     """creates an action item to add or remove tags. 
@@ -782,10 +781,18 @@ class Openbis:
     For creation of datasets, dataset-uploader-api needs to be installed.
     """
 
-    def __init__(self, url, verify_certificates=True, token=None):
+    def __init__(self, url=None, verify_certificates=True, token=None):
         """Initialize a new connection to an openBIS server.
         :param host:
         """
+
+        if url is None:
+            try:
+                url = os.environ["OPENBIS_URL"]
+                token = os.environ["OPENBIS_TOKEN"] if "OPENBIS_TOKEN" in os.environ else None
+            except KeyError:
+                raise ValueError("please provide a URL you want to connect to.")
+
         url_obj = urlparse(url)
         if url_obj.netloc is None:
             raise ValueError("please provide the url in this format: https://openbis.host.ch:8443")
@@ -810,6 +817,11 @@ class Openbis:
         # use an existing token, if available
         if self.token is None:
             self.token = self._get_cached_token()
+        elif self.is_token_valid(token):
+            pass
+        else:
+            print("Session is no longer valid. Please log in again.")
+
 
     def __dir__(self):
         return [
@@ -851,7 +863,7 @@ class Openbis:
             "get_group(code)",
             "get_role_assignments()",
             "get_role_assignment(techId)",
-            "new_group(code, description, users)",
+            "new_group(code, description, userIds)",
             'new_space(name, description)',
             'new_project(space, code, description, attachments)',
             'new_experiment(type, code, project, props={})',
@@ -946,17 +958,18 @@ class Openbis:
             request["jsonrpc"] = "2.0"
         if request["params"][0] is None:
             raise ValueError("Your session expired, please log in again")
+
+        if DEBUG_LEVEL >=LOG_DEBUG: print(json.dumps(request))
         resp = requests.post(
             full_url,
             json.dumps(request),
             verify=self.verify_certificates
         )
 
-
         if resp.ok:
             resp = resp.json()
             if 'error' in resp:
-                if VERBOSE: print(json.dumps(request))
+                if DEBUG_LEVEL >= LOG_ERROR: print(json.dumps(request))
                 raise ValueError(resp['error']['message'])
             elif 'result' in resp:
                 return resp['result']
@@ -1048,10 +1061,10 @@ class Openbis:
         )
 
 
-    def new_group(self, code, description=None, users=None):
+    def new_group(self, code, description=None, userIds=None):
         """ creates an openBIS person
         """
-        return Group(self, code=code, description=description, users=users)
+        return Group(self, code=code, description=description, userIds=userIds)
 
 
     def get_group(self, groupId, only_data=False):
@@ -1353,6 +1366,9 @@ class Openbis:
         )
         return p
 
+    get_users = get_persons # Alias
+
+
     def get_person(self, userId, only_data=False):
         """ Get a person (user)
         """
@@ -1389,6 +1405,8 @@ class Openbis:
             else:
                 return Person(self, data=person)
 
+    get_user = get_person # Alias
+
 
     def get_spaces(self, code=None):
         """ Get a list of all available spaces (DataFrame object). To create a sample or a
@@ -1873,7 +1891,7 @@ class Openbis:
     update_object = update_sample # Alias
 
 
-    def delete_entity(self, entity, id, reason, id_name='PermId'):
+    def delete_entity(self, entity, id, reason, id_name='permId'):
         """Deletes Spaces, Projects, Experiments, Samples and DataSets
         """
 
@@ -2809,6 +2827,9 @@ class OpenBisObject():
         if 'properties' in data:
             for key, value in data['properties'].items():
                 self.p.__dict__[key.lower()] = value
+            
+        # object is already saved to openBIS, so it is not new anymore
+        self.a.__dict__['_is_new'] = False
 
     @property
     def attrs(self):
@@ -3233,8 +3254,6 @@ class DataSet(OpenBisObject):
             else:
                 raise ValueError('Error while creating the DataSet: ' + resp['rows'][0][1]['value'])
 
-            self.__dict__['_is_new'] = False
-
             
         else:
             request = self._up_attrs()
@@ -3279,18 +3298,24 @@ class AttrHolder():
         Since the data comes from openBIS, we do not have to check it (hence the
         self.__dict__ statements to prevent invoking the __setattr__ method)
         Internally data is stored with an underscore, e.g.
-            sample._space --> { '@id': 4,
-                                '@type': 'as.dto.space.id.SpacePermId',
-                                'permId': 'MATERIALS' }
+            sample._space = { 
+                '@type': 'as.dto.space.id.SpacePermId',
+                'permId': 'MATERIALS' 
+            }
         but when fetching the attribute without the underscore, we only return
         the relevant data for the user:
-            sample.space  --> 'MATERIALS'
+            sample.space    # MATERIALS
         """
+        # entity is read from openBIS, so it is not new anymore
         self.__dict__['_is_new'] = False
+
         for attr in self._allowed_attrs:
             if attr in ["code", "permId", "identifier",
                         "type", "container", "components"]:
                 self.__dict__['_' + attr] = data.get(attr, None)
+                # remove the @id attribute
+                if isinstance(self.__dict__['_' + attr], dict):
+                    self.__dict__['_' + attr].pop('@id')
 
             elif attr in ["space"]:
                 d = data.get(attr, None)
@@ -3354,19 +3379,35 @@ class AttrHolder():
                 items = getattr(self, '_' + attr)
                 if items is None:
                     items = []
+
+            elif attr == 'userIds':
+                if '_changed_users' not in self.__dict__:
+                    continue
+
+                new_obj[attr]=[]
+                for userId in self.__dict__['_changed_users']:
+                    if self.__dict__['_changed_users'][userId]['action'] == 'Add':
+                        new_obj[attr].append({
+                            "permId": userId,
+                            "@type": "as.dto.person.id.PersonPermId"
+                        })
+
+            elif attr == 'description':
+                new_obj[attr] = self.__dict__['_description'].get('value')
+
             else:
                 items = getattr(self, '_' + attr)
 
-            key = None
-            if attr in attr2ids:
-                # translate parents into parentIds, children into childIds etc.
-                key = attr2ids[attr]
-            else:
-                key = attr
+                key = None
+                if attr in attr2ids:
+                    # translate parents into parentIds, children into childIds etc.
+                    key = attr2ids[attr]
+                else:
+                    key = attr
 
-            new_obj[key] = items
+                new_obj[key] = items
 
-        # create a new entity
+        # guess the method name for creating a new entity and build the request
         if method_name is None:
             method_name = "create{}s".format(self.entity)
         request = {
@@ -3411,9 +3452,8 @@ class AttrHolder():
                     "@type": "as.dto.attachment.update.AttachmentListUpdateValue"
                 }
 
-            elif attr in ['tags','users']:
+            elif attr == 'tags':
                 # look which tags/users have been added or removed and update them
-                id_name = _definitions(attr)['attr2ids'] # tagIds, userIds
 
                 if getattr(self, '_prev_'+attr) is None:
                     self.__dict__['_prev_'+attr] = []
@@ -3432,11 +3472,33 @@ class AttrHolder():
                             "@type": "as.dto.common.update.ListUpdateActionAdd"
                         })
 
-                up_obj[id_name] = {
+                up_obj['tagIds'] = {
                     "@type": "as.dto.common.update.IdListUpdateValue",
                     "actions": actions
                 }
 
+            elif attr == 'userIds':
+                actions = []
+                if '_changed_users' not in self.__dict__:
+                    continue
+                for userId in self.__dict__['_changed_users']:
+                    actions.append({
+		        "items": [
+                            {
+                                "permId": userId,
+                                "@type": "as.dto.person.id.PersonPermId"
+			    }
+                        ],
+		        "@type": "as.dto.common.update.ListUpdateAction{}".format(
+                            self.__dict__['_changed_users'][userId]['action']
+                        )
+		    })
+
+                up_obj['userIds'] = {
+                    "actions": actions,
+                    "@type": "as.dto.common.update.IdListUpdateValue" 
+                }
+
             elif '_' + attr in self.__dict__:
                 # handle multivalue attributes (parents, children, tags etc.)
                 # we only cover the Set mechanism, which means we always update 
@@ -3471,6 +3533,8 @@ class AttrHolder():
                         for x in ['identifier','permId','@type']:
                             if x in value:
                                 val[x] = value[x]
+                        if attr in ['description']:
+                            val = value['value']
 
                         up_obj[attr2ids[attr]] = {
                             "@type": "as.dto.common.update.FieldUpdateValue",
@@ -3587,6 +3651,15 @@ class AttrHolder():
             new_sample.space = 'MATERIALS'
             new_sample.parents = ['/MATERIALS/YEAST747']
         """
+        #allowed_attrs = []
+        #if self.is_new:
+        #    allowed_attrs = _definitions(self.entity)['attrs_new']
+        #else:
+        #    allowed_attrs = _definitions(self.entity)['attrs_up']
+
+        #if name not in allowed_attrs:
+        #    raise ValueError("{} is not in the list of changeable attributes ({})".format(name, ", ".join(allowed_attrs) ) )
+
         if name in ["parents", "parent", "children", "child", "components"]:
             if name == "parent":
                 name = "parents"
@@ -3612,10 +3685,6 @@ class AttrHolder():
                 if '@id' in permid: permid.pop('@id')
                 permids.append(permid)
 
-            # setting self._parents = [{ 
-            #    '@type': 'as.dto.sample.id.SampleIdentifier',
-            #    'identifier': '/SPACE_NAME/SAMPLE_NAME'
-            # }]
             self.__dict__['_' + name] = permids
         elif name in ["tags"]:
             self.set_tags(value)
@@ -3686,8 +3755,20 @@ class AttrHolder():
 
             self.__dict__['_code'] = value
 
-        elif name in [ "description", "userId" ]:
-            self.__dict__['_'+name] = value
+        elif name in [ "description" ]:
+            self.__dict__['_'+name] = {
+                "value": value,
+            }
+            if not self.is_new:
+                self.__dict__['_' + name]['isModified'] = True
+                
+        elif name in ["userId"]:
+            # values that are directly assigned
+            self.__dict__['_' + name] = value
+
+        elif name in ["userIds"]:
+            self.add_users(value)
+
         else:
             raise KeyError("no such attribute: {}".format(name))
 
@@ -3797,38 +3878,46 @@ class AttrHolder():
             if tagId not in self.__dict__['_tags']:
                 self.__dict__['_tags'].append(tagId)
 
-    def set_users(self, users):
-        if getattr(self, '_users') is None:
-            self.__dict__['_users'] = []
-        if not isinstance(users, list):
-            users = [users]
-        for user in users:
-            # TODO
+    def set_users(self, userIds):
+        if userIds is None:
+            return
+        if getattr(self, '_userIds') is None:
+            self.__dict__['_userIds'] = []
+        if not isinstance(userIds, list):
+            userIds = [userIds]
+        for userId in userIds:
             person = self.openbis.get_person(userId=user, only_data=True)
-        
+            self.__dict__['_userIds'].append({
+                "permId": userId,
+                "@type": "as.dto.person.id.PersonPermId"
+            })
 
-    def add_users(self, users):
-        if getattr(self, '_users') is None:
-            self.__dict__['_users'] = []
+        
+    def add_users(self, userIds):
+        if userIds is None:
+            return
+        if getattr(self, '_changed_users') is None:
+            self.__dict__['_changed_users'] = {}
 
-        # add the new users to the _users and _new_users list,
-        # if not listed yet
-        userIds = _create_userIds(users)
+        if not isinstance(userIds, list):
+            userIds = [userIds]
         for userId in userIds:
-            if not userId in self.__dict__['_users']:
-                self.__dict__['_users'].append(userId)
+            self.__dict__['_changed_users'][userId] = {
+                "action": "Add"
+            }
 
-    def del_users(self, users):
-        if getattr(self, '_users') is None:
-            self.__dict__['_users'] = []
+    def del_users(self, userIds):
+        if userIds is None:
+            return
+        if getattr(self, '_changed_users') is None:
+            self.__dict__['_changed_users'] = {}
 
-        # remove the users from the _users and _del_users list,
-        # if listed there
-        userIds = _create_userIds(users)
+        if not isinstance(userIds, list):
+            userIds = [userIds]
         for userId in userIds:
-            if userId in self.__dict__['_users']:
-                self.__dict__['_users'].remove(userId)
-
+            self.__dict__['_changed_users'][userId] = {
+                "action": "Remove"
+            }
 
     def add_tags(self, tags):
         if getattr(self, '_tags') is None:
@@ -3942,12 +4031,12 @@ class AttrHolder():
         for attr in self._allowed_attrs:
             if attr == 'attachments':
                 continue
-            elif attr == 'users':
+            elif attr == 'users' and '_users' in self.__dict__:
                 lines.append([
                     attr,
                     ", ".join(att['userId'] for att in self._users)
                 ])
-            elif attr == 'roleAssignments':
+            elif attr == 'roleAssignments' and '_roleAssignments' in self.__dict__:
                 roles = []
                 for role in self._roleAssignments:
                     if role.get('space') is not None:
@@ -4002,6 +4091,7 @@ class Sample():
         for key, value in data['properties'].items():
             self.p.__dict__[key.lower()] = value
 
+
     def __dir__(self):
         return [
             'props', 'get_parents()', 'get_children()', 
@@ -4068,7 +4158,7 @@ class Sample():
 
     def delete(self, reason):
         self.openbis.delete_entity(entity='Sample',id=self.permId, reason=reason)
-        if VERBOSE: print("Sample {} has been sucessfully deleted.".format(self.permId))
+        if VERBOSE: print("Sample {} successfully deleted.".format(self.permId))
 
     def get_datasets(self, **kwargs):
         return self.openbis.get_datasets(sample=self.permId, **kwargs)
@@ -4123,7 +4213,7 @@ class RoleAssignment(OpenBisObject):
             entity='RoleAssignment', id=self.id, 
             reason=reason, id_name='techId'
         ) 
-        if VERBOSE: print("RoleAssignment has been sucessfully deleted.".format(self.permId))
+        if VERBOSE: print("RoleAssignment successfully deleted.".format(self.permId))
 
 
 class Person(OpenBisObject):
@@ -4150,7 +4240,7 @@ class Person(OpenBisObject):
         return [
             'permId', 'userId', 'firstName', 'lastName', 'email',
             'registrator', 'registrationDate','space',
-            'get_roles()', 'assign_role()', 'revoke_role(techId)',
+            'get_roles()', 'assign_role(role, space)', 'revoke_role(techId)',
         ]
 
 
@@ -4166,25 +4256,57 @@ class Person(OpenBisObject):
 
 
     def assign_role(self, role, **kwargs):
-        self.openbis.assign_role(role=role, person=self, **kwargs)
-        if VERBOSE:
-            print(
-                "Role {} successfully assigned to person {}".format(role, self.userId)
-            ) 
+        try:
+            self.openbis.assign_role(role=role, person=self, **kwargs)
+            if VERBOSE:
+                print(
+                    "Role {} successfully assigned to person {}".format(role, self.userId)
+                ) 
+        except ValueError as e:
+            if 'exists' in str(e):
+                if VERBOSE:
+                    print(
+                        "Role {} already assigned to person {}".format(role, self.userId)
+                    )
+            else:
+                raise ValueError(str(e))
 
-    def revoke_role(self, role, reason='no specific reason'):
+
+    def revoke_role(self, role, space=None, project=None, reason='no specific reason'):
         """ Revoke a role from this person. 
         """
+
+        techId = None
         if isinstance(role, int):
-            ra = self.openbis.get_role_assignment(role)
-            ra.delete(reason)
-            if VERBOSE:
-                print(
-                    "Role {} successfully revoked from person {}".format(role, self.code)
+            techId = role
+        else:
+            query = { "role": role }
+            if space is None:
+                query['space'] = ''
+            else:
+                query['space'] = space.upper()
+
+            if project is None:
+                query['project'] = ''
+            else:
+                query['project'] = project.upper()
+
+            querystr = " & ".join( 
+                    '{} == "{}"'.format(key, value) for key, value in query.items()
+                    )
+            return querystr
+             
+            roles = self.get_roles().df
+            techId = roles.query(querystr)['techId'].values[0]
+
+        # finally delete the role assignment
+        ra = self.openbis.get_role_assignment(techId)
+        ra.delete(reason)
+        if VERBOSE:
+            print(
+                "Role {} successfully revoked from person {}".format(role, self.code)
                 ) 
             return
-        else:
-            raise ValueError("Please provide the techId of the role assignment you want to revoke")
 
     def __str__(self):
         return "{} {}".format(self.get('firstName'), self.get('lastName'))
@@ -4215,8 +4337,6 @@ class Person(OpenBisObject):
             if "spaceId" in request['params'][1][0]:
                 request['params'][1][0]['homeSpaceId'] =  request['params'][1][0]['spaceId']
                 del(request['params'][1][0]['spaceId'])
-
-            return json.dumps(request)
             self.openbis._post_request(self.openbis.as_v3, request)
             if VERBOSE: print("Person successfully updated.")
             new_person_data = self.openbis.get_person(self.permId, only_data=True)
@@ -4250,6 +4370,7 @@ class Group(OpenBisObject):
         """ Returns a Things object wich contains all Persons (Users)
         that belong to this group.
         """
+
         persons = DataFrame(self._users)
         persons['permId'] = persons['permId'].map(extract_permid)
         persons['registrationDate'] = persons['registrationDate'].map(format_timestamp)
@@ -4370,6 +4491,16 @@ class Group(OpenBisObject):
             """
         return html
 
+    def delete(self, reason='unknown'):
+        self.openbis.delete_entity(
+            entity = "AuthorizationGroup",
+            id = self.permId, 
+            reason = reason
+        )
+        if VERBOSE:
+            print("Authorization group {} successfully deleted".format(
+                self.permId
+            ))
 
     def save(self):
         if self.is_new:
@@ -4445,7 +4576,7 @@ class Space(OpenBisObject):
 
     def delete(self, reason):
         self.openbis.delete_entity('Space', self.permId, reason)
-        if VERBOSE: print("Space {} has been sucessfully deleted.".format(self.permId))
+        if VERBOSE: print("Space {} has been sucsessfully deleted.".format(self.permId))
 
     def save(self):
         if self.is_new:
@@ -4728,7 +4859,7 @@ class Experiment(OpenBisObject):
         if self.permId is None:
             return None
         self.openbis.delete_entity(entity='Experiment', id=self.permId, reason=reason)
-        if VERBOSE: print("Experiment {} has been sucessfully deleted.".format(self.permId))
+        if VERBOSE: print("Experiment {} successfully deleted.".format(self.permId))
 
     def get_datasets(self, **kwargs):
         if self.permId is None:
@@ -4866,13 +4997,12 @@ class Project(OpenBisObject):
 
     def delete(self, reason):
         self.openbis.delete_entity(entity='Project', id=self.permId, reason=reason)
-        if VERBOSE: print("Project {} has been sucessfully deleted.".format(self.permId))
+        if VERBOSE: print("Project {} successfully deleted.".format(self.permId))
 
     def save(self):
         if self.is_new:
             request = self._new_attrs()
             resp = self.openbis._post_request(self.openbis.as_v3, request)
-            self.a.__dict__['_is_new'] = False
             if VERBOSE: print("Project successfully created.")
             new_project_data = self.openbis.get_project(resp[0]['permId'], only_data=True)
             self._set_data(new_project_data)