Commit 8f819a5f authored by Lee Tickett's avatar Lee Tickett Committed by Jan Provaznik

Add Requirements Visibility Setting RUN AS-IF-FOSS

parent 06b92492
......@@ -68,6 +68,11 @@ export default {
required: false,
default: false,
},
requirementsAvailable: {
type: Boolean,
required: false,
default: false,
},
visibilityHelpPath: {
type: String,
required: false,
......@@ -132,6 +137,7 @@ export default {
snippetsAccessLevel: featureAccessLevel.EVERYONE,
pagesAccessLevel: featureAccessLevel.EVERYONE,
metricsDashboardAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
requirementsAccessLevel: featureAccessLevel.EVERYONE,
containerRegistryEnabled: true,
lfsEnabled: true,
requestAccessEnabled: true,
......@@ -234,6 +240,10 @@ export default {
featureAccessLevel.PROJECT_MEMBERS,
this.metricsDashboardAccessLevel,
);
this.requirementsAccessLevel = Math.min(
featureAccessLevel.PROJECT_MEMBERS,
this.requirementsAccessLevel,
);
if (this.pagesAccessLevel === featureAccessLevel.EVERYONE) {
// When from Internal->Private narrow access for only members
this.pagesAccessLevel = featureAccessLevel.PROJECT_MEMBERS;
......@@ -257,6 +267,9 @@ export default {
this.pagesAccessLevel = featureAccessLevel.EVERYONE;
if (this.metricsDashboardAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
this.metricsDashboardAccessLevel = featureAccessLevel.EVERYONE;
if (this.requirementsAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
this.requirementsAccessLevel = featureAccessLevel.EVERYONE;
this.highlightChanges();
}
},
......@@ -481,6 +494,18 @@ export default {
/>
</project-setting-row>
</div>
<project-setting-row
v-if="requirementsAvailable"
ref="requirements-settings"
:label="s__('ProjectSettings|Requirements')"
:help-text="s__('ProjectSettings|Requirements management system for this project')"
>
<project-feature-setting
v-model="requirementsAccessLevel"
:options="featureAccessLevelOptions"
name="project[project_feature_attributes][requirements_access_level]"
/>
</project-setting-row>
<project-setting-row
ref="wiki-settings"
:label="s__('ProjectSettings|Wiki')"
......
......@@ -2,6 +2,7 @@ export default {
data() {
return {
packagesEnabled: false,
requirementsEnabled: false,
};
},
watch: {
......
......@@ -381,6 +381,20 @@ class ProjectsController < Projects::ApplicationController
.merge(import_url_params)
end
def project_feature_attributes
%i[
builds_access_level
issues_access_level
forking_access_level
merge_requests_access_level
repository_access_level
snippets_access_level
wiki_access_level
pages_access_level
metrics_dashboard_access_level
]
end
def project_params_attributes
[
:allow_merge_on_skipped_pipeline,
......@@ -418,23 +432,11 @@ class ProjectsController < Projects::ApplicationController
:suggestion_commit_message,
:packages_enabled,
:service_desk_enabled,
project_feature_attributes: %i[
builds_access_level
issues_access_level
forking_access_level
merge_requests_access_level
repository_access_level
snippets_access_level
wiki_access_level
pages_access_level
metrics_dashboard_access_level
],
project_setting_attributes: %i[
show_default_award_emojis
squash_option
]
]
] + [project_feature_attributes: project_feature_attributes]
end
def project_params_create_attributes
......
......@@ -37,7 +37,8 @@ module Featurable
class_methods do
def set_available_features(available_features = [])
@available_features = available_features
@available_features ||= []
@available_features += available_features
class_eval do
available_features.each do |feature|
......
......@@ -88,3 +88,5 @@ module ProjectFeaturesCompatibility
project_feature.__send__(:write_attribute, field, value) # rubocop:disable GitlabSecurity/PublicSend
end
end
ProjectFeaturesCompatibility.prepend_if_ee('EE::ProjectFeaturesCompatibility')
# frozen_string_literal: true
class AddRequirementsAccessLevelToProjectFeatures < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_column :project_features, :requirements_access_level, :integer, default: 20, null: false
end
end
def down
with_lock_retries do
remove_column :project_features, :requirements_access_level, :integer
end
end
end
ced03562d300f99abf687c258a25bf280a6c4f1798a893ee8a79189c09f19e6e
\ No newline at end of file
......@@ -14992,7 +14992,8 @@ CREATE TABLE project_features (
repository_access_level integer DEFAULT 20 NOT NULL,
pages_access_level integer NOT NULL,
forking_access_level integer,
metrics_dashboard_access_level integer
metrics_dashboard_access_level integer,
requirements_access_level integer DEFAULT 20 NOT NULL
);
CREATE SEQUENCE project_features_id_seq
......
......@@ -1081,6 +1081,7 @@ POST /projects
| `only_allow_merge_if_pipeline_succeeds` | boolean | **{dotted-circle}** No | Set whether merge requests can only be merged with successful jobs. |
| `packages_enabled` | boolean | **{dotted-circle}** No | Enable or disable packages repository feature. |
| `pages_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private`, `enabled`, or `public`. |
| `requirements_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private`, `enabled` or `public` |
| `path` | string | **{check-circle}** Yes (if name isn't provided) | Repository name for new project. Generated based on name if not provided (generated as lowercase with dashes). |
| `printing_merge_request_link_enabled` | boolean | **{dotted-circle}** No | Show link to create/view merge request when pushing from the command line. |
| `public_builds` | boolean | **{dotted-circle}** No | If `true`, jobs can be viewed by non-project members. |
......@@ -1150,6 +1151,7 @@ POST /projects/user/:user_id
| `only_allow_merge_if_pipeline_succeeds` | boolean | **{dotted-circle}** No | Set whether merge requests can only be merged with successful jobs. |
| `packages_enabled` | boolean | **{dotted-circle}** No | Enable or disable packages repository feature. |
| `pages_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private`, `enabled`, or `public`. |
| `requirements_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private`, `enabled` or `public` |
| `path` | string | **{dotted-circle}** No | Custom repository name for new project. By default generated based on name. |
| `printing_merge_request_link_enabled` | boolean | **{dotted-circle}** No | Show link to create/view merge request when pushing from the command line. |
| `public_builds` | boolean | **{dotted-circle}** No | If `true`, jobs can be viewed by non-project-members. |
......@@ -1225,6 +1227,7 @@ PUT /projects/:id
| `only_mirror_protected_branches` **(STARTER)** | boolean | **{dotted-circle}** No | Only mirror protected branches. |
| `packages_enabled` | boolean | **{dotted-circle}** No | Enable or disable packages repository feature. |
| `pages_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private`, `enabled`, or `public`. |
| `requirements_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private`, `enabled` or `public` |
| `path` | string | **{dotted-circle}** No | Custom repository name for the project. By default generated based on name. |
| `public_builds` | boolean | **{dotted-circle}** No | If `true`, jobs can be viewed by non-project members. |
| `remove_source_branch_after_merge` | boolean | **{dotted-circle}** No | Enable `Delete source branch` option by default for all new merge requests. |
......
......@@ -72,6 +72,7 @@ Use the switches to enable or disable the following features:
| **Snippets** | ✓ | Enables [sharing of code and text](../../snippets.md) |
| **Pages** | ✓ | Allows you to [publish static websites](../pages/) |
| **Metrics Dashboard** | ✓ | Control access to [metrics dashboard](../integrations/prometheus.md)
| **Requirements** | ✓ | Control access to [Requirements Management](../requirements/index.md)
Some features depend on others:
......
......@@ -2,6 +2,7 @@ export default {
data() {
return {
packagesEnabled: true,
requirementsEnabled: true,
};
},
watch: {
......
......@@ -47,6 +47,11 @@ module EE
end
end
override :project_feature_attributes
def project_feature_attributes
super + [:requirements_access_level]
end
override :project_params_attributes
def project_params_attributes
super + project_params_ee
......
......@@ -39,9 +39,27 @@ module EE
nav_tabs << :project_insights
end
if can?(current_user, :read_requirement, project)
nav_tabs << :requirements
end
nav_tabs
end
override :project_permissions_settings
def project_permissions_settings(project)
super.merge(
requirementsAccessLevel: project.requirements_access_level
)
end
override :project_permissions_panel_data
def project_permissions_panel_data(project)
super.merge(
requirementsAvailable: project.feature_available?(:requirements)
)
end
override :default_url_to_repo
def default_url_to_repo(project = @project)
case default_clone_protocol
......
# frozen_string_literal: true
module EE
module ProjectFeaturesCompatibility
extend ActiveSupport::Concern
# TODO: remove in API v5, replaced by *_access_level
def requirements_enabled=(value)
write_feature_attribute_boolean(:requirements_access_level, value)
end
def requirements_access_level=(value)
write_feature_attribute_string(:requirements_access_level, value)
end
end
end
......@@ -190,6 +190,8 @@ module EE
delegate :auto_rollback_enabled, :auto_rollback_enabled=, :auto_rollback_enabled?, to: :ci_cd_settings
delegate :closest_gitlab_subscription, to: :namespace
delegate :requirements_access_level, to: :project_feature, allow_nil: true
validates :repository_size_limit,
numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true }
validates :max_pages_size,
......@@ -206,6 +208,17 @@ module EE
validates :mirror_user, presence: true
end
# Because we use default_value_for we need to be sure
# requirements_enabled= method does exist even if we rollback migration.
# Otherwise many tests from spec/migrations will fail.
def requirements_enabled=(value)
if has_attribute?(:requirements_enabled)
write_attribute(:requirements_enabled, value)
end
end
default_value_for :requirements_enabled, true
accepts_nested_attributes_for :status_page_setting, update_only: true, allow_destroy: true
accepts_nested_attributes_for :compliance_framework_setting, update_only: true, allow_destroy: true
......
......@@ -4,11 +4,17 @@ module EE
module ProjectFeature
extend ActiveSupport::Concern
EE_FEATURES = %i(requirements).freeze
prepended do
set_available_features(EE_FEATURES)
# Ensure changes to project visibility settings go to elasticsearch
after_commit on: :update do
project.maintain_elasticsearch_update if project.maintaining_elasticsearch?
end
default_value_for :requirements_access_level, value: Featurable::ENABLED, allows_nil: false
end
end
end
......@@ -16,7 +16,7 @@ module EE
condition(:iterations_available) { @subject.feature_available?(:iterations) }
with_scope :subject
condition(:requirements_available) { @subject.feature_available?(:requirements) }
condition(:requirements_available) { @subject.feature_available?(:requirements) & feature_available?(:requirements) }
condition(:compliance_framework_available) { @subject.feature_available?(:compliance_framework, @user) }
......
- return unless can?(current_user, :read_requirement, project)
- return unless project_nav_tab? :requirements
= nav_link(path: 'requirements#index') do
= link_to project_requirements_management_requirements_path(project), class: 'qa-project-requirements-link' do
......
---
title: Add requirements visibility access project setting
merge_request: 46532
author: Lee Tickett
type: added
......@@ -28,6 +28,9 @@ module EE
expose :marked_for_deletion_on, if: ->(project, _) { project.feature_available?(:adjourned_deletion_for_projects_and_groups) } do |project, _|
project.marked_for_deletion_at
end
expose :requirements_enabled do |project, options|
project.feature_available?(:requirements, options[:current_user])
end
expose :compliance_frameworks do |project, _|
[project.compliance_framework_setting&.compliance_management_framework&.name].compact
end
......
......@@ -13,7 +13,8 @@ module EE
:builds_access_level,
:repository_access_level,
:pages_access_level,
:metrics_dashboard_access_level].freeze
:metrics_dashboard_access_level,
:requirements_access_level].freeze
def initialize(current_user, model, project)
@project = project
......
......@@ -3,52 +3,86 @@
require 'spec_helper'
RSpec.describe Projects::RequirementsManagement::RequirementsController do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
subject { get :index, params: { namespace_id: project.namespace, project_id: project } }
describe 'GET #index' do
context 'with authorized user' do
before do
project.add_developer(user)
sign_in(user)
end
context 'private project' do
let(:project) { create(:project) }
context 'when feature is available' do
context 'with authorized user' do
before do
stub_licensed_features(requirements: true)
project.add_developer(user)
sign_in(user)
end
it 'renders the index template' do
subject
context 'when feature is available' do
before do
stub_licensed_features(requirements: true)
end
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
it 'renders the index template' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
end
end
context 'when feature is not available' do
before do
stub_licensed_features(requirements: false)
end
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'when feature is not available' do
context 'with unauthorized user' do
before do
stub_licensed_features(requirements: false)
sign_in(user)
end
it 'returns 404' do
context 'when feature is available' do
before do
stub_licensed_features(requirements: true)
end
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'with anonymous user' do
it 'returns 302' do
subject
expect(response).to have_gitlab_http_status(:not_found)
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(new_user_session_path)
end
end
end
context 'with unauthorized user' do
context 'public project' do
let(:project) { create(:project, :public) }
before do
sign_in(user)
stub_licensed_features(requirements: true)
end
context 'when feature is available' do
context 'with requirements disabled' do
before do
stub_licensed_features(requirements: true)
project.project_feature.update!({ requirements_access_level: ::ProjectFeature::DISABLED })
project.add_developer(user)
sign_in(user)
end
it 'returns 404' do
......@@ -57,14 +91,52 @@ RSpec.describe Projects::RequirementsManagement::RequirementsController do
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'with anonymous user' do
it 'returns 302' do
subject
context 'with requirements visible to project memebers' do
before do
project.project_feature.update!({ requirements_access_level: ::ProjectFeature::PRIVATE })
end
context 'with authorized user' do
before do
project.add_developer(user)
sign_in(user)
end
it 'renders the index template' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
end
end
context 'with unauthorized user' do
before do
sign_in(user)
end
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(new_user_session_path)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'with requirements visible to everyone' do
before do
project.project_feature.update!({ requirements_access_level: ::ProjectFeature::ENABLED })
end
context 'with anonymous user' do
it 'renders the index template' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
end
end
end
end
end
......
# frozen_string_literal: true
RSpec.shared_examples 'resource with requirement permissions' do
include AdminModeHelper
let(:all_permissions) do
[:read_requirement, :create_requirement, :admin_requirement,
:update_requirement, :destroy_requirement,
......@@ -77,6 +79,82 @@ RSpec.shared_examples 'resource with requirement permissions' do
it { is_expected.to be_disallowed(*all_permissions) }
end
end
context 'when access level is disabled' do
before do
parent = resource.is_a?(Project) ? resource : resource.resource_parent
parent.project_feature.update!(requirements_access_level: ProjectFeature::DISABLED)
end
context 'with owner' do
let(:current_user) { owner }
it { is_expected.to be_disallowed(*all_permissions) }
end
context 'with admin' do
let(:current_user) { admin }
it { is_expected.to be_disallowed(*all_permissions) }
end
end
context 'when access level is private' do
before do
parent = resource.is_a?(Project) ? resource : resource.resource_parent
parent.project_feature.update!(requirements_access_level: ProjectFeature::PRIVATE)
end
context 'with admin user' do
let(:current_user) { admin }
it { is_expected.to be_disallowed(*all_permissions) }
context 'with admin mode enabled' do
before do
enable_admin_mode!(current_user)
end
it_behaves_like 'user with read only permissions'
end
end
context 'with owner' do
let(:current_user) { owner }
it { is_expected.to be_allowed(*all_permissions) }
end
context 'with maintainer' do
let(:current_user) { maintainer }
it_behaves_like 'user with manage permissions'
end
context 'with developer' do
let(:current_user) { developer }
it_behaves_like 'user with manage permissions'
end
context 'with reporter' do
let(:current_user) { reporter }
it_behaves_like 'user with manage permissions'
end
context 'with guest' do
let(:current_user) { guest }
it_behaves_like 'user with read only permissions'
end
context 'with non member' do
let(:current_user) { create(:user) }
it { is_expected.to be_disallowed(*all_permissions) }
end
end
end
context 'when requirements feature is disabled' do
......
......@@ -21108,6 +21108,12 @@ msgstr ""
msgid "ProjectSettings|Require"
msgstr ""
msgid "ProjectSettings|Requirements"
msgstr ""
msgid "ProjectSettings|Requirements management system for this project"
msgstr ""
msgid "ProjectSettings|Set the default behavior and availability of this option in merge requests. Changes made are also applied to existing merge requests."
msgstr ""
......
......@@ -681,13 +681,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
end
it 'overrides project feature access levels' do
access_level_keys = project.project_feature.attributes.keys.select { |a| a =~ /_access_level/ }
# `pages_access_level` is not included, since it is not available in the public API
# and has a dependency on project's visibility level
# see ProjectFeature model
access_level_keys.delete('pages_access_level')
access_level_keys = ProjectFeature.available_features.map { |feature| ProjectFeature.access_level_attribute(feature) }
disabled_access_levels = Hash[access_level_keys.collect { |item| [item, 'disabled'] }]
project.create_import_data(data: { override_params: disabled_access_levels })
......
......@@ -575,6 +575,7 @@ ProjectFeature:
- repository_access_level
- pages_access_level
- metrics_dashboard_access_level
- requirements_access_level
- created_at
- updated_at
ProtectedBranch::MergeAccessLevel:
......
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