Skip to content

Commit

Permalink
dbio: record change to name, owner via ProjectService, requiring ADMI…
Browse files Browse the repository at this point in the history
…N perm
  • Loading branch information
RayPlante committed Nov 20, 2024
1 parent 0d811e1 commit 8639b5b
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 47 deletions.
84 changes: 52 additions & 32 deletions python/nistoar/midas/dbio/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
60 changes: 59 additions & 1 deletion python/nistoar/midas/dbio/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
23 changes: 9 additions & 14 deletions python/nistoar/midas/dbio/wsgi/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit 8639b5b

Please sign in to comment.