Add SecurityScanProfileDelete GraphQL mutation (with soft-delete)
Summary
Add a GraphQL mutation to delete Security::ScanProfile records, modeled on the existing
Security::Attribute delete flow. Part of epic &20193 (Advanced Configuration Profiles
for Security Scanners).
A scan profile is namespace-scoped and can be attached to thousands of projects via the
Security::ScanProfileProject join table. To keep the delete request cheap, we adopt the
Security::Attribute soft-delete pattern: the mutation soft-deletes the profile synchronously
(hiding it immediately) and enqueues a background worker to clean up the join records and hard-delete
the row.
Scope
- New mutation
Mutations::Security::ScanProfiles::Delete(graphql_name 'SecurityScanProfileDelete'), mounted withexperiment: { milestone: '19.3' }. - Argument:
id(Global ID of theSecurity::ScanProfile). ReturnsdeletedScanProfileId+errors. - Authorization:
authorize :apply_security_scan_profiles(viaauthorized_find!). The ability is already prevented when thesecurity_scan_profileslicensed feature is off, so the licensed-feature gate is automatic. - Gate on the
security_scan_profiles_featurefeature flag (checked inresolveagainst the profile's namespace). - Disallow deleting GitLab-recommended profiles: if
scan_profile.gitlab_recommended?, return a graceful user-facing error (errors: ['Cannot delete a GitLab-recommended scan profile']) and do not soft-delete or enqueue the worker. MirrorsSecurity::Attribute's non-editable guard.
Soft-delete implementation
- Migration: add
deleted_atcolumn + partial index (WHERE deleted_at IS NULL) tosecurity_scan_profiles. Do NOT change the existing unique index (it is theON CONFLICTtarget ofFindOrCreateService#upsert); scope name-uniqueness at the Rails layer only for now. - Model
Security::ScanProfile:not_deleted/deletedscopes,#destroysoft-delete override,#really_destroy!,.really_destroy_all!,#deleted?, uniquenessconditions: deleted_at: nil. Nodefault_scope. - Guard all read paths with
.not_deleted(resolvers,detach#find_profile!,FindOrCreateService,ScanProfileStatus::UpdateService,Project#security_scan_profile_for,scan_type_names_for_project, attach/detach workers). LeaveDeleteScanProfileService#find_by_idunguarded so the worker can locate and hard-delete the soft-deleted row. - Reuse existing
DeleteScanProfileService+DeleteScanProfilesWorker; change the service's finalscan_profile.destroytoreally_destroy!so the row is actually removed after join cleanup. - Add internal event
delete_scan_profile.
Acceptance criteria
-
SecurityScanProfileDeletemutation exists, mounted as an experiment, and is FF- + license-gated. - Caller must have
apply_security_scan_profiles; unauthorized/anonymous callers get an access error. - Deleting a profile attached to projects succeeds immediately (soft-delete) and cleans up join records + hard-deletes asynchronously.
- Deleting a
gitlab_recommendedprofile is rejected with a user-facing error and no state change. - Soft-deleted profiles no longer appear in any read path.
- Request spec (authorization matrix, FF off, license off, soft-delete, worker enqueue, recommended guard, internal event), model spec, and service/worker spec updates.
Notes
- Add
pipeline:run-as-if-foss(EE overrides of CE GraphQL/type classes touched). - Regenerate GraphQL docs/schema; update
db/structure.sql. - Known limitation (experiment): a name held by a not-yet-hard-deleted tombstone can still collide at the DB unique index; full DB-level scoping is a follow-up when the flag graduates.
Edited by 🤖 GitLab Bot 🤖