Secret push protection status does not toggle via UI
Summary
We've had a couple reports (Slack: https://gitlab.slack.com/archives/C06NY8LDMT2/p1739547328729319, https://gitlab.slack.com/archives/C06NY8LDMT2/p1739210474732269) where the user attempts to enable Secret push protection via the UI, but after refreshing, the status goes back to disabled. See this video for an example: https://gitlab.slack.com/archives/C06NY8LDMT2/p1739560645186419?thread_ts=1739547328.729319&cid=C06NY8LDMT2
The user is able to enable the feature via REST API, tbd on GraphQL API.
This does not happen in all projects, so far it's unclear in what type of project it's happening.
Steps to reproduce
- Go to a project > Secure > Security configuration
- Attempt to toggle the status of Secret push protection from off to on
- Refresh the page
- Observe if the status is what you expect
Example Project
https://gitlab.com/gl-demo-ultimate-sissar/helm_chart/-/security/configuration
What is the current bug behavior?
The user attempts to enable Secret push protection via the UI, but after refreshing, the status goes back to disabled and SPP scans are not on.
What is the expected correct behavior?
The user attempts to enable Secret push protection via the UI, and after refreshing, the status stays enabled and SPP scans block secrets.
Relevant logs and/or screenshots
No error logs are generated
Output of checks
This bug happens on GitLab.com
Results of GitLab environment info
Expand for output related to GitLab environment info
(For installations with omnibus-gitlab package run and paste the output of: `sudo gitlab-rake gitlab:env:info`) (For installations from source run and paste the output of: `sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
Results of GitLab application Check
Expand for output related to the GitLab application check
(For installations with omnibus-gitlab package run and paste the output of:
sudo gitlab-rake gitlab:check SANITIZE=true)(For installations from source run and paste the output of:
sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true)(we will only investigate if the tests are passing)
Workaround
As a workaround, Secret push protection can be enabled via the REST API:
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/<project_id>/security_settings?secret_push_protection_enabled=true"
Possible fixes
Need to check Project.where(secret_push_protection_enabled: nil).count and Project.where("secret_push_protection_enabled IS DISTINCT FROM pre_receive_secret_detection_enabled").count
I think it's possible that frontend toggle updates preReceiveSecretDetectionEnabled via GraphQL, but database and backend are using secret_push_protection_enabled. This mismatch means the toggle appears to switch in the UI, but the correct value isn’t saved or read back properly, causing it to reset after a refresh.
Possible fix is to merge !178811 (diffs)
I think my initial hunch was off, but I think I'm closer to the root cause:
After investigating a test project, I see that project.security_setting.secret_push_protection_enabled: nil . app/assets/javascripts/security_configuration/components/secret_push_protection_feature_card.vue:116 (which is the file that generates the UI toggle) checks that the column value isn't null before toggling the feature enablement status -- if it's null/nil, it doesn't toggle. I tested an existing different test project where I'd previously enabled/disabled SPP, and it shows p.security_setting.secret_push_protection_enabled => true, and I can confirm I'm able to toggle the status via the UI. I just created a new test project that's never had SPP enabled, and I was able to successfully toggle SPP and p.security_setting.secret_push_protection_enabled => false rather than nil. So I believe that this is happening to some projects that existed before the column rename that had never enabled SPP before. Potential maximum customer impact is ProjectSecuritySetting.where(secret_push_protection_enabled: nil).count => 10583626
I think the reason for the secret_push_protection_enabled: nil is that the background migration to backfill the values of prsd_enabled into spp_enabled failed to complete for some reason -- when I check
ProjectSecuritySetting.where("secret_push_protection_enabled IS false AND pre_receive_secret_detection_enabled IS false").count => 29422868
so we can see that the migration didn't just put nil for all projects that had never enabled SPP or had it explicitly disabled, it just failed to complete after 29422868 rows. Concerningly,
ProjectSecuritySetting.where("secret_push_protection_enabled IS DISTINCT FROM pre_receive_secret_detection_enabled AND pre_receive_secret_detection_enabled IS true").count => 14946
so 14946 projects that had pre_receive_secret_detection_enabled => true now have secret_push_protection_enabled => nil
Anyway, to fix it:
- Create a new migration to complete the backfill the nil values:
class BackfillNullSecretPushProtectionEnabled < ActiveRecord::Migration[6.0]
disable_ddl_transaction!
def up
execute <<~SQL
UPDATE project_security_settings
SET secret_push_protection_enabled = pre_receive_secret_detection_enabled
WHERE secret_push_protection_enabled IS NULL;
SQL
execute <<~SQL
ALTER TABLE project_security_settings
VALIDATE CONSTRAINT check_20a23efdb6; # this constraint was added in the column rename MR and it checks that spp_enabled is not null
SQL
end
def down
# No rollback needed as this just ensures consistency
end
end
Regarding re-enablement, we could possibly ask someone with db write access to do something like:
UPDATE project_security_setting
SET secret_push_protection_enabled = pre_receive_secret_detection_enabled
WHERE pre_receive_secret_detection_enabled IS true AND secret_push_protection_enabled IS NULL;
which could be faster than the previously proposed solution of doing a backfill migration. This could solve the immediate concern of the 15k projects that should have SPP enabled. Then we could follow up with the backfill migration for the other ~10M projects where SPP is expected to be false, but the column is currently nil (preventing the UI toggle from working)