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] (attribute 1000028 was 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:

  1. Calculate intersection of add_attribute_ids and remove_attribute_ids
  2. Remove common IDs from both lists before processing
  3. Process remaining operations through existing UpdateProjectAttributesService logic

Key Changes

  • Service Logic: Added common_attribute_ids method to filter duplicate IDs
  • Test Coverage: Added test cases for the edge case scenario

How to set up and validate locally

  1. Enable the feature flag:

    Feature.enable(:security_categories_and_attributes)
  2. 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)
  3. 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
      }
    }
  4. 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
      }
    }
  5. 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.

Edited by Nicolae Rotaru

Merge request reports

Loading