Commit e11ae35f authored by James Lopez's avatar James Lopez

Merge branch '224698-fj-add-operations-setting' into 'master'

Adding operations setting

See merge request gitlab-org/gitlab!48347
parents 5552304f 07dd6338
......@@ -386,6 +386,7 @@ class ProjectsController < Projects::ApplicationController
wiki_access_level
pages_access_level
metrics_dashboard_access_level
operations_access_level
]
end
......
......@@ -468,6 +468,8 @@ module ProjectsHelper
end
def can_view_operations_tab?(current_user, project)
return false unless project.feature_available?(:operations, current_user)
[
:metrics_dashboard,
:read_alert_management_alert,
......@@ -622,6 +624,7 @@ module ProjectsHelper
lfsEnabled: !!project.lfs_enabled,
emailsDisabled: project.emails_disabled?,
metricsDashboardAccessLevel: feature.metrics_dashboard_access_level,
operationsAccessLevel: feature.operations_access_level,
showDefaultAwardEmojis: project.show_default_award_emojis?
}
end
......
......@@ -70,6 +70,10 @@ module ProjectFeaturesCompatibility
write_feature_attribute_string(:metrics_dashboard_access_level, value)
end
def operations_access_level=(value)
write_feature_attribute_string(:operations_access_level, value)
end
private
def write_feature_attribute_boolean(field, value)
......
......@@ -388,7 +388,7 @@ class Project < ApplicationRecord
:merge_requests_access_level, :forking_access_level, :issues_access_level,
:wiki_access_level, :snippets_access_level, :builds_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=,
:show_default_award_emojis?,
to: :project_setting, allow_nil: true
......
......@@ -3,7 +3,7 @@
class ProjectFeature < ApplicationRecord
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)
......@@ -45,6 +45,7 @@ class ProjectFeature < ApplicationRecord
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 :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|
if ::Gitlab::Pages.access_control_is_forced?
......
......@@ -147,6 +147,7 @@ class ProjectPolicy < BasePolicy
builds
pages
metrics_dashboard
operations
]
features.each do |f|
......@@ -272,6 +273,19 @@ class ProjectPolicy < BasePolicy
prevent(:metrics_dashboard)
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
enable :read_prometheus
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 (
pages_access_level integer NOT NULL,
forking_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
......
......@@ -14,6 +14,7 @@ module EE
:repository_access_level,
:pages_access_level,
:metrics_dashboard_access_level,
:operations_access_level,
:requirements_access_level].freeze
def initialize(current_user, model, project)
......
......@@ -32,6 +32,7 @@ FactoryBot.define do
visibility_level == Gitlab::VisibilityLevel::PUBLIC ? ProjectFeature::ENABLED : ProjectFeature::PRIVATE
end
metrics_dashboard_access_level { ProjectFeature::PRIVATE }
operations_access_level { ProjectFeature::ENABLED }
# we can't assign the delegated `#ci_cd_settings` attributes directly, as the
# `#ci_cd_settings` relation needs to be created first
......@@ -57,7 +58,8 @@ FactoryBot.define do
merge_requests_access_level: merge_requests_access_level,
repository_access_level: evaluator.repository_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)
......@@ -322,6 +324,9 @@ FactoryBot.define do
trait(:metrics_dashboard_enabled) { metrics_dashboard_access_level { ProjectFeature::ENABLED } }
trait(:metrics_dashboard_disabled) { metrics_dashboard_access_level { ProjectFeature::DISABLED } }
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
association :auto_devops, factory: :project_auto_devops
......
......@@ -2,31 +2,88 @@
require 'spec_helper'
RSpec.describe 'Operations dropdown sidebar' do
let_it_be(:project) { create(:project, :repository) }
RSpec.describe 'Operations dropdown sidebar', :aggregate_failures do
let_it_be_with_reload(:project) { create(:project, :internal, :repository) }
let(:user) { create(:user) }
let(:access_level) { ProjectFeature::PUBLIC }
let(:role) { nil }
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)
visit project_issues_path(project)
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
let(:role) { :guest }
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: '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: '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: '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
it_behaves_like 'shows Operations menu based on the access level'
end
context 'user has reporter role' 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: 'Kubernetes', href: project_clusters_path(project))
end
it_behaves_like 'shows Operations menu based on the access level'
end
context 'user has developer role' 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: 'Kubernetes', href: project_clusters_path(project))
end
it_behaves_like 'shows Operations menu based on the access level'
end
context 'user has maintainer role' 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: 'Kubernetes', href: project_clusters_path(project))
end
it_behaves_like 'shows Operations menu based on the access level'
end
end
......@@ -488,6 +488,7 @@ RSpec.describe ProjectsHelper do
describe '#can_view_operations_tab?' do
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?).and_return(false)
end
subject { helper.send(:can_view_operations_tab?, user, project) }
......@@ -501,11 +502,19 @@ RSpec.describe ProjectsHelper do
:read_cluster
].each do |ability|
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)
is_expected.to be(true)
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
......
......@@ -577,6 +577,7 @@ ProjectFeature:
- pages_access_level
- metrics_dashboard_access_level
- requirements_access_level
- operations_access_level
- created_at
- updated_at
ProtectedBranch::MergeAccessLevel:
......
......@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe ProjectFeaturesCompatibility do
let(:project) { create(:project) }
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
# 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
end
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|
it "does not allow public access level for #{feature}" do
......
......@@ -944,4 +944,116 @@ RSpec.describe ProjectPolicy do
end
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
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