diff --git a/irods/manager/metadata_manager.py b/irods/manager/metadata_manager.py index 8cf239669..931948a21 100644 --- a/irods/manager/metadata_manager.py +++ b/irods/manager/metadata_manager.py @@ -36,13 +36,13 @@ 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 - def _updated_keywords(self, opts): kw_ = self.__kw.copy() kw_.update(opts) @@ -52,18 +52,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 880bf1fe5..0819737dd 100644 --- a/irods/test/meta_test.py +++ b/irods/test/meta_test.py @@ -798,6 +798,59 @@ 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_admin_mode_and_keyword_exhibit_no_stickyness__issue_833(self): + # Create a rodsuser, and a session for that roduser. + adm = self.sess + user = d = None + try: + # Create a test user. + user = adm.users.create("bobby", "rodsuser") + user.modify("password", "bpass") + + # This is a convenience function to (re)instantiate the test iRODSSessions: + def new_session(): + return iRODSSession( + port=adm.port, + zone=adm.zone, + host=adm.host, + user=user.name, + password="bpass", + ) + + with new_session() as ses1: + d = ses1.data_objects.create(data_name:="/{adm.zone}/home/{user.name}/testfile".format(**locals())) + d.metadata(admin=True) + + with new_session() as ses2: + # Repeat the fetch of the data object using the new session, so we are clean of old references. + d = ses2.data_objects.get(data_name) + + # In this use of set(), we expect not to end up applying ADMIN_KW in the underlying API call. + # (Doing so as a rodsuser would raise INSUFFICIENT_PRIVILEGE_LEVEL and the test would fail.) + d.metadata.set('a','b') + + # Check that the option flag for use of ADMIN_KW is not set. + self.assertFalse(d.metadata.admin) + + # This function duplicates the way in which the client API endpoint calculates iRODS option keywords + # for the underlying API call: + get_call_keywords = lambda metacoll: metacoll._manager._updated_keywords((),) + + # Applying admin=True should result in API flags containing ADMIN_KW among the lookup keys. + md_modified=d.metadata(admin=True) + self.assertIn(kw.ADMIN_KW, get_call_keywords(md_modified)) + + # The modified admin setting should be reflected when reading it back from the object's + # internal options # bookkeeping. + self.assertTrue(md_modified.admin) + + # But the original (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