Fix bulk attributes replace edge case
What does this MR do and why?
This MR fixes an edge case in the REPLACE mode of the security attributes bulk update feature where existing attributes that should remain were being incorrectly removed.
The Problem
When using REPLACE mode, if a project already has an attribute that's also in the desired final state, the attribute would be removed but not re-added.
Example:
- Project has attribute
1000028 - REPLACE request with
[1000028, 1000033] - Expected:
[1000028, 1000033] - Actual:
[1000033](attribute1000028was lost)
Solution
Introduced a common_attribute_ids helper method in UpdateProjectAttributesService that:
- Identifies attributes present in both add and remove lists
- Excludes them from both operations
- Prevents redundant remove-then-add cycles
This ensures attributes that should remain are neither removed nor re-added, fixing the REPLACE mode edge case.
Implementation Details
The fix works by filtering out common IDs at the service level:
- Calculate intersection of
add_attribute_idsandremove_attribute_ids - Remove common IDs from both lists before processing
- Process remaining operations through existing
UpdateProjectAttributesServicelogic
Key Changes
-
Service Logic: Added
common_attribute_idsmethod to filter duplicate IDs - Test Coverage: Added test cases for the edge case scenario
How to set up and validate locally
-
Enable the feature flag:
Feature.enable(:security_categories_and_attributes) -
Create test data through the UI:
- Create a group
- Inside the group above, create:
- One project (note its ID)
- One subgroup with a project inside it (note the subgroup ID)
- Go to Group → Secure → Security Configuration
- Create 2 security attributes in the Category: "Application"
- Get the attribute IDs from console:
Security::Attribute.last(2).map(&:id)
-
Assign the attributes via the ADD GraphQL mutation:
mutation bulkUpdateSecurityAttributes { bulkUpdateSecurityAttributes(input: { items: [ "gid://gitlab/Group/176", # subgroup ID "gid://gitlab/Project/77" # project in root namespace ID ], attributes: [ "gid://gitlab/Security::Attribute/44" # first attribute ID from step 2 ], mode: ADD }) { errors } } -
Test the REPLACE mode:
mutation bulkUpdateSecurityAttributes { bulkUpdateSecurityAttributes(input: { items: [ "gid://gitlab/Project/176", "gid://gitlab/Project/77" ], attributes: [ "gid://gitlab/Security::Attribute/44" # this attribute is already assigned "gid://gitlab/Security::Attribute/45" # second attribute ID from step 2 ], mode: REPLACE }) { errors } } -
Verify both attributes are present on the project
- Open each project → Secure → Security configuration → Security attributes
- Check that the attributes were assigned to both projects
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist.