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
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