Allow Editor and Reviewer profiles to transfer record ownership#9284
Draft
juanluisrp wants to merge 1 commit into
Draft
Allow Editor and Reviewer profiles to transfer record ownership#9284juanluisrp wants to merge 1 commit into
juanluisrp wants to merge 1 commit into
Conversation
Previously only UserAdmin and above could transfer ownership of records. This change extends the transfer-ownership feature to Editor and Reviewer profiles, with the following restrictions: - An Editor can only transfer records they own, and only to groups they belong to. - A Reviewer can transfer records owned by their groups, and only to groups they belong to. The /users/groups endpoint is updated so that Editor and Reviewer users can retrieve the list of users in their groups (needed to populate the transfer-ownership dialog). The UI visibility guards (selection-widget and mdactionmenu) are relaxed from isUserAdminOrMore() to isEditorOrMore() to expose the action to the new eligible profiles. Tests are added to MetadataSharingApiTest to cover the happy paths and the rejection cases.
juanluisrp
force-pushed
the
transfer-ownership
branch
from
May 19, 2026 09:54
238193c to
4fd05cf
Compare
josegar74
reviewed
May 19, 2026
| return; | ||
| } | ||
| } | ||
| } |
Member
There was a problem hiding this comment.
@juanluisrp I would refactor a bit the code of the changes:
private boolean isEditableMetadata(AbstractMetadata metadata, AccessManager accessMan, UserSession callerSession) throws Exception {
Profile callerProfile = callerSession.getProfile();
if (callerProfile == Profile.Editor || callerProfile == Profile.Reviewer) {
int callerUserId = callerSession.getUserIdAsInt();
Integer sourceUsr = metadata.getSourceInfo().getOwner();
Integer sourceGrp = metadata.getSourceInfo().getGroupOwner();
if (callerProfile == Profile.Editor) {
if (!Objects.equals(sourceUsr, callerUserId)) {
return false;
}
} else {
Set<Integer> reviewerGroups = accessMan.getReviewerGroups(callerSession);
if (sourceGrp == null || !reviewerGroups.contains(sourceGrp)) {
return false;
}
}
return true;
} else if (callerProfile == Profile.Administrator) {
return true;
} else {
return false;
}
}
private boolean isDestinationTransferGroupAllowed(Integer groupIdentifier, UserSession callerSession) {
Profile callerProfile = callerSession.getProfile();
if (callerProfile == Profile.Editor || callerProfile == Profile.Reviewer) {
int callerUserId = callerSession.getUserIdAsInt();
if (!ReservedGroup.isReserved(groupIdentifier)) {
List<Integer> myGroups = userGroupRepository.findGroupIds(
UserGroupSpecs.hasUserId(callerUserId));
if (!myGroups.contains(groupIdentifier)) {
return false;
}
}
return true;
} else if (callerProfile == Profile.Administrator) {
return true;
} else {
return false;
}
}...
UserSession callerSession = ApiUtils.getUserSession(session);
if (!isEditableMetadata(metadata, accessMan, callerSession)) {
report.addNotEditableMetadataId(metadata.getId());
return;
}
if (!isDestinationTransferGroupAllowed(groupIdentifier, callerSession)) {
report.addMetadataInfos(metadata,
"Transfer to group " + groupIdentifier + " is not allowed for your profile.");
return;
}
// Retrieve the identifiers associated with the metadata uuid.
// When the workflow is enabled, the metadata can have an approved and a working copy version.
...
Comment on lines
+189
to
+202
| } else if (myProfile == Profile.Reviewer || myProfile == Profile.Editor) { | ||
| List<Integer> myGroups = userGroupRepository.findAll(UserGroupSpecs.hasUserId(session.getUserIdAsInt())) | ||
| .stream().map(ug -> ug.getGroup().getId()).collect(Collectors.toList()); | ||
| List<UserGroup> userGroups = userGroupRepository.findAll(UserGroupSpecs.hasGroupIds(myGroups)); | ||
|
|
||
| if (groupTypes != null) { | ||
| userGroups = userGroups.stream() | ||
| .filter(ug -> groupTypes.contains(ug.getGroup().getType())) | ||
| .collect(Collectors.toList()); | ||
| } | ||
| for (UserGroup ug : userGroups) { | ||
| list.add(new UserGroupsResponse(ug.getUser(), ug.getGroup(), ug.getProfile().name())); | ||
| } | ||
| return list; |
Member
There was a problem hiding this comment.
I would refactor, with previous lines also:
private List<UserGroupsResponse> getUserGroupsResponse(List<UserGroup> userGroups, List<GroupType> groupTypes) {
List<UserGroupsResponse> list = new ArrayList<>();
if (groupTypes != null) {
userGroups = userGroups.stream()
.filter(ug -> groupTypes.contains(ug.getGroup().getType()))
.collect(Collectors.toList());
}
for (UserGroup ug : userGroups) {
list.add(new UserGroupsResponse(ug.getUser(), ug.getGroup(), ug.getProfile().name()));
}
return list;
}...
list.addAll(getUserGroupsResponse(userGroups, groupTypes));
return list;
} else if (myProfile == Profile.Reviewer || myProfile == Profile.Editor) {
List<Integer> myGroups = userGroupRepository.findAll(UserGroupSpecs.hasUserId(session.getUserIdAsInt()))
.stream().map(ug -> ug.getGroup().getId()).collect(Collectors.toList());
List<UserGroup> userGroups = userGroupRepository.findAll(UserGroupSpecs.hasGroupIds(myGroups));
return getUserGroupsResponse(userGroups, groupTypes);
} else {
throw new SecurityException("You don't have rights to do get the groups for this user");
}
...
josegar74
reviewed
May 19, 2026
| return; | ||
| } | ||
| } | ||
| } |
Member
There was a problem hiding this comment.
@juanluisrp I would refactor a bit the code of the changes:
private boolean isEditableMetadata(AbstractMetadata metadata, AccessManager accessMan, UserSession callerSession) throws Exception {
Profile callerProfile = callerSession.getProfile();
if (callerProfile == Profile.Editor || callerProfile == Profile.Reviewer) {
int callerUserId = callerSession.getUserIdAsInt();
Integer sourceUsr = metadata.getSourceInfo().getOwner();
Integer sourceGrp = metadata.getSourceInfo().getGroupOwner();
if (callerProfile == Profile.Editor) {
if (!Objects.equals(sourceUsr, callerUserId)) {
return false;
}
} else {
Set<Integer> reviewerGroups = accessMan.getReviewerGroups(callerSession);
if (sourceGrp == null || !reviewerGroups.contains(sourceGrp)) {
return false;
}
}
} else if (callerProfile == Profile.Administrator) {
return true;
}
return false;
}
private boolean isDestinationTransferGroupAllowed(Integer groupIdentifier, UserSession callerSession) {
Profile callerProfile = callerSession.getProfile();
if (callerProfile == Profile.Editor || callerProfile == Profile.Reviewer) {
int callerUserId = callerSession.getUserIdAsInt();
if (!ReservedGroup.isReserved(groupIdentifier)) {
List<Integer> myGroups = userGroupRepository.findGroupIds(
UserGroupSpecs.hasUserId(callerUserId));
if (!myGroups.contains(groupIdentifier)) {
return false;
}
}
return true;
} else if (callerProfile == Profile.Administrator) {
return true;
}
return false;
}...
UserSession callerSession = ApiUtils.getUserSession(session);
if (!isEditableMetadata(metadata, accessMan, callerSession)) {
report.addNotEditableMetadataId(metadata.getId());
return;
}
if (!isDestinationTransferGroupAllowed(groupIdentifier, callerSession)) {
report.addMetadataInfos(metadata,
"Transfer to group " + groupIdentifier + " is not allowed for your profile.");
return;
}
// Retrieve the identifiers associated with the metadata uuid.
// When the workflow is enabled, the metadata can have an approved and a working copy version.
...
josegar74
reviewed
May 19, 2026
| return; | ||
| } | ||
| } | ||
| } |
Member
There was a problem hiding this comment.
@juanluisrp I would refactor a bit the code of the changes:
private boolean isEditableMetadata(AbstractMetadata metadata, AccessManager accessMan, UserSession callerSession) throws Exception {
Profile callerProfile = callerSession.getProfile();
if (callerProfile == Profile.Editor || callerProfile == Profile.Reviewer) {
int callerUserId = callerSession.getUserIdAsInt();
Integer sourceUsr = metadata.getSourceInfo().getOwner();
Integer sourceGrp = metadata.getSourceInfo().getGroupOwner();
if (callerProfile == Profile.Editor) {
if (!Objects.equals(sourceUsr, callerUserId)) {
return false;
}
} else {
Set<Integer> reviewerGroups = accessMan.getReviewerGroups(callerSession);
if (sourceGrp == null || !reviewerGroups.contains(sourceGrp)) {
return false;
}
}
} else if (callerProfile == Profile.Administrator) {
return true;
}
return false;
}
private boolean isDestinationTransferGroupAllowed(Integer groupIdentifier, UserSession callerSession) {
Profile callerProfile = callerSession.getProfile();
if (callerProfile == Profile.Editor || callerProfile == Profile.Reviewer) {
int callerUserId = callerSession.getUserIdAsInt();
if (!ReservedGroup.isReserved(groupIdentifier)) {
List<Integer> myGroups = userGroupRepository.findGroupIds(
UserGroupSpecs.hasUserId(callerUserId));
if (!myGroups.contains(groupIdentifier)) {
return false;
}
}
return true;
} else if (callerProfile == Profile.Administrator) {
return true;
} else {
return false;
}
}...
UserSession callerSession = ApiUtils.getUserSession(session);
if (!isEditableMetadata(metadata, accessMan, callerSession)) {
report.addNotEditableMetadataId(metadata.getId());
return;
}
if (!isDestinationTransferGroupAllowed(groupIdentifier, callerSession)) {
report.addMetadataInfos(metadata,
"Transfer to group " + groupIdentifier + " is not allowed for your profile.");
return;
}
// Retrieve the identifiers associated with the metadata uuid.
// When the workflow is enabled, the metadata can have an approved and a working copy version.
...
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
/users/groupsendpoint now returns group members for Editor and Reviewer profiles (previously restricted to UserAdmin+), which is required to populate the transfer-ownership dialog.selection-widget.htmlandmdactionmenu.htmlare relaxed fromisUserAdminOrMore()toisEditorOrMore().Test plan
GET /srv/api/users/groups— should return users from their groups.MetadataSharingApiTest) pass.