Accept member_role_id in project protected branches REST API params
## Context
| | |
|---|---|
| **Phase** | 3 of 6 |
| **Parallel with** | https://gitlab.com/gitlab-org/gitlab/-/work_items/594881+ <br> https://gitlab.com/gitlab-org/gitlab/-/work_items/594882+ <br> https://gitlab.com/gitlab-org/gitlab/-/work_items/594883+ <br> https://gitlab.com/gitlab-org/gitlab/-/work_items/594884+ |
| **Blocked by** | https://gitlab.com/gitlab-org/gitlab/-/work_items/594877+ <br> https://gitlab.com/gitlab-org/gitlab/-/work_items/594878+ <br> https://gitlab.com/gitlab-org/gitlab/-/work_items/594879+ |
| **Unblocks** | https://gitlab.com/gitlab-org/gitlab/-/work_items/594885+ <br> https://gitlab.com/gitlab-org/gitlab/-/work_items/594886+ |
## Summary
Update the EE protected branches API helpers and `AccessLevelParams` service to accept and process `member_role_id` as a valid parameter in the `allowed_to_push`, `allowed_to_merge`, and `allowed_to_unprotect` arrays for the project-level protected branches API.
## Background
The project protected branches REST API accepts `allowed_to_push`/`allowed_to_merge`/`allowed_to_unprotect` as arrays of objects. Currently valid keys per object are: `access_level`, `user_id`, `group_id`, `deploy_key_id` (push only), `id` (for update/destroy), `_destroy`.
In EE, `ProtectedRefs::AccessLevelParams` (prepended by `EE::ProtectedRefs::AccessLevelParams`) processes these arrays. Currently `granular_access_levels` passes through entries that do **not** have `deploy_key_id`. Adding `member_role_id` requires ensuring these entries flow through correctly.
## Relevant files
- `ee/lib/ee/api/helpers/protected_branches_helpers.rb` — Grape param declarations
- `ee/app/services/ee/protected_refs/access_level_params.rb` — service that processes params
- `app/services/protected_refs/access_level_params.rb` — CE base
## Changes required
### `ee/lib/ee/api/helpers/protected_branches_helpers.rb`
Add `member_role_id` to the `shared_params` block and/or directly to each `allowed_to_*` array param block:
```ruby
params :shared_params do
optional :user_id, type: Integer, desc: 'ID of a user'
optional :group_id, type: Integer, desc: 'ID of a group'
optional :member_role_id, type: Integer, desc: 'ID of a custom member role'
optional :id, type: Integer, desc: 'ID of a project'
optional :_destroy, type: Grape::API::Boolean, desc: 'Delete the object when true'
end
```
This will automatically propagate `member_role_id` into all three `allowed_to_*` array blocks that `use :shared_params`.
Add API documentation for the new parameter.
### `ee/app/services/ee/protected_refs/access_level_params.rb`
The current `granular_access_levels` method rejects entries with `deploy_key_id`. It passes through everything else (including `user_id`, `group_id`). Since `member_role_id` is not `deploy_key_id`, it will already pass through the filter — however verify explicitly:
```ruby
def granular_access_levels
entries = params[:"allowed_to_#{type}"] || []
entries.reject { |entry| entry[:deploy_key_id].present? }
end
```
No change needed unless `member_role_id` entries also need to be excluded from `use_default_access_level?` — verify that when `member_role_id` entries are the only entries, `use_default_access_level?` returns `false` so that the Maintainer default is not prepended.
Current logic:
```ruby
def use_default_access_level?(params)
return false unless super
entries = params[:"allowed_to_#{type}"] || []
entries.reject { |entry| entry[:deploy_key_id].present? }.blank?
end
```
Since `member_role_id` entries are not deploy key entries, `reject` will keep them, and `blank?` will return `false` — meaning `use_default_access_level?` correctly returns `false`. **No change needed**, but add a test to confirm this behaviour.
### Validation
In the API endpoint (or via model validation from Issue 4), validate that the provided `member_role_id` belongs to the project's root namespace. If not, return a `422 Unprocessable Entity` with a clear error message.
## API examples
**Create a protected branch with a custom role allowed to merge:**
```
POST /projects/:id/protected_branches
{
"name": "main",
"allowed_to_merge": [{ "member_role_id": 42 }],
"allowed_to_push": [{ "access_level": 40 }]
}
```
**Update — add a custom role, remove an existing one:**
```
PATCH /projects/:id/protected_branches/main
{
"allowed_to_merge": [
{ "id": 5, "_destroy": true },
{ "member_role_id": 42 }
]
}
```
## Testing
- API request spec: create protected branch with `member_role_id` in `allowed_to_merge` — verify access level row created with correct `member_role_id`
- API request spec: create with `member_role_id` in `allowed_to_push` and `allowed_to_unprotect`
- API request spec: update with `member_role_id`
- API request spec: `member_role_id` from wrong namespace returns 422
- API request spec: passing only `member_role_id` entries does not add a default Maintainer role entry
- Feature flag off: `member_role_id` param is ignored or returns 422
## Dependencies
- Issue 1 (DB migration)
- Issues 3 & 4 (model concern) — model must accept and validate `member_role_id`
## Labels
issue