diff --git a/python/nistoar/midas/dbio/base.py b/python/nistoar/midas/dbio/base.py index 6a39bd3..c64b122 100644 --- a/python/nistoar/midas/dbio/base.py +++ b/python/nistoar/midas/dbio/base.py @@ -215,6 +215,39 @@ def id(self): def owner(self): return self._data.get('owner', "") + @owner.setter + def owner(self, val): + self.reassign(val) + + def reassign(self, who: str): + """ + transfer ownership to the given user. To transfer ownership, the calling user must have + "admin" permission on this record. Note that this will not remove any permissions assigned + to the former owner. + + :param str who: the identifier for the user to set as the owner of this record + :raises NotAuthorized: if the calling user is not authorized to change the owner. + :raises InvalidUpdate: if the target user identifier is not recognized or not legal + """ + if not self.authorized(ACLs.ADMIN): + raise NotAuthorized(self._cli.user_id, "change owner") + + # make sure the target user is valid + if not self._validate_user_id(who): + raise InvalidUpdate("Unable to update owner: invalid user ID: "+str(who)) + + self._data['owner'] = who + for perm in ACLs.OWN: + self.acls.grant_perm_to(perm, who) + + def _validate_user_id(self, who: str): + if not bool(who) or not isinstance(who, str): + # default test: ensure user is a non-empty string + return False + if self._cli and self._cli.people_service: + return bool(self._cli.people_service.get_person_by_eid(who)) + return True + @property def created(self) -> float: """ @@ -325,35 +358,6 @@ def save(self): self._data['since']) = olddates raise - def reassign(self, who: str): - """ - transfer ownership to the given user. To transfer ownership, the calling user must have - "admin" permission on this record. Note that this will not remove any permissions assigned - to the former owner. - - :param str who: the identifier for the user to set as the owner of this record - :raises NotAuthorized: if the calling user is not authorized to change the owner. - :raises InvalidUpdate: if the target user identifier is not recognized or not legal - """ - if not self.authorized(ACLs.ADMIN): - raise NotAuthorized(self._cli.user_id, "change owner") - - # make sure the target user is valid - if not self._validate_user_id(who): - raise InvalidUpdate("Unable to update owner: invalid user ID: "+str(who)) - - self._data['owner'] = who - for perm in ACLs.OWN: - self.acls.grant_perm_to(perm, who) - - def _validate_user_id(self, who: str): - if not bool(who) or not isinstance(who, str): - # default test: ensure user is a non-empty string - return False - if self._cli and self._cli.people_service: - return bool(self._cli.people_service.get_person_by_eid(who)) - return True - def authorized(self, perm: Permissions, who: str = None): """ return True if the given user has the specified permission to commit an action on this record. @@ -794,10 +798,26 @@ def name(self) -> str: @name.setter def name(self, val): + self.rename(val) + + def rename(self, newname): """ - assign the given name as the record's mnemonic name + assign the given name as the record's mnemonic name. If this record was pulled from + the backend storage, then a check will be done to ensure that the name does not match + that of any other record owned by the current user. + + :param str newname: the new name to assign to the record + :raises NotAuthorized: if the calling user is not authorized to changed the name; for + non-superusers, ADMIN permission is required to rename a record. + :raises AlreadyExists: if the name has already been given to a record owned by the + current user. """ - self._data['name'] = val + if not self.authorized(ACLs.ADMIN): + raise NotAuthorized(self._cli.user_id, "change owner") + if self._cli and self._cli.name_exists(newname): + raise AlreadyExists(f"User {self_cli.user_id} has already defined a record with name={newname}") + + self._data['name'] = newname @property def data(self) -> MutableMapping: @@ -1003,7 +1023,7 @@ def exists(self, gid: str) -> bool: def name_exists(self, name: str, owner: str = None) -> bool: """ - return True if a group with the given name exists. READ permission on the identified + return True if a project with the given name exists. READ permission on the identified record is not required to use this method. :param str name: the mnemonic name of the group given to it by its owner :param str owner: the ID of the user owning the group of interest; if not given, the diff --git a/python/nistoar/midas/dbio/project.py b/python/nistoar/midas/dbio/project.py index 322e5a1..f8d55ae 100644 --- a/python/nistoar/midas/dbio/project.py +++ b/python/nistoar/midas/dbio/project.py @@ -185,11 +185,69 @@ def delete_record(self, id) -> ProjectRecord: # TODO: handling previously published records raise NotImplementedError() + def reassign_record(self, id, recipient: str): + """ + reassign ownership of the record with the given recepient ID. This is a wrapper around + :py:class:`~nistoar.midas.dbio.base.ProjectRecord`.reassign() that also logs the change. + :param id: the record identifier to reassign + :param str recipient: the identifier of the user to reassign ownership to + :raises InvalidUpdate: if the recipient ID is not legal or unrecognized + :raises NotAuthorized: if the current user does is not authorized to reassign. Non-superusers + must have "admin" permission to reassign. + :raises ObjectNotFound: if the record ``id`` is not found + :returns: the identifier for the new owner that was set for the record + :rtype: str + """ + prec = self.dbcli.get_record_for(id) # may raise ObjectNotFound + + message = "from %s to %s" % (prec.owner, recipient) + try: + self.log.info("Reassigning ownership of %s %s", id, message) + prec.reassign(recipient) + prec.save() + self._record_action(Action(Action.COMMENT, prec.id, self.who, + f"Reassigned ownership {message}")) + return prec.owner + + except Exception as ex: + self.log.error("Failed to reassign record %s to %s: %s", id, recipient, str(ex)) + raise + + def rename_record(self, id, newname: str): + """ + change the short, mnemonic name assigned to the record with the given recepient ID. This is + a wrapper around :py:class:`~nistoar.midas.dbio.base.ProjectRecord`.rename() that also logs + the change. + :param id: the record identifier to rename + :param str newname: the new name to give to the record + :raises AlreadyExists: if the name has already been assigned to another record owned by the + current user. + :raises NotAuthorized: if the current user does is not authorized to reassign. Non-superusers + must have "admin" permission to reassign. + :raises ObjectNotFound: if the record ``id`` is not found + :returns: the identifier for the new owner that was set for the record + :rtype: str + """ + prec = self.dbcli.get_record_for(id) # may raise ObjectNotFound + + message = "from %s to %s" % (prec.name, newname) + try: + self.log.info("Renaming %s %s", id, message) + prec.rename(newname) + prec.save() + self._record_action(Action(Action.COMMENT, prec.id, self.who, + f"Renaming {message}")) + return prec.name + + except Exception as ex: + self.log.error("Failed to rename record %s to %s: %s", id, newname, str(ex)) + raise + def _get_id_shoulder(self, user: Agent): """ return an ID shoulder that is appropriate for the given user agent :param Agent user: the user agent that is creating a record, requiring a shoulder - :raises NotAuthorized: if an uathorized shoulder appropriate for the user cannot be determined. + :raises NotAuthorized: if an authorized shoulder appropriate for the user cannot be determined. """ out = None client_ctl = self.cfg.get('clients', {}).get(user.agent_class) diff --git a/python/nistoar/midas/dbio/wsgi/project.py b/python/nistoar/midas/dbio/wsgi/project.py index 8f650c6..2a6eb29 100644 --- a/python/nistoar/midas/dbio/wsgi/project.py +++ b/python/nistoar/midas/dbio/wsgi/project.py @@ -321,29 +321,24 @@ def do_PUT(self, path): "Supplied value is tool long for "+path, self._id) try: - prec = self.svc.get_record(self._id) if path == "owner": - self.log.info("Reassigning ownership of %s from %s to %s", self._id, prec.owner, name) - prec.reassign(name) - else: - self.log.info("Changing short name of %s from %s to %s", self._id, prec.name, name) - setattr(prec, path, name) - if not prec.authorized(dbio.ACLs.ADMIN): - raise dbio.NotAuthorized(self._dbcli.user_id, "change record "+path) - prec.save() + newname = self.svc.reassign_record(self._id, name) # may raise NotAuthorized + + elif path == "name": + newname = self.svc.rename_record(self._id, name) # may raise NotAuthorized, AlreadyExists + except dbio.NotAuthorized as ex: return self.send_unauthorized() except dbio.ObjectNotFound as ex: return self.send_error_resp(404, "ID not found", "Record with requested identifier not found", self._id) - except dbio.InvalidUpdate as ex: - return self.send_error_resp(400, "Invalid Input", - "Invalid value provided for %s: %s" % (path, str(ex)), self._id) + except (dbio.InvalidUpdate, dbio.AlreadyExists) as ex: + return self.send_error_resp(400, "Invalid Input", str(ex), self._id) if format.name == "text": - return self.send_ok(getattr(prec, path), contenttype=format.ctype) + return self.send_ok(newname, contenttype=format.ctype) else: - return self.send_json(getattr(prec, path)) + return self.send_json(newname) class ProjectDataHandler(ProjectRecordHandler):