Commit 07dd6338 authored by Francisco Javier López's avatar Francisco Javier López Committed by James Lopez

Adding Operations project setting logic

In this commit we add the BE logic for the new setting
'Operations' withint the project settings.
parent 87a0c544
...@@ -386,6 +386,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -386,6 +386,7 @@ class ProjectsController < Projects::ApplicationController
wiki_access_level wiki_access_level
pages_access_level pages_access_level
metrics_dashboard_access_level metrics_dashboard_access_level
operations_access_level
] ]
end end
......
...@@ -468,6 +468,8 @@ module ProjectsHelper ...@@ -468,6 +468,8 @@ module ProjectsHelper
end end
def can_view_operations_tab?(current_user, project) def can_view_operations_tab?(current_user, project)
return false unless project.feature_available?(:operations, current_user)
[ [
:metrics_dashboard, :metrics_dashboard,
:read_alert_management_alert, :read_alert_management_alert,
...@@ -622,6 +624,7 @@ module ProjectsHelper ...@@ -622,6 +624,7 @@ module ProjectsHelper
lfsEnabled: !!project.lfs_enabled, lfsEnabled: !!project.lfs_enabled,
emailsDisabled: project.emails_disabled?, emailsDisabled: project.emails_disabled?,
metricsDashboardAccessLevel: feature.metrics_dashboard_access_level, metricsDashboardAccessLevel: feature.metrics_dashboard_access_level,
operationsAccessLevel: feature.operations_access_level,
showDefaultAwardEmojis: project.show_default_award_emojis? showDefaultAwardEmojis: project.show_default_award_emojis?
} }
end end
......
...@@ -70,6 +70,10 @@ module ProjectFeaturesCompatibility ...@@ -70,6 +70,10 @@ module ProjectFeaturesCompatibility
write_feature_attribute_string(:metrics_dashboard_access_level, value) write_feature_attribute_string(:metrics_dashboard_access_level, value)
end end
def operations_access_level=(value)
write_feature_attribute_string(:operations_access_level, value)
end
private private
def write_feature_attribute_boolean(field, value) def write_feature_attribute_boolean(field, value)
......
...@@ -388,7 +388,7 @@ class Project < ApplicationRecord ...@@ -388,7 +388,7 @@ class Project < ApplicationRecord
:merge_requests_access_level, :forking_access_level, :issues_access_level, :merge_requests_access_level, :forking_access_level, :issues_access_level,
:wiki_access_level, :snippets_access_level, :builds_access_level, :wiki_access_level, :snippets_access_level, :builds_access_level,
:repository_access_level, :pages_access_level, :metrics_dashboard_access_level, :repository_access_level, :pages_access_level, :metrics_dashboard_access_level,
to: :project_feature, allow_nil: true :operations_enabled?, :operations_access_level, to: :project_feature, allow_nil: true
delegate :show_default_award_emojis, :show_default_award_emojis=, delegate :show_default_award_emojis, :show_default_award_emojis=,
:show_default_award_emojis?, :show_default_award_emojis?,
to: :project_setting, allow_nil: true to: :project_setting, allow_nil: true
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
class ProjectFeature < ApplicationRecord class ProjectFeature < ApplicationRecord
include Featurable include Featurable
FEATURES = %i(issues forking merge_requests wiki snippets builds repository pages metrics_dashboard).freeze FEATURES = %i(issues forking merge_requests wiki snippets builds repository pages metrics_dashboard operations).freeze
set_available_features(FEATURES) set_available_features(FEATURES)
...@@ -45,6 +45,7 @@ class ProjectFeature < ApplicationRecord ...@@ -45,6 +45,7 @@ class ProjectFeature < ApplicationRecord
default_value_for :wiki_access_level, value: ENABLED, allows_nil: false default_value_for :wiki_access_level, value: ENABLED, allows_nil: false
default_value_for :repository_access_level, value: ENABLED, allows_nil: false default_value_for :repository_access_level, value: ENABLED, allows_nil: false
default_value_for :metrics_dashboard_access_level, value: PRIVATE, allows_nil: false default_value_for :metrics_dashboard_access_level, value: PRIVATE, allows_nil: false
default_value_for :operations_access_level, value: ENABLED, allows_nil: false
default_value_for(:pages_access_level, allows_nil: false) do |feature| default_value_for(:pages_access_level, allows_nil: false) do |feature|
if ::Gitlab::Pages.access_control_is_forced? if ::Gitlab::Pages.access_control_is_forced?
......
...@@ -147,6 +147,7 @@ class ProjectPolicy < BasePolicy ...@@ -147,6 +147,7 @@ class ProjectPolicy < BasePolicy
builds builds
pages pages
metrics_dashboard metrics_dashboard
operations
] ]
features.each do |f| features.each do |f|
...@@ -272,6 +273,19 @@ class ProjectPolicy < BasePolicy ...@@ -272,6 +273,19 @@ class ProjectPolicy < BasePolicy
prevent(:metrics_dashboard) prevent(:metrics_dashboard)
end end
rule { operations_disabled }.policy do
prevent(*create_read_update_admin_destroy(:feature_flag))
prevent(*create_read_update_admin_destroy(:environment))
prevent(*create_read_update_admin_destroy(:sentry_issue))
prevent(*create_read_update_admin_destroy(:alert_management_alert))
prevent(*create_read_update_admin_destroy(:cluster))
prevent(*create_read_update_admin_destroy(:terraform_state))
prevent(*create_read_update_admin_destroy(:deployment))
prevent(:metrics_dashboard)
prevent(:read_pod_logs)
prevent(:read_prometheus)
end
rule { can?(:metrics_dashboard) }.policy do rule { can?(:metrics_dashboard) }.policy do
enable :read_prometheus enable :read_prometheus
enable :read_deployment enable :read_deployment
......
---
title: Add Operations project setting logic
merge_request: 48347
author:
type: added
---
name: operations
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48347
rollout_issue_url:
milestone: '13.7'
type: development
group: group::editor
default_enabled: true
# frozen_string_literal: true
class AddOperationsProjectFeatureToMetrics < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_column :project_features, :operations_access_level, :integer, default: 20, null: false
end
end
def down
with_lock_retries do
remove_column :project_features, :operations_access_level
end
end
end
9b212f5fd6f58123f0d46249c82b2da49af9bcdd36bcc0de610c4be186b17345
\ No newline at end of file
...@@ -15126,7 +15126,8 @@ CREATE TABLE project_features ( ...@@ -15126,7 +15126,8 @@ CREATE TABLE project_features (
pages_access_level integer NOT NULL, pages_access_level integer NOT NULL,
forking_access_level integer, forking_access_level integer,
metrics_dashboard_access_level integer, metrics_dashboard_access_level integer,
requirements_access_level integer DEFAULT 20 NOT NULL requirements_access_level integer DEFAULT 20 NOT NULL,
operations_access_level integer DEFAULT 20 NOT NULL
); );
CREATE SEQUENCE project_features_id_seq CREATE SEQUENCE project_features_id_seq
......
...@@ -14,6 +14,7 @@ module EE ...@@ -14,6 +14,7 @@ module EE
:repository_access_level, :repository_access_level,
:pages_access_level, :pages_access_level,
:metrics_dashboard_access_level, :metrics_dashboard_access_level,
:operations_access_level,
:requirements_access_level].freeze :requirements_access_level].freeze
def initialize(current_user, model, project) def initialize(current_user, model, project)
......
...@@ -32,6 +32,7 @@ FactoryBot.define do ...@@ -32,6 +32,7 @@ FactoryBot.define do
visibility_level == Gitlab::VisibilityLevel::PUBLIC ? ProjectFeature::ENABLED : ProjectFeature::PRIVATE visibility_level == Gitlab::VisibilityLevel::PUBLIC ? ProjectFeature::ENABLED : ProjectFeature::PRIVATE
end end
metrics_dashboard_access_level { ProjectFeature::PRIVATE } metrics_dashboard_access_level { ProjectFeature::PRIVATE }
operations_access_level { ProjectFeature::ENABLED }
# we can't assign the delegated `#ci_cd_settings` attributes directly, as the # we can't assign the delegated `#ci_cd_settings` attributes directly, as the
# `#ci_cd_settings` relation needs to be created first # `#ci_cd_settings` relation needs to be created first
...@@ -57,7 +58,8 @@ FactoryBot.define do ...@@ -57,7 +58,8 @@ FactoryBot.define do
merge_requests_access_level: merge_requests_access_level, merge_requests_access_level: merge_requests_access_level,
repository_access_level: evaluator.repository_access_level, repository_access_level: evaluator.repository_access_level,
pages_access_level: evaluator.pages_access_level, pages_access_level: evaluator.pages_access_level,
metrics_dashboard_access_level: evaluator.metrics_dashboard_access_level metrics_dashboard_access_level: evaluator.metrics_dashboard_access_level,
operations_access_level: evaluator.operations_access_level
} }
project.build_project_feature(hash) project.build_project_feature(hash)
...@@ -322,6 +324,9 @@ FactoryBot.define do ...@@ -322,6 +324,9 @@ FactoryBot.define do
trait(:metrics_dashboard_enabled) { metrics_dashboard_access_level { ProjectFeature::ENABLED } } trait(:metrics_dashboard_enabled) { metrics_dashboard_access_level { ProjectFeature::ENABLED } }
trait(:metrics_dashboard_disabled) { metrics_dashboard_access_level { ProjectFeature::DISABLED } } trait(:metrics_dashboard_disabled) { metrics_dashboard_access_level { ProjectFeature::DISABLED } }
trait(:metrics_dashboard_private) { metrics_dashboard_access_level { ProjectFeature::PRIVATE } } trait(:metrics_dashboard_private) { metrics_dashboard_access_level { ProjectFeature::PRIVATE } }
trait(:operations_enabled) { operations_access_level { ProjectFeature::ENABLED } }
trait(:operations_disabled) { operations_access_level { ProjectFeature::DISABLED } }
trait(:operations_private) { operations_access_level { ProjectFeature::PRIVATE } }
trait :auto_devops do trait :auto_devops do
association :auto_devops, factory: :project_auto_devops association :auto_devops, factory: :project_auto_devops
......
...@@ -2,31 +2,88 @@ ...@@ -2,31 +2,88 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Operations dropdown sidebar' do RSpec.describe 'Operations dropdown sidebar', :aggregate_failures do
let_it_be(:project) { create(:project, :repository) } let_it_be_with_reload(:project) { create(:project, :internal, :repository) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:access_level) { ProjectFeature::PUBLIC }
let(:role) { nil }
before do before do
project.add_role(user, role) project.add_role(user, role) if role
project.project_feature.update_attribute(:operations_access_level, access_level)
sign_in(user) sign_in(user)
visit project_issues_path(project) visit project_issues_path(project)
end end
shared_examples 'shows Operations menu based on the access level' do
context 'when operations project feature is PRIVATE' do
let(:access_level) { ProjectFeature::PRIVATE }
it 'shows the `Operations` menu' do
expect(page).to have_selector('a.shortcuts-operations', text: 'Operations')
end
end
context 'when operations project feature is DISABLED' do
let(:access_level) { ProjectFeature::DISABLED }
it 'does not show the `Operations` menu' do
expect(page).not_to have_selector('a.shortcuts-operations')
end
end
end
context 'user is not a member' do
it 'has the correct `Operations` menu items', :aggregate_failures do
expect(page).to have_selector('a.shortcuts-operations', text: 'Operations')
expect(page).to have_link(title: 'Incidents', href: project_incidents_path(project))
expect(page).to have_link(title: 'Environments', href: project_environments_path(project))
expect(page).not_to have_link(title: 'Metrics', href: project_metrics_dashboard_path(project))
expect(page).not_to have_link(title: 'Alerts', href: project_alert_management_index_path(project))
expect(page).not_to have_link(title: 'Error Tracking', href: project_error_tracking_index_path(project))
expect(page).not_to have_link(title: 'Product Analytics', href: project_product_analytics_path(project))
expect(page).not_to have_link(title: 'Serverless', href: project_serverless_functions_path(project))
expect(page).not_to have_link(title: 'Logs', href: project_logs_path(project))
expect(page).not_to have_link(title: 'Kubernetes', href: project_clusters_path(project))
end
context 'when operations project feature is PRIVATE' do
let(:access_level) { ProjectFeature::PRIVATE }
it 'does not show the `Operations` menu' do
expect(page).not_to have_selector('a.shortcuts-operations')
end
end
context 'when operations project feature is DISABLED' do
let(:access_level) { ProjectFeature::DISABLED }
it 'does not show the `Operations` menu' do
expect(page).not_to have_selector('a.shortcuts-operations')
end
end
end
context 'user has guest role' do context 'user has guest role' do
let(:role) { :guest } let(:role) { :guest }
it 'has the correct `Operations` menu items' do it 'has the correct `Operations` menu items' do
expect(page).to have_selector('a.shortcuts-operations', text: 'Operations')
expect(page).to have_link(title: 'Incidents', href: project_incidents_path(project)) expect(page).to have_link(title: 'Incidents', href: project_incidents_path(project))
expect(page).to have_link(title: 'Environments', href: project_environments_path(project))
expect(page).not_to have_link(title: 'Metrics', href: project_metrics_dashboard_path(project)) expect(page).not_to have_link(title: 'Metrics', href: project_metrics_dashboard_path(project))
expect(page).not_to have_link(title: 'Alerts', href: project_alert_management_index_path(project)) expect(page).not_to have_link(title: 'Alerts', href: project_alert_management_index_path(project))
expect(page).not_to have_link(title: 'Environments', href: project_environments_path(project))
expect(page).not_to have_link(title: 'Error Tracking', href: project_error_tracking_index_path(project)) expect(page).not_to have_link(title: 'Error Tracking', href: project_error_tracking_index_path(project))
expect(page).not_to have_link(title: 'Product Analytics', href: project_product_analytics_path(project)) expect(page).not_to have_link(title: 'Product Analytics', href: project_product_analytics_path(project))
expect(page).not_to have_link(title: 'Serverless', href: project_serverless_functions_path(project)) expect(page).not_to have_link(title: 'Serverless', href: project_serverless_functions_path(project))
expect(page).not_to have_link(title: 'Logs', href: project_logs_path(project)) expect(page).not_to have_link(title: 'Logs', href: project_logs_path(project))
expect(page).not_to have_link(title: 'Kubernetes', href: project_clusters_path(project)) expect(page).not_to have_link(title: 'Kubernetes', href: project_clusters_path(project))
end end
it_behaves_like 'shows Operations menu based on the access level'
end end
context 'user has reporter role' do context 'user has reporter role' do
...@@ -44,6 +101,8 @@ RSpec.describe 'Operations dropdown sidebar' do ...@@ -44,6 +101,8 @@ RSpec.describe 'Operations dropdown sidebar' do
expect(page).not_to have_link(title: 'Logs', href: project_logs_path(project)) expect(page).not_to have_link(title: 'Logs', href: project_logs_path(project))
expect(page).not_to have_link(title: 'Kubernetes', href: project_clusters_path(project)) expect(page).not_to have_link(title: 'Kubernetes', href: project_clusters_path(project))
end end
it_behaves_like 'shows Operations menu based on the access level'
end end
context 'user has developer role' do context 'user has developer role' do
...@@ -61,6 +120,8 @@ RSpec.describe 'Operations dropdown sidebar' do ...@@ -61,6 +120,8 @@ RSpec.describe 'Operations dropdown sidebar' do
expect(page).not_to have_link(title: 'Serverless', href: project_serverless_functions_path(project)) expect(page).not_to have_link(title: 'Serverless', href: project_serverless_functions_path(project))
expect(page).not_to have_link(title: 'Kubernetes', href: project_clusters_path(project)) expect(page).not_to have_link(title: 'Kubernetes', href: project_clusters_path(project))
end end
it_behaves_like 'shows Operations menu based on the access level'
end end
context 'user has maintainer role' do context 'user has maintainer role' do
...@@ -77,5 +138,7 @@ RSpec.describe 'Operations dropdown sidebar' do ...@@ -77,5 +138,7 @@ RSpec.describe 'Operations dropdown sidebar' do
expect(page).to have_link(title: 'Logs', href: project_logs_path(project)) expect(page).to have_link(title: 'Logs', href: project_logs_path(project))
expect(page).to have_link(title: 'Kubernetes', href: project_clusters_path(project)) expect(page).to have_link(title: 'Kubernetes', href: project_clusters_path(project))
end end
it_behaves_like 'shows Operations menu based on the access level'
end end
end end
...@@ -488,6 +488,7 @@ RSpec.describe ProjectsHelper do ...@@ -488,6 +488,7 @@ RSpec.describe ProjectsHelper do
describe '#can_view_operations_tab?' do describe '#can_view_operations_tab?' do
before do before do
allow(helper).to receive(:current_user).and_return(user) allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?).and_return(false)
end end
subject { helper.send(:can_view_operations_tab?, user, project) } subject { helper.send(:can_view_operations_tab?, user, project) }
...@@ -501,11 +502,19 @@ RSpec.describe ProjectsHelper do ...@@ -501,11 +502,19 @@ RSpec.describe ProjectsHelper do
:read_cluster :read_cluster
].each do |ability| ].each do |ability|
it 'includes operations tab' do it 'includes operations tab' do
allow(helper).to receive(:can?).and_return(false)
allow(helper).to receive(:can?).with(user, ability, project).and_return(true) allow(helper).to receive(:can?).with(user, ability, project).and_return(true)
is_expected.to be(true) is_expected.to be(true)
end end
context 'when operations feature is disabled' do
it 'does not include operations tab' do
allow(helper).to receive(:can?).with(user, ability, project).and_return(true)
project.project_feature.update_attribute(:operations_access_level, ProjectFeature::DISABLED)
is_expected.to be(false)
end
end
end end
end end
......
...@@ -577,6 +577,7 @@ ProjectFeature: ...@@ -577,6 +577,7 @@ ProjectFeature:
- pages_access_level - pages_access_level
- metrics_dashboard_access_level - metrics_dashboard_access_level
- requirements_access_level - requirements_access_level
- operations_access_level
- created_at - created_at
- updated_at - updated_at
ProtectedBranch::MergeAccessLevel: ProtectedBranch::MergeAccessLevel:
......
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe ProjectFeaturesCompatibility do RSpec.describe ProjectFeaturesCompatibility do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:features_enabled) { %w(issues wiki builds merge_requests snippets) } let(:features_enabled) { %w(issues wiki builds merge_requests snippets) }
let(:features) { features_enabled + %w(repository pages) } let(:features) { features_enabled + %w(repository pages operations) }
# We had issues_enabled, snippets_enabled, builds_enabled, merge_requests_enabled and issues_enabled fields on projects table # We had issues_enabled, snippets_enabled, builds_enabled, merge_requests_enabled and issues_enabled fields on projects table
# All those fields got moved to a new table called project_feature and are now integers instead of booleans # All those fields got moved to a new table called project_feature and are now integers instead of booleans
......
...@@ -40,7 +40,7 @@ RSpec.describe ProjectFeature do ...@@ -40,7 +40,7 @@ RSpec.describe ProjectFeature do
end end
context 'public features' do context 'public features' do
features = %w(issues wiki builds merge_requests snippets repository metrics_dashboard) features = %w(issues wiki builds merge_requests snippets repository metrics_dashboard operations)
features.each do |feature| features.each do |feature|
it "does not allow public access level for #{feature}" do it "does not allow public access level for #{feature}" do
......
...@@ -944,4 +944,116 @@ RSpec.describe ProjectPolicy do ...@@ -944,4 +944,116 @@ RSpec.describe ProjectPolicy do
end end
it_behaves_like 'Self-managed Core resource access tokens' it_behaves_like 'Self-managed Core resource access tokens'
describe 'operations feature' do
using RSpec::Parameterized::TableSyntax
let(:guest_operations_permissions) { [:read_environment, :read_deployment] }
let(:developer_operations_permissions) do
guest_operations_permissions + [
:read_feature_flag, :read_sentry_issue, :read_alert_management_alert, :read_terraform_state,
:metrics_dashboard, :read_pod_logs, :read_prometheus, :create_feature_flag,
:create_environment, :create_deployment, :update_feature_flag, :update_environment,
:update_sentry_issue, :update_alert_management_alert, :update_deployment,
:destroy_feature_flag, :destroy_environment, :admin_feature_flag
]
end
let(:maintainer_operations_permissions) do
developer_operations_permissions + [
:read_cluster, :create_cluster, :update_cluster, :admin_environment,
:admin_cluster, :admin_terraform_state, :admin_deployment
]
end
where(:project_visibility, :access_level, :role, :allowed) do
:public | ProjectFeature::ENABLED | :maintainer | true
:public | ProjectFeature::ENABLED | :developer | true
:public | ProjectFeature::ENABLED | :guest | true
:public | ProjectFeature::ENABLED | :anonymous | true
:public | ProjectFeature::PRIVATE | :maintainer | true
:public | ProjectFeature::PRIVATE | :developer | true
:public | ProjectFeature::PRIVATE | :guest | true
:public | ProjectFeature::PRIVATE | :anonymous | false
:public | ProjectFeature::DISABLED | :maintainer | false
:public | ProjectFeature::DISABLED | :developer | false
:public | ProjectFeature::DISABLED | :guest | false
:public | ProjectFeature::DISABLED | :anonymous | false
:internal | ProjectFeature::ENABLED | :maintainer | true
:internal | ProjectFeature::ENABLED | :developer | true
:internal | ProjectFeature::ENABLED | :guest | true
:internal | ProjectFeature::ENABLED | :anonymous | false
:internal | ProjectFeature::PRIVATE | :maintainer | true
:internal | ProjectFeature::PRIVATE | :developer | true
:internal | ProjectFeature::PRIVATE | :guest | true
:internal | ProjectFeature::PRIVATE | :anonymous | false
:internal | ProjectFeature::DISABLED | :maintainer | false
:internal | ProjectFeature::DISABLED | :developer | false
:internal | ProjectFeature::DISABLED | :guest | false
:internal | ProjectFeature::DISABLED | :anonymous | false
:private | ProjectFeature::ENABLED | :maintainer | true
:private | ProjectFeature::ENABLED | :developer | true
:private | ProjectFeature::ENABLED | :guest | false
:private | ProjectFeature::ENABLED | :anonymous | false
:private | ProjectFeature::PRIVATE | :maintainer | true
:private | ProjectFeature::PRIVATE | :developer | true
:private | ProjectFeature::PRIVATE | :guest | false
:private | ProjectFeature::PRIVATE | :anonymous | false
:private | ProjectFeature::DISABLED | :maintainer | false
:private | ProjectFeature::DISABLED | :developer | false
:private | ProjectFeature::DISABLED | :guest | false
:private | ProjectFeature::DISABLED | :anonymous | false
end
with_them do
let(:current_user) { user_subject(role) }
let(:project) { project_subject(project_visibility) }
it 'allows/disallows the abilities based on the operation feature access level' do
project.project_feature.update!(operations_access_level: access_level)
if allowed
expect_allowed(*permissions_abilities(role))
else
expect_disallowed(*permissions_abilities(role))
end
end
def project_subject(project_type)
case project_type
when :public
public_project
when :internal
internal_project
else
private_project
end
end
def user_subject(role)
case role
when :maintainer
maintainer
when :developer
developer
when :guest
guest
when :anonymous
anonymous
end
end
def permissions_abilities(role)
case role
when :maintainer
maintainer_operations_permissions
when :developer
developer_operations_permissions
else
guest_operations_permissions
end
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