Commit 3a770845 authored by Lee Tickett's avatar Lee Tickett Committed by Jan Provaznik

Add requirements visibility access project settings

parent 0d9d2706
......@@ -67,6 +67,11 @@ export default {
required: false,
default: false,
},
requirementsAvailable: {
type: Boolean,
required: false,
default: false,
},
visibilityHelpPath: {
type: String,
required: false,
......@@ -131,6 +136,7 @@ export default {
snippetsAccessLevel: featureAccessLevel.EVERYONE,
pagesAccessLevel: featureAccessLevel.EVERYONE,
metricsDashboardAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
requirementsAccessLevel: featureAccessLevel.EVERYONE,
containerRegistryEnabled: true,
lfsEnabled: true,
requestAccessEnabled: true,
......@@ -233,6 +239,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;
......@@ -256,6 +266,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();
}
},
......@@ -470,6 +483,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: {
......
......@@ -356,6 +356,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,
......@@ -391,22 +405,10 @@ class ProjectsController < Projects::ApplicationController
:initialize_with_readme,
:autoclose_referenced_issues,
:suggestion_commit_message,
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
]
]
] + [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
ProjectsHelper.prepend_if_ee('EE::ProjectFeaturesCompatibility')
---
title: Add requirements visibility/access project settings
merge_request: 34420
author: Lee Tickett
type: added
# 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
......@@ -13956,7 +13956,8 @@ CREATE TABLE public.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 public.project_features_id_seq
......@@ -23427,6 +23428,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200615121217
20200615123055
20200615193524
20200615203153
20200615232735
20200615234047
20200616145031
......
......@@ -1048,6 +1048,7 @@ POST /projects
| `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `pages_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
| `requirements_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
| `emails_disabled` | boolean | no | Disable email notifications |
| `show_default_award_emojis` | boolean | no | Show default award emojis |
| `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push |
......@@ -1119,6 +1120,7 @@ POST /projects/user/:user_id
| `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `pages_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
| `requirements_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
| `emails_disabled` | boolean | no | Disable email notifications |
| `show_default_award_emojis` | boolean | no | Show default award emojis |
| `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push |
......@@ -1189,6 +1191,7 @@ PUT /projects/:id
| `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `pages_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
| `requirements_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
| `emails_disabled` | boolean | no | Disable email notifications |
| `show_default_award_emojis` | boolean | no | Show default award emojis |
| `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push |
......
......@@ -62,6 +62,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: {
......
......@@ -49,6 +49,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
......
......@@ -53,6 +53,10 @@ module EE
nav_tabs << :project_insights
end
if can?(current_user, :read_requirement, project)
nav_tabs << :requirements
end
nav_tabs
end
......@@ -66,7 +70,8 @@ module EE
override :project_permissions_settings
def project_permissions_settings(project)
super.merge(
packagesEnabled: !!project.packages_enabled
packagesEnabled: !!project.packages_enabled,
requirementsAccessLevel: project.requirements_access_level
)
end
......@@ -74,7 +79,8 @@ module EE
def project_permissions_panel_data(project)
super.merge(
packagesAvailable: ::Gitlab.config.packages.enabled && project.feature_available?(:packages),
packagesHelpPath: help_page_path('user/packages/index')
packagesHelpPath: help_page_path('user/packages/index'),
requirementsAvailable: project.feature_available?(:requirements)
)
end
......
# 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
......@@ -167,6 +167,8 @@ module EE
delegate :merge_trains_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,
......@@ -184,6 +186,7 @@ module EE
end
default_value_for :packages_enabled, true
default_value_for :requirements_enabled, true
accepts_nested_attributes_for :tracing_setting, update_only: true, allow_destroy: true
accepts_nested_attributes_for :status_page_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(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
......@@ -34,7 +34,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 Feature.enabled?(:requirements_management, project, default_enabled: true)
- 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
......
......@@ -31,6 +31,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&.framework].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,33 +3,48 @@
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
it 'renders the index template' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
end
context 'when requirements_management flag is disabled' do
before do
stub_feature_flags(requirements_management: false)
end
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'when requirements_management flag is disabled' do
context 'when feature is not available' do
before do
stub_feature_flags(requirements_management: false)
stub_licensed_features(requirements: false)
end
it 'returns 404' do
......@@ -40,27 +55,46 @@ RSpec.describe Projects::RequirementsManagement::RequirementsController do
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
......@@ -69,14 +103,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
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(new_user_session_path)
context 'with unauthorized user' do
before do
sign_in(user)
end
it 'returns 404' do
subject
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
......
......@@ -76,6 +76,10 @@ FactoryBot.modify do
service_desk_enabled { true }
end
trait(:issues_enabled) do
issues_access_level { ProjectFeature::ENABLED }
end
trait :github_imported do
import_type { 'github' }
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' do
let(:current_user) { admin }
it { is_expected.to be_disallowed(*all_permissions) }
context 'with ' 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
......
......@@ -17782,6 +17782,12 @@ msgstr ""
msgid "ProjectSettings|Repository"
msgstr ""
msgid "ProjectSettings|Requirements"
msgstr ""
msgid "ProjectSettings|Requirements management system for this project"
msgstr ""
msgid "ProjectSettings|Share code pastes with others out of Git repository"
msgstr ""
......
......@@ -533,6 +533,7 @@ Project:
- merge_requests_enabled
- wiki_enabled
- snippets_enabled
- requirements_enabled
- visibility_level
- archived
- created_at
......@@ -600,6 +601,7 @@ ProjectFeature:
- repository_access_level
- pages_access_level
- metrics_dashboard_access_level
- requirements_access_level
- created_at
- updated_at
ProtectedBranch::MergeAccessLevel:
......
......@@ -80,6 +80,7 @@ RSpec.describe ProjectPolicy do
let(:additional_guest_permissions) { [] }
let(:additional_reporter_permissions) { [] }
let(:additional_maintainer_permissions) { [] }
let(:additional_owner_permissions) { [] }
let(:guest_permissions) { base_guest_permissions + additional_guest_permissions }
let(:reporter_permissions) { base_reporter_permissions + additional_reporter_permissions }
......
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