Introduce new add member labels permission.

This commit is contained in:
jeffrey-signal
2026-03-06 15:58:00 -05:00
parent 7beb4dd939
commit 13444136bd
26 changed files with 441 additions and 60 deletions

View File

@@ -48,4 +48,6 @@ public interface ChangeSetModifier {
void removePromotePendingPniAciMembers(int i);
void removeModifyMemberLabels(int i);
void clearModifyMemberLabelAccess();
}

View File

@@ -114,6 +114,10 @@ internal class DecryptedGroupChangeActionsBuilderChangeSetModifier(private val r
result.modifyMemberLabels = result.modifyMemberLabels.removeIndex(i)
}
override fun clearModifyMemberLabelAccess() {
result.newMemberLabelAccess = AccessControl.AccessRequired.UNKNOWN
}
private fun <T> List<T>.removeIndex(i: Int): List<T> {
val modifiedList = this.toMutableList()
modifiedList.removeAt(i)

View File

@@ -74,6 +74,7 @@ fun DecryptedGroupChange.getChangedFields(): Set<GroupChangeField> {
if (newInviteLinkAccess != AccessControl.AccessRequired.UNKNOWN) add(GroupChangeField.INVITE_LINK_ACCESS)
if (newInviteLinkPassword.size != 0) add(GroupChangeField.INVITE_LINK_PASSWORD)
if (newMemberAccess != AccessControl.AccessRequired.UNKNOWN) add(GroupChangeField.MEMBER_ACCESS)
if (newMemberLabelAccess != AccessControl.AccessRequired.UNKNOWN) add(GroupChangeField.MEMBER_LABEL_ACCESS)
if (modifyMemberLabels.isNotEmpty()) add(GroupChangeField.MEMBER_LABELS)
if (deleteMembers.isNotEmpty()) add(GroupChangeField.MEMBER_REMOVALS)
if (modifyMemberRoles.isNotEmpty()) add(GroupChangeField.MEMBER_ROLES)
@@ -115,6 +116,7 @@ enum class GroupChangeField(val changeSilently: Boolean = false) {
INVITE_LINK_ACCESS,
INVITE_LINK_PASSWORD,
MEMBER_ACCESS,
MEMBER_LABEL_ACCESS,
MEMBER_LABELS(changeSilently = true),
MEMBER_REMOVALS,
MEMBER_ROLES,

View File

@@ -1,5 +1,7 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.signal.core.models.ServiceId;
import org.signal.core.models.ServiceId.ACI;
import org.signal.libsignal.protocol.logging.Log;
import org.signal.storageservice.storage.protos.groups.AccessControl;
import org.signal.storageservice.storage.protos.groups.Member;
@@ -13,8 +15,6 @@ import org.signal.storageservice.storage.protos.groups.local.DecryptedPendingMem
import org.signal.storageservice.storage.protos.groups.local.DecryptedPendingMemberRemoval;
import org.signal.storageservice.storage.protos.groups.local.DecryptedRequestingMember;
import org.signal.storageservice.storage.protos.groups.local.EnabledState;
import org.signal.core.models.ServiceId;
import org.signal.core.models.ServiceId.ACI;
import org.whispersystems.signalservice.api.push.ServiceIds;
import java.util.ArrayList;
@@ -323,6 +323,8 @@ public final class DecryptedGroupUtil {
applyModifyAddFromInviteLinkAccessControlAction(builder, change);
applyModifyMemberLabelAccessControlAction(builder, change);
applyAddRequestingMembers(builder, change.newRequestingMembers);
applyDeleteRequestingMembers(builder, change.deleteRequestingMembers);
@@ -524,6 +526,15 @@ public final class DecryptedGroupUtil {
}
}
private static void applyModifyMemberLabelAccessControlAction(DecryptedGroup.Builder builder, DecryptedGroupChange change) {
AccessControl.AccessRequired newAccessLevel = change.newMemberLabelAccess;
if (newAccessLevel != AccessControl.AccessRequired.UNKNOWN) {
AccessControl.Builder accessControlBuilder = builder.accessControl != null ? builder.accessControl.newBuilder() : new AccessControl.Builder();
builder.accessControl(accessControlBuilder.memberLabel(newAccessLevel).build());
}
}
private static void applyAddRequestingMembers(DecryptedGroup.Builder builder, List<DecryptedRequestingMember> newRequestingMembers) {
List<DecryptedRequestingMember> requestingMembers = new ArrayList<>(builder.requestingMembers);
requestingMembers.addAll(newRequestingMembers);

View File

@@ -105,6 +105,10 @@ internal class GroupChangeActionsBuilderChangeSetModifier(private val result: Gr
result.modifyMemberLabels = result.modifyMemberLabels.removeIndex(i)
}
override fun clearModifyMemberLabelAccess() {
result.modifyMemberLabelAccess = null
}
private fun <T> List<T>.removeIndex(i: Int): List<T> {
val modifiedList = this.toMutableList()
modifiedList.removeAt(i)

View File

@@ -151,6 +151,12 @@ public final class GroupChangeReconstruct {
}
}
if (fromState.accessControl == null || (toState.accessControl != null && !fromState.accessControl.memberLabel.equals(toState.accessControl.memberLabel))) {
if (toState.accessControl != null) {
builder.newMemberLabelAccess(toState.accessControl.memberLabel);
}
}
builder.newRequestingMembers(new ArrayList<>(intersectRequestingByAci(toState.requestingMembers, newRequestingMemberAcis)));
builder.deleteRequestingMembers(rejectedRequestMembers.stream().map(requestingMember -> requestingMember.aciBytes).collect(Collectors.toList()));

View File

@@ -29,29 +29,30 @@ public final class GroupChangeUtil {
* True iff there are no change actions.
*/
public static boolean changeIsEmpty(GroupChange.Actions change) {
return change.addMembers.size() == 0 && // field 3
change.deleteMembers.size() == 0 && // field 4
change.modifyMemberRoles.size() == 0 && // field 5
change.modifyMemberProfileKeys.size() == 0 && // field 6
change.addMembersPendingProfileKey.size() == 0 && // field 7
change.deleteMembersPendingProfileKey.size() == 0 && // field 8
change.promoteMembersPendingProfileKey.size() == 0 && // field 9
change.modifyTitle == null && // field 10
change.modifyAvatar == null && // field 11
change.modifyDisappearingMessageTimer == null && // field 12
change.modifyAttributesAccess == null && // field 13
change.modifyMemberAccess == null && // field 14
change.modifyAddFromInviteLinkAccess == null && // field 15
change.addMembersPendingAdminApproval.size() == 0 && // field 16
change.deleteMembersPendingAdminApproval.size() == 0 && // field 17
change.promoteMembersPendingAdminApproval.size() == 0 && // field 18
change.modifyInviteLinkPassword == null && // field 19
change.modifyDescription == null && // field 20
change.modify_announcements_only == null && // field 21
change.add_members_banned.size() == 0 && // field 22
change.delete_members_banned.size() == 0 && // field 23
change.promote_members_pending_pni_aci_profile_key.size() == 0 && // field 24
change.modifyMemberLabels.isEmpty(); // field 26
return change.addMembers.isEmpty() && // field 3
change.deleteMembers.isEmpty() && // field 4
change.modifyMemberRoles.isEmpty() && // field 5
change.modifyMemberProfileKeys.isEmpty() && // field 6
change.addMembersPendingProfileKey.isEmpty() && // field 7
change.deleteMembersPendingProfileKey.isEmpty() && // field 8
change.promoteMembersPendingProfileKey.isEmpty() && // field 9
change.modifyTitle == null && // field 10
change.modifyAvatar == null && // field 11
change.modifyDisappearingMessageTimer == null && // field 12
change.modifyAttributesAccess == null && // field 13
change.modifyMemberAccess == null && // field 14
change.modifyAddFromInviteLinkAccess == null && // field 15
change.addMembersPendingAdminApproval.isEmpty() && // field 16
change.deleteMembersPendingAdminApproval.isEmpty() && // field 17
change.promoteMembersPendingAdminApproval.isEmpty() && // field 18
change.modifyInviteLinkPassword == null && // field 19
change.modifyDescription == null && // field 20
change.modify_announcements_only == null && // field 21
change.add_members_banned.isEmpty() && // field 22
change.delete_members_banned.isEmpty() && // field 23
change.promote_members_pending_pni_aci_profile_key.isEmpty() && // field 24
change.modifyMemberLabels.isEmpty() && // field 26
change.modifyMemberLabelAccess == null; // field 27
}
/**
@@ -155,6 +156,7 @@ public final class GroupChangeUtil {
resolveField23DeleteBannedMembers (conflictingChange, changeSetModifier, bannedMembersByServiceId);
resolveField24PromotePendingPniAciMembers (conflictingChange, changeSetModifier, fullMembersByUuid);
resolveField26ModifyMemberLabels (conflictingChange, changeSetModifier, fullMembersByUuid);
resolveField27ModifyMemberLabelAccess (groupState, conflictingChange, changeSetModifier);
}
private static void resolveField3AddMembers(DecryptedGroupChange conflictingChange, ChangeSetModifier result, HashMap<ByteString, DecryptedMember> fullMembersByUuid, HashMap<ByteString, DecryptedPendingMember> pendingMembersByServiceId) {
@@ -390,4 +392,15 @@ public final class GroupChangeUtil {
}
}
}
private static void resolveField27ModifyMemberLabelAccess(
@Nonnull DecryptedGroup groupState,
@Nonnull DecryptedGroupChange conflictingChange,
@Nonnull ChangeSetModifier result
)
{
if (groupState.accessControl != null && conflictingChange.newMemberLabelAccess == groupState.accessControl.memberLabel) {
result.clearModifyMemberLabelAccess();
}
}
}

View File

@@ -1,5 +1,9 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.signal.core.models.ServiceId;
import org.signal.core.models.ServiceId.ACI;
import org.signal.core.models.ServiceId.PNI;
import org.signal.core.util.UuidUtil;
import org.signal.libsignal.protocol.logging.Log;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.NotarySignature;
@@ -17,14 +21,14 @@ import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialPresentation;
import org.signal.storageservice.storage.protos.groups.AccessControl;
import org.signal.storageservice.storage.protos.groups.MemberBanned;
import org.signal.storageservice.storage.protos.groups.Group;
import org.signal.storageservice.storage.protos.groups.GroupAttributeBlob;
import org.signal.storageservice.storage.protos.groups.GroupChange;
import org.signal.storageservice.storage.protos.groups.GroupJoinInfo;
import org.signal.storageservice.storage.protos.groups.Member;
import org.signal.storageservice.storage.protos.groups.MemberPendingProfileKey;
import org.signal.storageservice.storage.protos.groups.MemberBanned;
import org.signal.storageservice.storage.protos.groups.MemberPendingAdminApproval;
import org.signal.storageservice.storage.protos.groups.MemberPendingProfileKey;
import org.signal.storageservice.storage.protos.groups.local.DecryptedApproveMember;
import org.signal.storageservice.storage.protos.groups.local.DecryptedBannedMember;
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup;
@@ -39,10 +43,6 @@ import org.signal.storageservice.storage.protos.groups.local.DecryptedRequesting
import org.signal.storageservice.storage.protos.groups.local.DecryptedString;
import org.signal.storageservice.storage.protos.groups.local.DecryptedTimer;
import org.signal.storageservice.storage.protos.groups.local.EnabledState;
import org.signal.core.models.ServiceId;
import org.signal.core.models.ServiceId.ACI;
import org.signal.core.models.ServiceId.PNI;
import org.signal.core.util.UuidUtil;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -53,11 +53,11 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -344,6 +344,12 @@ public final class GroupsV2Operations {
);
}
public GroupChange.Actions.Builder createChangeMemberLabelRights(AccessControl.AccessRequired newRights) {
return new GroupChange.Actions.Builder().modifyMemberLabelAccess(
new GroupChange.Actions.ModifyMemberLabelAccessControlAction.Builder().memberLabelAccess(newRights).build()
);
}
public GroupChange.Actions.Builder createAnnouncementGroupChange(boolean isAnnouncementGroup) {
return new GroupChange.Actions.Builder().modify_announcements_only(
new GroupChange.Actions.ModifyAnnouncementsOnlyAction.Builder().announcements_only(isAnnouncementGroup).build()
@@ -770,6 +776,11 @@ public final class GroupsV2Operations {
}
builder.modifyMemberLabels(modifyMemberLabels);
// Field 27
if (actions.modifyMemberLabelAccess != null) {
builder.newMemberLabelAccess(actions.modifyMemberLabelAccess.memberLabelAccess);
}
if (editorServiceId instanceof ServiceId.PNI) {
if (actions.addMembers.size() == 1 && builder.newMembers.size() == 1) {
GroupChange.Actions.AddMemberAction addMemberAction = actions.addMembers.get(0);

View File

@@ -111,6 +111,7 @@ message DecryptedGroupChange {
repeated DecryptedBannedMember deleteBannedMembers = 23;
repeated DecryptedMember promotePendingPniAciMembers = 24;
repeated DecryptedModifyMemberLabel modifyMemberLabels = 26;
AccessControl.AccessRequired newMemberLabelAccess = 27;
}
message DecryptedString {

View File

@@ -69,6 +69,7 @@ message AccessControl {
AccessRequired attributes = 1;
AccessRequired members = 2;
AccessRequired addFromInviteLink = 3;
AccessRequired memberLabel = 4;
}
message Group {
@@ -225,6 +226,10 @@ message GroupChange {
AccessControl.AccessRequired addFromInviteLinkAccess = 1;
}
message ModifyMemberLabelAccessControlAction {
AccessControl.AccessRequired memberLabelAccess = 1;
}
message ModifyInviteLinkPasswordAction {
bytes inviteLinkPassword = 1;
}
@@ -262,7 +267,8 @@ message GroupChange {
repeated DeleteMemberBannedAction delete_members_banned = 23; // change epoch = 4
repeated PromoteMemberPendingPniAciProfileKeyAction promote_members_pending_pni_aci_profile_key = 24; // change epoch = 5
repeated ModifyMemberLabelAction modifyMemberLabels = 26; // change epoch = 6;
// next: 27
ModifyMemberLabelAccessControlAction modifyMemberLabelAccess = 27; // change epoch = 6
// next: 28
}
bytes actions = 1;