diff --git a/irods/manager/metadata_manager.py b/irods/manager/metadata_manager.py index 8cf23966..2007a77d 100644 --- a/irods/manager/metadata_manager.py +++ b/irods/manager/metadata_manager.py @@ -36,12 +36,14 @@ class MetadataManager(Manager): def __init__(self, *_): self._opts = _MetadataManager_opts_initializer.copy() super().__init__(*_) + # For the iRODS-api keywords only (currently ADMIN_KW is the sole one used): + self.__kw = {} @property def use_timestamps(self): return self._opts['timestamps'] - __kw: Dict[str, Any] = {} # default (empty) keywords + __default_kw: Dict[str, Any] = {} # default (empty) keywords def _updated_keywords(self, opts): kw_ = self.__kw.copy() @@ -52,18 +54,20 @@ def get_api_keywords(self): return self.__kw.copy() def __call__(self, **flags): - # Make a new shallow copy of the manager object, but update options from parameter list. + # Make a new shallow copy of the manager object, but duplicate options from parameter list as well as iRODS API + # flags (stored in the instance's private __kw member) to be applied in each call. new_self = copy.copy(self) new_self._opts = copy.copy(self._opts) + new_self.__kw = copy.copy(self.__kw) # Update the flags that do bookkeeping in the returned(new) manager object. new_self._opts.update((key, val) for key, val in flags.items() if val is not None) - # Update the ADMIN_KW flag in the returned(new) object. + # For the new object, make ADMIN_KW flag or absence thereof reflect the admin option in _opts. if new_self._opts.get('admin'): - self.__kw[kw.ADMIN_KW] = "" + new_self.__kw[kw.ADMIN_KW] = "" else: - self.__kw.pop(kw.ADMIN_KW, None) + new_self.__kw.pop(kw.ADMIN_KW, None) return new_self diff --git a/irods/test/meta_test.py b/irods/test/meta_test.py index 880bf1fe..f2f0933f 100644 --- a/irods/test/meta_test.py +++ b/irods/test/meta_test.py @@ -798,6 +798,52 @@ def test_prevention_of_attribute_creation__issue_795(self): # data.metadata(admin = True) generates a cloned object but for the one change to "admin". data.metadata.admin = True + def test_destickifying_of_admin_option__issue_833(self): + # Create a rodsuser, and a session for that roduser. + adm = self.sess + user = d = None + try: + user = adm.users.create("bobby", "rodsuser") + user.modify("password", "bpass") + sessions = [] + for _ in range(2): + with iRODSSession( + port=adm.port, + zone=adm.zone, + host=adm.host, + user=user.name, + password="bpass", + ) as ses: + # Get a reference to a data object owned by the rodsuser, creating it if not already there. + d = ses.data_objects.create("/{adm.zone}/home/{user.name}/testfile".format(**locals())) + + # d.metadata is a different MetadataManager instance (and d, a different session instance) on the + # second iteration of the loop as compared to the first. Thus, admin flags should not carry over. + if not sessions: + d.metadata(admin=True) + else: + # Should not be applying ADMIN_KW because INSUFFICIENT_PRIVILEGE_LEVEL exception would result + d.metadata.set('a','b') + sessions.append(ses) + + # Admin option should be false after the second loop iteration. + self.assertFalse(d.metadata.admin) + + get_call_keywords = lambda metacoll: metacoll._manager._updated_keywords((),) + + # Applying admin=True should result in API flags containing ADMIN_KW among the lookup keys. + self.assertIn(kw.ADMIN_KW, get_call_keywords(md_modified:=d.metadata(admin=True))) + + # Admin option should be on in the object options bookkeeping. + self.assertTrue(md_modified.admin) + + # However, the unmodified source object should not reflect use of an ADMIN_KW. + self.assertNotIn(kw.ADMIN_KW, get_call_keywords(d.metadata)) # keyword updates not reflected in copied obj. + finally: + if d: + d.unlink(force=True) + if user: + user.remove() if __name__ == "__main__": # let the tests find the parent irods lib