Skip to content
Snippets Groups Projects
Verified Commit fcff268a authored by Hinam Mehra's avatar Hinam Mehra :red_circle: Committed by GitLab
Browse files

Merge branch '448823-protected-branches-cr' into 'master'

Add managing protected branches as custom permission

See merge request !162208



Merged-by: default avatarHinam Mehra <hmehra@gitlab.com>
Approved-by: Hercules Merscher's avatarHercules Merscher <hmerscher@gitlab.com>
Approved-by: default avatarAlex Pennells <apennells@gitlab.com>
Approved-by: default avatarScott de Jonge <sdejonge@gitlab.com>
Approved-by: default avatarBrett Walker <bwalker@gitlab.com>
Approved-by: default avatarHinam Mehra <hmehra@gitlab.com>
Reviewed-by: default avatarBrett Walker <bwalker@gitlab.com>
Reviewed-by: Jarka Košanová's avatarJarka Košanová <jarka@gitlab.com>
Reviewed-by: default avatarHinam Mehra <hmehra@gitlab.com>
Reviewed-by: default avatarAlex Pennells <apennells@gitlab.com>
Reviewed-by: default avatarScott de Jonge <sdejonge@gitlab.com>
Reviewed-by: Hercules Merscher's avatarHercules Merscher <hmerscher@gitlab.com>
Co-authored-by: Jarka Košanová's avatarJarka Košanová <jarka@gitlab.com>
parents 2bda758d 8e47294d
No related branches found
No related tags found
1 merge request!162208Add managing protected branches as custom permission
Pipeline #1437950978 failed
Showing
with 277 additions and 44 deletions
......@@ -9,6 +9,7 @@ import {
GlSprintf,
} from '@gitlab/ui';
import { debounce, intersectionWith, groupBy, differenceBy, intersectionBy } from 'lodash';
import glAbilitiesMixin from '~/vue_shared/mixins/gl_abilities_mixin';
import { createAlert } from '~/alert';
import { __, s__, n__ } from '~/locale';
import { getUsers, getGroups, getDeployKeys } from '../api/access_dropdown_api';
......@@ -35,6 +36,7 @@ export default {
GlAvatar,
GlSprintf,
},
mixins: [glAbilitiesMixin()],
props: {
accessLevelsData: {
type: Array,
......@@ -187,6 +189,9 @@ export default {
...this.getDataForSave(LEVEL_TYPES.DEPLOY_KEY, 'deploy_key_id'),
];
},
canAdminContainer() {
return this.glAbilities.adminProject || this.glAbilities.adminGroup;
},
},
watch: {
query: debounce(function debouncedSearch() {
......@@ -226,29 +231,45 @@ export default {
focusInput() {
this.$refs.search?.focusInput();
},
getGroups() {
return this.groups.length
? Promise.resolve({ data: this.groups })
: getGroups({ withProjectAccess: this.groupsWithProjectAccess });
},
getData({ initial = false } = {}) {
this.initialLoading = initial;
this.loading = true;
if (this.hasLicense) {
Promise.all([
getDeployKeys(this.query),
getUsers(this.query),
this.groups.length
? Promise.resolve({ data: this.groups })
: getGroups({ withProjectAccess: this.groupsWithProjectAccess }),
])
.then(([deployKeysResponse, usersResponse, groupsResponse]) => {
this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse.data);
this.setSelected({ initial });
})
.catch(() =>
createAlert({ message: __('Failed to load groups, users and deploy keys.') }),
)
.finally(() => {
this.initialLoading = false;
this.loading = false;
});
if (this.canAdminContainer) {
Promise.all([getDeployKeys(this.query), getUsers(this.query), this.getGroups()])
.then(([deployKeysResponse, usersResponse, groupsResponse]) => {
this.consolidateData(
deployKeysResponse.data,
usersResponse.data,
groupsResponse.data,
);
this.setSelected({ initial });
})
.catch(() =>
createAlert({ message: __('Failed to load groups, users and deploy keys.') }),
)
.finally(() => {
this.initialLoading = false;
this.loading = false;
});
} else if (this.glAbilities.adminProtectedBranch) {
Promise.all([getUsers(this.query), this.getGroups()])
.then(([usersResponse, groupsResponse]) => {
this.consolidateData(null, usersResponse.data, groupsResponse.data);
this.setSelected({ initial });
})
.catch(() => createAlert({ message: __('Failed to load groups and users.') }))
.finally(() => {
this.initialLoading = false;
this.loading = false;
});
}
} else {
getDeployKeys(this.query)
.then((deployKeysResponse) => {
......@@ -284,27 +305,31 @@ export default {
}
}
this.deployKeys = deployKeysResponse.map((response) => {
const {
id,
fingerprint,
fingerprint_sha256: fingerprintSha256,
title,
owner: { avatar_url, name, username },
} = response;
if (this.canAdminContainer) {
this.deployKeys = deployKeysResponse.map((response) => {
const {
id,
fingerprint,
fingerprint_sha256: fingerprintSha256,
title,
owner: { avatar_url, name, username },
} = response;
const availableFingerprint = fingerprintSha256 || fingerprint;
const shortFingerprint = `(${availableFingerprint.substring(0, 14)}...)`;
const availableFingerprint = fingerprintSha256 || fingerprint;
const shortFingerprint = `(${availableFingerprint.substring(0, 14)}...)`;
return {
id,
title: title.concat(' ', shortFingerprint),
avatar_url,
fullname: name,
username,
type: LEVEL_TYPES.DEPLOY_KEY,
};
});
return {
id,
title: title.concat(' ', shortFingerprint),
avatar_url,
fullname: name,
username,
type: LEVEL_TYPES.DEPLOY_KEY,
};
});
} else {
this.deployKeys = [];
}
},
setSelected({ initial } = {}) {
if (initial) {
......
......@@ -143,13 +143,26 @@ export default class ProtectedBranchCreate {
});
}
createLimitedSuccessAlert() {
this.alert = createAlert({
variant: VARIANT_SUCCESS,
containerSelector: '.js-alert-protected-branch-created-container',
message: s__('ProtectedBranch|Protected branch was sucessfully created'),
});
}
showSuccessAlertIfNeeded() {
if (!this.hasProtectedBranchSuccessAlert()) {
return;
}
this.expandAndScroll(PROTECTED_BRANCHES_ANCHOR);
this.createSuccessAlert();
if (gon.abilities.adminProject || gon.abilities.adminGroup) {
this.createSuccessAlert();
} else {
this.createLimitedSuccessAlert();
}
localStorage.removeItem(IS_PROTECTED_BRANCH_CREATED);
}
......
......@@ -9,6 +9,10 @@ class RepositoryController < Groups::ApplicationController
before_action :authorize_access!, only: :show
before_action :define_deploy_token_variables, if: -> { can?(current_user, :create_deploy_token, @group) }
before_action do
push_frontend_ability(ability: :admin_group, resource: @group, user: current_user)
end
feature_category :continuous_delivery
urgency :low
......
......@@ -17,6 +17,8 @@ class CiCdController < Projects::ApplicationController
push_frontend_feature_flag(:ci_variables_pages, current_user)
push_frontend_feature_flag(:allow_push_repository_for_job_token, @project)
push_frontend_feature_flag(:ci_hidden_variables, @project.root_ancestor)
push_frontend_ability(ability: :admin_project, resource: @project, user: current_user)
end
helper_method :highlight_badge
......
......@@ -9,6 +9,8 @@ class RepositoryController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:edit_branch_rules, @project)
push_frontend_ability(ability: :admin_project, resource: @project, user: current_user)
push_frontend_ability(ability: :admin_protected_branch, resource: @project, user: current_user)
end
feature_category :source_code_management, [:show, :cleanup, :update]
......
......@@ -2,10 +2,12 @@
module Projects
class BranchRulePolicy < ::ProtectedBranchPolicy
rule { can?(:read_protected_branch) }.enable :read_branch_rule
rule { can?(:create_protected_branch) }.enable :create_branch_rule
rule { can?(:update_protected_branch) }.enable :update_branch_rule
rule { can?(:destroy_protected_branch) }.enable :destroy_branch_rule
rule { can?(:admin_project) }.policy do
enable :read_branch_rule
enable :create_branch_rule
enable :update_branch_rule
enable :destroy_branch_rule
end
end
end
......
......@@ -19,6 +19,9 @@
"admin_merge_request": {
"type": "boolean"
},
"admin_protected_branch": {
"type": "boolean"
},
"admin_push_rules": {
"type": "boolean"
},
......
......@@ -36899,6 +36899,7 @@ Member role permission.
| <a id="memberrolepermissionadmin_group_member"></a>`ADMIN_GROUP_MEMBER` | Add or remove users in a group, and assign roles to users. When assigning a role, users with this custom permission must select a role that has the same or fewer permissions as the default role used as the base for their custom role. |
| <a id="memberrolepermissionadmin_integrations"></a>`ADMIN_INTEGRATIONS` | Create, read, update, and delete integrations with external applications. |
| <a id="memberrolepermissionadmin_merge_request"></a>`ADMIN_MERGE_REQUEST` | Allows approval of merge requests. |
| <a id="memberrolepermissionadmin_protected_branch"></a>`ADMIN_PROTECTED_BRANCH` | Create, read, update, and delete protected branches for a project. |
| <a id="memberrolepermissionadmin_push_rules"></a>`ADMIN_PUSH_RULES` | Configure push rules for repositories at the group or project level. |
| <a id="memberrolepermissionadmin_runners"></a>`ADMIN_RUNNERS` | Create, view, edit, and delete group or project Runners. Includes configuring Runner settings. |
| <a id="memberrolepermissionadmin_terraform_state"></a>`ADMIN_TERRAFORM_STATE` | Execute terraform commands, lock/unlock terraform state files, and remove file versions. |
......@@ -86,6 +86,7 @@ These requirements are documented in the `Required permission` column in the fol
| Name | Required permission | Description | Introduced in | Feature flag | Enabled in |
|:-----|:------------|:------------------|:---------|:--------------|:---------|
| [`admin_merge_request`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128302) | | Allows approval of merge requests. | GitLab [16.4](https://gitlab.com/gitlab-org/gitlab/-/issues/412708) | | |
| [`admin_protected_branch`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/162208) | | Create, read, update, and delete protected branches for a project. | GitLab [17.4](https://gitlab.com/gitlab-org/gitlab/-/issues/448823) | | |
| [`admin_push_rules`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147872) | | Configure push rules for repositories at the group or project level. | GitLab [16.11](https://gitlab.com/gitlab-org/gitlab/-/issues/421786) | `custom_ability_admin_push_rules` | |
| [`read_code`](https://gitlab.com/gitlab-org/gitlab/-/issues/376180) | | Allows read-only access to the source code in the user interface. Does not allow users to edit or download repository archives, clone or pull repositories, view source code in an IDE, or view merge requests for private projects. You can download individual files because read-only access inherently grants the ability to make a local copy of the file. | GitLab [15.7](https://gitlab.com/gitlab-org/gitlab/-/issues/20277) | `customizable_roles` | GitLab [15.9](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110810) |
......
......@@ -15,6 +15,10 @@ def protected_ref_params(*attrs)
params_hash
end
def authorize_admin_protected_refs!
authorize_admin_protected_branch!
end
end
end
end
......@@ -95,7 +95,8 @@ def allow_protected_branches_for_group?(group)
def authorize_view_repository_settings!
return if can?(current_user, :admin_push_rules, project) ||
can?(current_user, :manage_deploy_tokens, project)
can?(current_user, :manage_deploy_tokens, project) ||
can?(current_user, :admin_protected_branch, project)
authorize_admin_project!
end
......
......@@ -774,6 +774,14 @@ module ProjectPolicy
enable :destroy_deploy_token
end
rule { custom_role_enables_admin_protected_branch }.policy do
enable :read_protected_branch
enable :create_protected_branch
enable :update_protected_branch
enable :destroy_protected_branch
enable :admin_protected_branch
end
rule { can?(:create_issue) & okrs_enabled }.policy do
enable :create_objective
enable :create_key_result
......
---
name: admin_protected_branch
description: Create, read, update, and delete protected branches for a project.
introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/448823
introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/162208
feature_category: source_code_management
milestone: '17.4'
group_ability: false
project_ability: true
requirements: []
available_from_access_level: 40
......@@ -16,7 +16,8 @@ module SettingsMenu
],
repository_menu_item: [
:admin_push_rules,
:manage_deploy_tokens
:manage_deploy_tokens,
:admin_protected_branch
],
merge_requests_menu_item: [
:manage_merge_request_settings
......
......@@ -11,6 +11,62 @@
project.add_maintainer(user)
end
context 'when using custom roles' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
let(:base_params) { project_params.merge(id: protected_branch.id) }
let_it_be(:protected_branch) { create(:protected_branch, project: project) }
let_it_be(:another_user) { create(:user) }
let(:maintainer_access_level) { [{ access_level: Gitlab::Access::MAINTAINER }] }
let(:access_level_params) do
{ merge_access_levels_attributes: maintainer_access_level,
push_access_levels_attributes: maintainer_access_level }
end
let(:create_params) do
attributes_for(:protected_branch).merge(access_level_params)
end
let(:update_params) { { name: 'new_name' } }
before do
sign_in(another_user)
end
context 'when a user has custom roles with `admin_protected_branch` assigned' do
let_it_be(:role) { create(:member_role, :guest, namespace: group, admin_protected_branch: true) }
let_it_be(:membership) { create(:group_member, :guest, member_role: role, user: another_user, group: group) }
context 'when custom_roles feature is available' do
before do
stub_licensed_features(custom_roles: true)
end
describe "POST #create" do
subject(:create_request) { post(:create, params: project_params.merge(protected_branch: create_params)) }
it 'creates a protected branch' do
expect { create_request }.to change { ProtectedBranch.count }.by(1)
expect(response).to have_gitlab_http_status(:found)
end
end
describe "PUT #update" do
subject(:update_request) { put(:update, params: base_params.merge(protected_branch: update_params)) }
it 'creates a protected branch' do
expect { update_request }.to change { protected_branch.reload.name }
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end
end
describe "POST #create" do
shared_examples "protected branch with code owner approvals feature" do |boolean|
it "sets code owner approvals to #{boolean} when protecting the branch" do
......
......@@ -198,5 +198,39 @@
.to contain_exactly(protected_branch_from_deletion)
end
end
context 'when accessing through custom ability' do
let_it_be(:another_user) { create(:user) }
let_it_be(:role) { create(:member_role, :guest, namespace: group, admin_protected_branch: true) }
let_it_be(:membership) { create(:group_member, :guest, member_role: role, user: another_user, group: group) }
before do
sign_in(another_user)
end
context 'with custom_roles feature enabled' do
before do
stub_licensed_features(custom_roles: true)
end
it 'allows access' do
get :show, params: { namespace_id: group, project_id: project }
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'with custom_roles feature disabled' do
before do
stub_licensed_features(custom_roles: false)
end
it 'does not allow access' do
get :show, params: { namespace_id: group, project_id: project }
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Protected Branches', :js, feature_category: :source_code_management do
include ProtectedBranchHelpers
context 'when a guest has custom roles with `admin_protected_branch` assigned' do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
let_it_be(:role) { create(:member_role, :guest, namespace: group, admin_protected_branch: true) }
let_it_be(:membership) { create(:group_member, :guest, member_role: role, user: user, group: group) }
let(:success_message) { s_('ProtectedBranch|Protected branch was sucessfully created') }
before do
stub_licensed_features(custom_roles: true)
sign_in(user)
end
it_behaves_like 'setting project protected branches'
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Projects > Settings > Repository settings using custom role', :js, feature_category: :source_code_management do
include ProtectedBranchHelpers
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
let_it_be(:current_user) { create(:user) }
let_it_be(:admin) { create(:admin) }
let_it_be(:role) { create(:member_role, :guest, namespace: group, admin_protected_branch: true) }
let_it_be(:membership) { create(:group_member, :guest, member_role: role, user: current_user, group: group) }
let(:success_message) { s_('ProtectedBranch|Protected branch was sucessfully created') }
context 'when user is a guest with custom roles that enables handling protected branches' do
before do
stub_licensed_features(custom_roles: true)
sign_in(current_user)
end
it_behaves_like 'setting project protected branches'
it 'does not show sections not allowed by the custom role', :aggregate_failures do
expect(page).not_to have_content('Branch defaults')
expect(page).not_to have_content('Push rules')
expect(page).not_to have_content('Mirroring repositories')
expect(page).not_to have_content('Protected tags')
expect(page).not_to have_content('Deploy tokens')
expect(page).not_to have_content('Deploy keys')
expect(page).not_to have_content('Repository maintenance')
end
end
end
......@@ -74,6 +74,7 @@ describe('ee/protected_environments/add_approvers.vue', () => {
deploy_access_levels: {
roles: [],
},
abilities: { adminProject: true },
};
mockAxios = new MockAdapter(axios);
});
......
......@@ -57,6 +57,7 @@ describe('ee/protected_environments/create_protected_environment.vue', () => {
deploy_access_levels: {
roles: [],
},
abilities: { adminProject: true },
};
mockAxios = new MockAdapter(axios);
});
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment