Add rate limit for protected branch writes
What does this MR do and why
Rate limits protected branch creation and deletion: 5,000 writes per hour, per user, per top-level group. Create and delete share one counter, so a protect/unprotect loop does not get twice the budget.
This covers the project and group protected branch REST endpoints, the branch/:branch/(un)protect endpoints, and the branchRuleCreate / branchRuleDelete mutations — all of them funnel through ProtectedBranches::CreateService and DestroyService.
Out of scope: updates to an existing protected branch, and system callers. Projects::ProtectDefaultBranchService and the GitHub importer pass the new skip_rate_limit: true, so automatic writes are not charged to the project creator's quota.
The threshold
The limit is the application setting protected_branch_write_limit: default 5000, set to 0 to disable. It lives in the existing rate_limits jsonb column, so there is no migration, and it is read and written through /api/v4/application/settings.
The setting is labelled beta in the docs, following the convention agreed for the Project Bulkhead limits (internal discussion in gitlab-org&24176, reference implementation !266323 (merged)). Beta attributes sit outside the deprecation policy, so these keys can still be renamed or grouped once a generalized rate limit configuration exists.
Because the threshold is a setting rather than a constant, correcting it during the observation phase is an API call, not a release.
Feature flag
The key is registered with staged_rule, so it ships in log mode: the :log twin counts every write, and nothing is rejected until protected_branch_write_rate_limit_enforce is enabled. The registry checks the flag, so the service only sees the decision.
While the flag is off, over-limit writes show up in:
gitlab_labkit_rate_limiter_rule_evaluations_totalwithrule="limit_protected_branch_writes_by_user_namespace_log"andresult="log".rate_limit_stateon the request log line:applimiter_protected_branch_write:limit_protected_branch_writes_by_user_namespace_log:logover the limit,:allowunder it. Themetafields on the same line say who made the request.
So we can watch real traffic, correct the threshold through the setting, and then enforce with a flag flip instead of a second release. The flag actor is the root namespace, so enforcement rolls out per top-level group. The :log and :limit rules use separate counters, so the hour the flag flips starts from zero.
Documentation
The limit is listed in configurable rate limits, on the protected branches and group protected branches API pages, and the setting is documented in the application settings API. doc/api/openapi/openapi_v3.yaml is regenerated with bundle exec rake gitlab:openapi:v3:generate.
How to set up and validate locally
Leave the flag off, lower the limit, then protect and unprotect a branch past it. The write succeeds, and the request log line in log/api_json.log or log/development_json.log carries:
"rate_limit_state":["applimiter_protected_branch_write:limit_protected_branch_writes_by_user_namespace_log:log"]ApplicationSetting.current.update!(protected_branch_write_limit: 2)Enable the flag and repeat: the API returns 429.
Feature.enable(:protected_branch_write_rate_limit_enforce)Set the limit to 0 and nothing is counted at all.
bundle exec rspec spec/services/protected_branches/create_service_spec.rb \
spec/services/protected_branches/destroy_service_spec.rb \
ee/spec/services/ee/protected_branches/create_service_spec.rb \
ee/spec/services/ee/protected_branches/destroy_service_spec.rb