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