Implement member_role_access_allowed? and namespace validation
## Context
| | |
|---|---|
| **Phase** | 2 of 6 |
| **Parallel with** | https://gitlab.com/gitlab-org/gitlab/-/work_items/594877+ <br> https://gitlab.com/gitlab-org/gitlab/-/work_items/594879+ |
| **Blocked by** | https://gitlab.com/gitlab-org/gitlab/-/work_items/594874+ |
| **Unblocks** | https://gitlab.com/gitlab-org/gitlab/-/work_items/594880+ <br> 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+ |
## Summary
Implement `member_role_access_allowed?` in the `EE::ProtectedBranchAccess` concern, along with namespace-scoping validation. This is the core access check that determines whether a user's custom role grants them permission to push, merge, or unprotect a protected branch.
## Background
The `ProtectedRefAccess#check_access` method dispatches to `send(:"#{type}_access_allowed?", current_user, current_project)`. Adding `type == :member_role` (done in a separate issue) will automatically route to `member_role_access_allowed?` defined here.
The check must handle two contexts:
1. **Project-level protected branches** — `current_project` is the project; check the user's member record in that project
2. **Group-level protected branches** — `current_project` is still passed (it is the project being accessed), but the rule originates from the group; check that the user has the custom role inherited from the group hierarchy
## Relevant files
- `ee/app/models/concerns/ee/protected_ref_access.rb` — reference for `user_access_allowed?` and `group_access_allowed?`
- `app/models/concerns/protected_ref_access.rb` — reference for `role_access_allowed?` which handles both project and group contexts
## Changes required
### In `ee/app/models/concerns/ee/protected_branch_access.rb` (created in Issue 3)
Add to the private section:
```ruby
def member_role_access_allowed?(current_user, current_project)
return false unless member_role
return false unless Feature.enabled?(:custom_roles_for_protected_branches, current_project)
member = find_member_for_user(current_user, current_project)
return false unless member
member.member_role_id == member_role.id
end
def find_member_for_user(current_user, current_project)
if current_project
current_project.members.find_by(user: current_user)
elsif protected_branch_group
protected_branch_group.members_with_parents.find_by(user: current_user)
end
end
```
### Namespace validation
Add a `validate` that the `member_role` belongs to the same root namespace as the project or group:
```ruby
validate :validate_member_role_namespace, if: -> { member_role_id.present? }
def validate_member_role_namespace
return unless member_role
root_namespace = protected_ref_project&.root_namespace || protected_branch_group&.root_ancestor
return unless root_namespace
unless member_role.namespace_id == root_namespace.id
errors.add(:member_role, 'must belong to the same root namespace as the project or group')
end
end
```
## Behaviour specification
| Scenario | Expected result |
|---|---|
| User has the exact `member_role` assigned | `true` |
| User has a different custom role with the same `base_access_level` | `false` |
| User has no custom role (plain member) | `false` |
| User not a member of the project/group | `false` |
| `member_role` belongs to a different namespace | validation error at save time |
| Feature flag disabled | `false` |
| Group-level rule, user has the role via group inheritance | `true` |
| Group-level rule, user has the role only at project level (not group) | `false` (group hierarchy check only) |
## Notes
- The `members_with_parents` method on group follows the same inheritance pattern used by `group_access_allowed?` in `EE::ProtectedRefAccess`
- The `current_project` argument is always present even for group-level rules (it is the project at which access is being evaluated). Check `protected_branch_group` to detect group-level context
- Do not fall back to checking `base_access_level` — access must require the exact custom role
## Testing
- Unit tests for all scenarios in the table above
- Shared example with `ProtectedBranch::MergeAccessLevel`, `PushAccessLevel`, and `UnprotectAccessLevel`
- Integration test: create custom role, assign to user, protect branch with that role, assert push/merge access granted/denied correctly
- Verify a user with the same `base_access_level` but a different (or no) custom role is denied
## Dependencies
- Issue 1 (DB migration)
- Issue 3 (`EE::ProtectedBranchAccess` concern structure) — this adds methods to the same concern
## Labels
issue