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

@@ -51,7 +51,7 @@ public final class DecryptedGroupUtil_apply_Test {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("DecryptedGroupUtil and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
26, maxFieldFound);
27, maxFieldFound);
}
@Test
@@ -1052,4 +1052,36 @@ public final class DecryptedGroupUtil_apply_Test {
.modifyMemberLabels(List.of(modifyLabelAction))
.build());
}
@Test
public void apply_sets_member_label_access() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup group = new DecryptedGroup.Builder()
.revision(10)
.accessControl(
new AccessControl.Builder()
.attributes(AccessControl.AccessRequired.ADMINISTRATOR)
.members(AccessControl.AccessRequired.ADMINISTRATOR)
.memberLabel(AccessControl.AccessRequired.ADMINISTRATOR)
.build()
)
.build();
DecryptedGroupChange groupChange = new DecryptedGroupChange.Builder()
.revision(11)
.newMemberLabelAccess(AccessControl.AccessRequired.MEMBER)
.build();
DecryptedGroup expectedResult = new DecryptedGroup.Builder()
.revision(11)
.accessControl(
new AccessControl.Builder()
.attributes(AccessControl.AccessRequired.ADMINISTRATOR)
.members(AccessControl.AccessRequired.ADMINISTRATOR)
.memberLabel(AccessControl.AccessRequired.MEMBER)
.build()
)
.build();
assertEquals(expectedResult, DecryptedGroupUtil.apply(group, groupChange));
}
}

View File

@@ -41,7 +41,7 @@ public final class DecryptedGroupUtil_empty_Test {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("GroupChangeField and getChangedFields() need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
26, maxFieldFound);
27, maxFieldFound);
}
@Test
@@ -285,6 +285,16 @@ public final class DecryptedGroupUtil_empty_Test {
assertTrue(DecryptedGroupExtensions.isSilent(change));
}
@Test
public void not_empty_with_modify_member_label_access_field_27() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.newMemberLabelAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.build();
assertFalse(DecryptedGroupExtensions.getChangedFields(change).isEmpty());
assertFalse(DecryptedGroupExtensions.isSilent(change));
}
@Test
public void silent_with_profile_keys_and_banned_members() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()

View File

@@ -462,4 +462,29 @@ public final class GroupChangeReconstructTest {
assertEquals("", change.modifyMemberLabels.get(0).labelEmoji);
assertEquals("", change.modifyMemberLabels.get(0).labelString);
}
@Test
public void new_member_label_access() {
DecryptedGroup from = new DecryptedGroup.Builder()
.accessControl(
new AccessControl.Builder()
.memberLabel(AccessControl.AccessRequired.ADMINISTRATOR)
.build())
.build();
DecryptedGroup to = new DecryptedGroup.Builder()
.accessControl(
new AccessControl.Builder()
.memberLabel(AccessControl.AccessRequired.MEMBER)
.build())
.build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(
new DecryptedGroupChange.Builder()
.newMemberLabelAccess(AccessControl.AccessRequired.MEMBER)
.build(),
decryptedGroupChange);
}
}

View File

@@ -22,7 +22,7 @@ public final class GroupChangeUtil_changeIsEmpty_Test {
int maxFieldFound = getMaxDeclaredFieldNumber(GroupChange.Actions.class);
assertEquals("GroupChangeUtil and its tests need updating to account for new fields on " + GroupChange.Actions.class.getName(),
26, maxFieldFound);
27, maxFieldFound);
}
@Test
@@ -236,4 +236,13 @@ public final class GroupChangeUtil_changeIsEmpty_Test {
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_modify_member_label_access_field_27() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.modifyMemberLabelAccess(new GroupChange.Actions.ModifyMemberLabelAccessControlAction())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
}

View File

@@ -53,7 +53,7 @@ public final class GroupChangeUtil_resolveConflict_Test {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
26, maxFieldFound);
27, maxFieldFound);
}
/**
@@ -63,10 +63,10 @@ public final class GroupChangeUtil_resolveConflict_Test {
*/
@Test
public void ensure_resolveConflict_knows_about_all_fields_of_GroupChange() {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
int maxFieldFound = getMaxDeclaredFieldNumber(GroupChange.Actions.class);
assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + GroupChange.class.getName(),
26, maxFieldFound);
27, maxFieldFound);
}
/**
@@ -857,7 +857,7 @@ public final class GroupChangeUtil_resolveConflict_Test {
}
@Test
public void field_26__modify_member_label__remove_if_label_already_matches() {
public void field_26__member_label_change_removed_when_same_as_group_state() {
UUID memberUuid = UUID.fromString("d1d1d1d1-0000-4000-8000-000000000001");
DecryptedMember existingMember = member(memberUuid)
@@ -866,7 +866,7 @@ public final class GroupChangeUtil_resolveConflict_Test {
.labelString("matching label")
.build();
DecryptedGroup existingGroup = new DecryptedGroup.Builder()
DecryptedGroup groupState = new DecryptedGroup.Builder()
.revision(10)
.members(List.of(existingMember))
.build();
@@ -881,21 +881,58 @@ public final class GroupChangeUtil_resolveConflict_Test {
.modifyMemberLabels(List.of(modifyLabelAction))
.build();
DecryptedGroupChange.Builder resolvedActions = GroupChangeUtil.resolveConflict(existingGroup, conflictingChange);
assertTrue(resolvedActions.build().modifyMemberLabels.isEmpty());
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyMemberLabels(List.of(new GroupChange.Actions.ModifyMemberLabelAction()))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, conflictingChange, change).build();
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
}
@Test
public void field_26__modify_member_label__remove_if_member_not_in_group() {
public void field_26__member_label_change_preserved_when_differs_from_group_state() {
UUID memberUuid = UUID.fromString("d1d1d1d1-0000-4000-8000-000000000001");
DecryptedMember existingMember = member(memberUuid)
.newBuilder()
.labelEmoji("🔥")
.labelString("Old Label")
.build();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.revision(10)
.members(List.of(existingMember))
.build();
DecryptedModifyMemberLabel modifyLabelAction = new DecryptedModifyMemberLabel.Builder()
.aciBytes(UuidUtil.toByteString(memberUuid))
.labelEmoji("🎉")
.labelString("New Label")
.build();
DecryptedGroupChange conflictingChange = new DecryptedGroupChange.Builder()
.modifyMemberLabels(List.of(modifyLabelAction))
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyMemberLabels(List.of(new GroupChange.Actions.ModifyMemberLabelAction()))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, conflictingChange, change).build();
assertEquals(change, resolvedActions);
}
@Test
public void field_26__member_label_change_removed_when_member_not_in_group() {
UUID memberUuuid = UUID.fromString("d1d1d1d1-0000-4000-8000-000000000001");
UUID nonMemberUuid = UUID.fromString("d2d2d2d2-0000-4000-8000-000000000002");
DecryptedGroup existingGroup = new DecryptedGroup.Builder()
DecryptedGroup groupState = new DecryptedGroup.Builder()
.revision(10)
.members(List.of(member(memberUuuid)))
.build();
DecryptedModifyMemberLabel modifyLabelAction = new org.signal.storageservice.storage.protos.groups.local.DecryptedModifyMemberLabel.Builder()
DecryptedModifyMemberLabel modifyLabelAction = new DecryptedModifyMemberLabel.Builder()
.aciBytes(UuidUtil.toByteString(nonMemberUuid))
.labelEmoji("🔥")
.labelString("foo bar")
@@ -905,7 +942,55 @@ public final class GroupChangeUtil_resolveConflict_Test {
.modifyMemberLabels(List.of(modifyLabelAction))
.build();
DecryptedGroupChange.Builder resolved = GroupChangeUtil.resolveConflict(existingGroup, conflictingChange);
assertTrue(resolved.build().modifyMemberLabels.isEmpty());
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyMemberLabels(List.of(new GroupChange.Actions.ModifyMemberLabelAction()))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, conflictingChange, change).build();
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
}
@Test
public void field_27__member_label_access_change_preserved_when_differs_from_group_state() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.accessControl(new AccessControl.Builder().memberLabel(AccessControl.AccessRequired.ADMINISTRATOR).build())
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newMemberLabelAccess(AccessControl.AccessRequired.MEMBER)
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyMemberLabelAccess(
new GroupChange.Actions.ModifyMemberLabelAccessControlAction.Builder()
.memberLabelAccess(AccessControl.AccessRequired.MEMBER)
.build()
)
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertEquals(change, resolvedActions);
}
@Test
public void field_27__member_label_access_change_removed_when_same_as_group_state() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.accessControl(new AccessControl.Builder().memberLabel(AccessControl.AccessRequired.ADMINISTRATOR).build())
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newMemberLabelAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyMemberLabelAccess(
new GroupChange.Actions.ModifyMemberLabelAccessControlAction.Builder()
.memberLabelAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.build()
)
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
}
}

View File

@@ -19,6 +19,7 @@ import java.util.UUID;
import okio.ByteString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.admin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.approveMember;
@@ -45,7 +46,7 @@ public final class GroupChangeUtil_resolveConflict_decryptedOnly_Test {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
26, maxFieldFound);
27, maxFieldFound);
}
/**
@@ -676,7 +677,7 @@ public final class GroupChangeUtil_resolveConflict_decryptedOnly_Test {
}
@Test
public void field_26__modify_member_label__remove_if_label_already_matches() {
public void field_26__member_label_change_removed_when_same_as_group_state() {
UUID memberUuid = UUID.fromString("d1d1d1d1-0000-4000-8000-000000000001");
DecryptedMember existingMember = member(memberUuid)
@@ -691,7 +692,7 @@ public final class GroupChangeUtil_resolveConflict_decryptedOnly_Test {
.labelString("Already Set")
.build();
DecryptedGroup existingGroup = new DecryptedGroup.Builder()
DecryptedGroup groupState = new DecryptedGroup.Builder()
.revision(10)
.members(List.of(existingMember))
.build();
@@ -700,16 +701,16 @@ public final class GroupChangeUtil_resolveConflict_decryptedOnly_Test {
.modifyMemberLabels(List.of(modifyLabelAction))
.build();
DecryptedGroupChange.Builder resolved = GroupChangeUtil.resolveConflict(existingGroup, conflictingChange);
assertTrue(resolved.build().modifyMemberLabels.isEmpty());
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, conflictingChange).build();
assertTrue(DecryptedGroupExtensions.getChangedFields(resolvedChanges).isEmpty());
}
@Test
public void field_26__modify_member_label__remove_if_member_not_in_group() {
public void field_26__member_label_change_removed_when_member_not_in_group() {
UUID memberUuid = UUID.fromString("d1d1d1d1-0000-4000-8000-000000000001");
UUID notInGroupUuid = UUID.fromString("d2d2d2d2-0000-4000-8000-000000000002");
DecryptedGroup existingGroup = new DecryptedGroup.Builder()
DecryptedGroup groupState = new DecryptedGroup.Builder()
.revision(10)
.members(List.of(member(memberUuid)))
.build();
@@ -724,7 +725,64 @@ public final class GroupChangeUtil_resolveConflict_decryptedOnly_Test {
.modifyMemberLabels(List.of(modifyLabelAction))
.build();
DecryptedGroupChange.Builder resolved = GroupChangeUtil.resolveConflict(existingGroup, conflictingChange);
assertTrue(resolved.build().modifyMemberLabels.isEmpty());
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, conflictingChange).build();
assertTrue(DecryptedGroupExtensions.getChangedFields(resolvedChanges).isEmpty());
}
@Test
public void field_26__member_label_change_preserved_when_label_differs() {
UUID memberUuid = UUID.fromString("d1d1d1d1-0000-4000-8000-000000000001");
DecryptedMember existingMember = member(memberUuid)
.newBuilder()
.labelEmoji("🔥")
.labelString("Old Label")
.build();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.revision(10)
.members(List.of(existingMember))
.build();
DecryptedModifyMemberLabel modifyLabelAction = new DecryptedModifyMemberLabel.Builder()
.aciBytes(UuidUtil.toByteString(memberUuid))
.labelEmoji("🎉")
.labelString("New Label")
.build();
DecryptedGroupChange conflictingChange = new DecryptedGroupChange.Builder()
.modifyMemberLabels(List.of(modifyLabelAction))
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, conflictingChange).build();
assertFalse(DecryptedGroupExtensions.getChangedFields(resolvedChanges).isEmpty());
}
@Test
public void field_27__member_label_access_change_preserved_when_differs_from_group_state() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.accessControl(new AccessControl.Builder().memberLabel(AccessControl.AccessRequired.ADMINISTRATOR).build())
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newMemberLabelAccess(AccessControl.AccessRequired.MEMBER)
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertEquals(decryptedChange, resolvedChanges);
}
@Test
public void field_27__member_label_access_change_removed_when_same_as_group_state() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.accessControl(new AccessControl.Builder().memberLabel(AccessControl.AccessRequired.ADMINISTRATOR).build())
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newMemberLabelAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertTrue(DecryptedGroupExtensions.getChangedFields(resolvedChanges).isEmpty());
}
}

View File

@@ -73,7 +73,7 @@ public final class GroupsV2Operations_decrypt_change_Test {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("GroupV2Operations#decryptChange and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
26,
27,
maxFieldFound);
}
@@ -476,6 +476,16 @@ public final class GroupsV2Operations_decrypt_change_Test {
);
}
@Test
public void can_pass_through_new_member_label_access_field_27() {
GroupChange.Actions.Builder encryptedChange = groupOperations.createChangeMemberLabelRights(AccessControl.AccessRequired.ADMINISTRATOR);
DecryptedGroupChange.Builder expectedDecryptedChange = new DecryptedGroupChange.Builder()
.newMemberLabelAccess(AccessControl.AccessRequired.ADMINISTRATOR);
assertDecryption(encryptedChange, expectedDecryptedChange);
}
private static ProfileKey newProfileKey() {
try {
return new ProfileKey(Util.getSecretBytes(32));