Commit e2387b46 authored by Tan Le's avatar Tan Le

Add API to query group MR approval settings

This MR adds supported migration, model and GET API to manage group
merge request approval settings.

The new GET API is limited to user with owner or admin role.

GET /groups/:id/merge_request_approval_setting
parent 259c247e
---
title: Add group MR approval settings table
merge_request: 50256
author:
type: added
# frozen_string_literal: true
class AddGroupMergeRequestApprovalSettings < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
create_table :group_merge_request_approval_settings, id: false do |t|
t.timestamps_with_timezone null: false
t.references :group, references: :namespaces, primary_key: true, default: nil, index: false,
foreign_key: { to_table: :namespaces, on_delete: :cascade }
t.boolean :allow_author_approval, null: false, default: false
end
end
end
def down
with_lock_retries do
drop_table :group_merge_request_approval_settings
end
end
end
59e40a24e8422e64bc85c4d54e6da923512017bac6a167350affeff241046e9f
\ No newline at end of file
...@@ -12949,6 +12949,13 @@ CREATE SEQUENCE group_import_states_group_id_seq ...@@ -12949,6 +12949,13 @@ CREATE SEQUENCE group_import_states_group_id_seq
ALTER SEQUENCE group_import_states_group_id_seq OWNED BY group_import_states.group_id; ALTER SEQUENCE group_import_states_group_id_seq OWNED BY group_import_states.group_id;
CREATE TABLE group_merge_request_approval_settings (
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
group_id bigint NOT NULL,
allow_author_approval boolean DEFAULT false NOT NULL
);
CREATE TABLE group_wiki_repositories ( CREATE TABLE group_wiki_repositories (
shard_id bigint NOT NULL, shard_id bigint NOT NULL,
group_id bigint NOT NULL, group_id bigint NOT NULL,
...@@ -19680,6 +19687,9 @@ ALTER TABLE ONLY group_group_links ...@@ -19680,6 +19687,9 @@ ALTER TABLE ONLY group_group_links
ALTER TABLE ONLY group_import_states ALTER TABLE ONLY group_import_states
ADD CONSTRAINT group_import_states_pkey PRIMARY KEY (group_id); ADD CONSTRAINT group_import_states_pkey PRIMARY KEY (group_id);
ALTER TABLE ONLY group_merge_request_approval_settings
ADD CONSTRAINT group_merge_request_approval_settings_pkey PRIMARY KEY (group_id);
ALTER TABLE ONLY group_wiki_repositories ALTER TABLE ONLY group_wiki_repositories
ADD CONSTRAINT group_wiki_repositories_pkey PRIMARY KEY (group_id); ADD CONSTRAINT group_wiki_repositories_pkey PRIMARY KEY (group_id);
...@@ -24345,6 +24355,9 @@ ALTER TABLE ONLY merge_request_blocks ...@@ -24345,6 +24355,9 @@ ALTER TABLE ONLY merge_request_blocks
ALTER TABLE ONLY merge_request_reviewers ALTER TABLE ONLY merge_request_reviewers
ADD CONSTRAINT fk_rails_3704a66140 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_3704a66140 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY group_merge_request_approval_settings
ADD CONSTRAINT fk_rails_37b6b4cdba FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY analytics_cycle_analytics_project_stages ALTER TABLE ONLY analytics_cycle_analytics_project_stages
ADD CONSTRAINT fk_rails_3829e49b66 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_3829e49b66 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
......
...@@ -44,6 +44,7 @@ module EE ...@@ -44,6 +44,7 @@ module EE
has_many :provisioned_users, through: :provisioned_user_details, source: :user has_many :provisioned_users, through: :provisioned_user_details, source: :user
has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::GroupStage' has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::GroupStage'
has_many :value_streams, class_name: 'Analytics::CycleAnalytics::GroupValueStream' has_many :value_streams, class_name: 'Analytics::CycleAnalytics::GroupValueStream'
has_one :group_merge_request_approval_setting, inverse_of: :group
has_one :deletion_schedule, class_name: 'GroupDeletionSchedule' has_one :deletion_schedule, class_name: 'GroupDeletionSchedule'
delegate :deleting_user, :marked_for_deletion_on, to: :deletion_schedule, allow_nil: true delegate :deleting_user, :marked_for_deletion_on, to: :deletion_schedule, allow_nil: true
......
# frozen_string_literal: true
class GroupMergeRequestApprovalSetting < ApplicationRecord
belongs_to :group, inverse_of: :group_merge_request_approval_setting
validates :group, presence: true
validates :allow_author_approval, inclusion: { in: [true, false], message: 'must be a boolean value' }
self.primary_key = :group_id
end
...@@ -88,6 +88,7 @@ class License < ApplicationRecord ...@@ -88,6 +88,7 @@ class License < ApplicationRecord
group_forking_protection group_forking_protection
group_ip_restriction group_ip_restriction
group_merge_request_analytics group_merge_request_analytics
group_merge_request_approval_settings
group_milestone_project_releases group_milestone_project_releases
group_project_templates group_project_templates
group_repository_analytics group_repository_analytics
......
...@@ -111,6 +111,10 @@ module EE ...@@ -111,6 +111,10 @@ module EE
@subject.feature_available?(:push_rules) @subject.feature_available?(:push_rules)
end end
condition(:group_merge_request_approval_settings_enabled) do
@subject.feature_available?(:group_merge_request_approval_settings)
end
condition(:over_storage_limit, scope: :subject) { @subject.over_storage_limit? } condition(:over_storage_limit, scope: :subject) { @subject.over_storage_limit? }
rule { public_group | logged_in_viewable }.policy do rule { public_group | logged_in_viewable }.policy do
...@@ -255,6 +259,10 @@ module EE ...@@ -255,6 +259,10 @@ module EE
enable :admin_group_credentials_inventory enable :admin_group_credentials_inventory
end end
rule { (admin | owner) & group_merge_request_approval_settings_enabled }.policy do
enable :admin_merge_request_approval_settings
end
rule { needs_new_sso_session }.policy do rule { needs_new_sso_session }.policy do
prevent :read_group prevent :read_group
end end
......
---
name: group_merge_request_approval_settings_feature_flag
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50256
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/247921
milestone: '13.8'
type: development
group: group::compliance
default_enabled: false
# frozen_string_literal: true
module API
module Entities
class GroupMergeRequestApprovalSetting < Grape::Entity
expose :allow_author_approval
end
end
end
# frozen_string_literal: true
module API
class GroupMergeRequestApprovalSettings < ::API::Base
feature_category :source_code_management
before do
authenticate!
not_found! unless ::Feature.enabled?(:group_merge_request_approval_settings_feature_flag, user_group)
end
params do
requires :id, type: String, desc: 'The ID of a group'
end
resource :groups, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
segment ':id/merge_request_approval_setting' do
desc 'Get group merge request approval setting' do
detail 'This feature is gated by the :group_merge_request_approval_settings_feature_flag'
success ::API::Entities::GroupMergeRequestApprovalSetting
end
get do
authorize! :admin_merge_request_approval_settings, user_group
present user_group.group_merge_request_approval_setting,
with: ::API::Entities::GroupMergeRequestApprovalSetting
end
end
end
end
end
...@@ -29,6 +29,7 @@ module EE ...@@ -29,6 +29,7 @@ module EE
mount ::API::GroupPushRule mount ::API::GroupPushRule
mount ::API::MergeTrains mount ::API::MergeTrains
mount ::API::GroupHooks mount ::API::GroupHooks
mount ::API::GroupMergeRequestApprovalSettings
mount ::API::Scim mount ::API::Scim
mount ::API::ManagedLicenses mount ::API::ManagedLicenses
mount ::API::ProjectApprovals mount ::API::ProjectApprovals
......
# frozen_string_literal: true
FactoryBot.define do
factory :group_merge_request_approval_setting do
group
end
end
{
"type": "object",
"properties": {
"allow_author_approval": { "type": "boolean" }
},
"additionalProperties": false
}
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GroupMergeRequestApprovalSetting do
describe 'Associations' do
it { is_expected.to belong_to :group }
end
describe 'Validations' do
let_it_be(:setting) { create(:group_merge_request_approval_setting) }
subject { setting }
it { is_expected.to validate_presence_of(:group) }
it { is_expected.not_to allow_value(nil).for(:allow_author_approval) }
it { is_expected.to allow_value(true, false).for(:allow_author_approval) }
end
end
...@@ -26,6 +26,7 @@ RSpec.describe Group do ...@@ -26,6 +26,7 @@ RSpec.describe Group do
it { is_expected.to have_many(:epic_boards).inverse_of(:group) } it { is_expected.to have_many(:epic_boards).inverse_of(:group) }
it { is_expected.to have_many(:provisioned_user_details).inverse_of(:provisioned_by_group) } it { is_expected.to have_many(:provisioned_user_details).inverse_of(:provisioned_by_group) }
it { is_expected.to have_many(:provisioned_users) } it { is_expected.to have_many(:provisioned_users) }
it { is_expected.to have_one(:group_merge_request_approval_setting) }
it_behaves_like 'model with wiki' do it_behaves_like 'model with wiki' do
let(:container) { create(:group, :nested, :wiki_repo) } let(:container) { create(:group, :nested, :wiki_repo) }
......
...@@ -1341,5 +1341,36 @@ RSpec.describe GroupPolicy do ...@@ -1341,5 +1341,36 @@ RSpec.describe GroupPolicy do
it_behaves_like 'read_group_release_stats permissions' it_behaves_like 'read_group_release_stats permissions'
end end
describe ':admin_merge_request_approval_settings' do
using RSpec::Parameterized::TableSyntax
let(:policy) { :admin_merge_request_approval_settings }
where(:role, :licensed, :allowed) do
:guest | true | false
:guest | false | false
:reporter | true | false
:reporter | false | false
:developer | true | false
:developer | false | false
:maintainer | true | false
:maintainer | false | false
:owner | true | true
:owner | false | false
:admin | true | true
:admin | false | false
end
with_them do
let(:current_user) { public_send(role) }
before do
stub_licensed_features(group_merge_request_approval_settings: licensed)
end
it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
end
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::GroupMergeRequestApprovalSettings do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
let_it_be(:setting) { create(:group_merge_request_approval_setting, group: group) }
let(:url) { "/groups/#{group.id}/merge_request_approval_setting" }
describe 'GET /groups/:id/merge_request_approval_settings' do
context 'when feature flag is disabled' do
before do
stub_feature_flags(group_merge_request_approval_settings_feature_flag: false)
end
it 'returns 404 status' do
get api(url, user)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when feature flag is enabled' do
before do
allow(Ability).to receive(:allowed?).and_call_original
stub_feature_flags(group_merge_request_approval_settings_feature_flag: true)
end
context 'when the user is authorised' do
before do
allow(Ability).to receive(:allowed?)
.with(user, :admin_merge_request_approval_settings, group)
.and_return(true)
end
it 'returns 200 status with correct response body', :aggregate_failures do
get api(url, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['allow_author_approval']).to eq(false)
end
it 'matches the response schema' do
get api(url, user)
expect(response).to match_response_schema('public_api/v4/group_merge_request_approval_settings', dir: 'ee')
end
end
context 'when the user is not authorised' do
before do
allow(Ability).to receive(:allowed?)
.with(user, :admin_merge_request_approval_settings, group)
.and_return(false)
end
it 'returns 403 status' do
get api(url, user)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment