Commit 892dea67 authored by Felipe's avatar Felipe

Project tools visibility level

parent e71cd7a3
Pipeline #4115990 passed with stages
in 39 minutes
......@@ -34,6 +34,7 @@ v 8.12.0 (unreleased)
- Added project specific enable/disable setting for LFS !5997
- Don't expose a user's token in the `/api/v3/user` API (!6047)
- Remove redundant js-timeago-pending from user activity log (ClemMakesApps)
- Ability to manage project issues, snippets, wiki, merge requests and builds access level
- Added tests for diff notes
- Add a button to download latest successful artifacts for branches and tags !5142
- Remove redundant pipeline tooltips (ClemMakesApps)
......
......@@ -37,7 +37,7 @@ class JwtController < ApplicationController
def authenticate_project(login, password)
if login == 'gitlab-ci-token'
Project.find_by(builds_enabled: true, runners_token: password)
Project.with_builds_enabled.find_by(runners_token: password)
end
end
......
......@@ -88,6 +88,6 @@ class Projects::ApplicationController < ApplicationController
end
def builds_enabled
return render_404 unless @project.builds_enabled?
return render_404 unless @project.feature_available?(:builds, current_user)
end
end
......@@ -38,6 +38,6 @@ class Projects::DiscussionsController < Projects::ApplicationController
end
def module_enabled
render_404 unless @project.merge_requests_enabled
render_404 unless @project.feature_available?(:merge_requests, current_user)
end
end
......@@ -201,7 +201,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def module_enabled
return render_404 unless @project.issues_enabled && @project.default_issues_tracker?
return render_404 unless @project.feature_available?(:issues, current_user) && @project.default_issues_tracker?
end
def redirect_to_external_issue_tracker
......
......@@ -99,7 +99,7 @@ class Projects::LabelsController < Projects::ApplicationController
protected
def module_enabled
unless @project.issues_enabled || @project.merge_requests_enabled
unless @project.feature_available?(:issues, current_user) || @project.feature_available?(:merge_requests, current_user)
return render_404
end
end
......
......@@ -413,7 +413,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def module_enabled
return render_404 unless @project.merge_requests_enabled
return render_404 unless @project.feature_available?(:merge_requests, current_user)
end
def validates_merge_request
......
......@@ -106,7 +106,7 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def module_enabled
unless @project.issues_enabled || @project.merge_requests_enabled
unless @project.feature_available?(:issues, current_user) || @project.feature_available?(:merge_requests, current_user)
return render_404
end
end
......
......@@ -94,7 +94,7 @@ class Projects::SnippetsController < Projects::ApplicationController
end
def module_enabled
return render_404 unless @project.snippets_enabled
return render_404 unless @project.feature_available?(:snippets, current_user)
end
def snippet_params
......
......@@ -303,13 +303,23 @@ class ProjectsController < Projects::ApplicationController
end
def project_params
project_feature_attributes =
{
project_feature_attributes:
[
:issues_access_level, :builds_access_level,
:wiki_access_level, :merge_requests_access_level, :snippets_access_level
]
}
params.require(:project).permit(
:name, :path, :description, :issues_tracker, :tag_list, :runners_token,
:issues_enabled, :merge_requests_enabled, :snippets_enabled, :container_registry_enabled,
:container_registry_enabled,
:issues_tracker_id, :default_branch,
:wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
:builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
:public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled, :lfs_enabled
:visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
:build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
:public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled,
:lfs_enabled, project_feature_attributes
)
end
......
......@@ -110,7 +110,7 @@ module ApplicationHelper
project = event.project
# Skip if project repo is empty or MR disabled
return false unless project && !project.empty_repo? && project.merge_requests_enabled
return false unless project && !project.empty_repo? && project.feature_available?(:merge_requests, current_user)
# Skip if user already created appropriate MR
return false if project.merge_requests.where(source_branch: event.branch_name).opened.any?
......
......@@ -3,7 +3,7 @@ module CompareHelper
from.present? &&
to.present? &&
from != to &&
project.merge_requests_enabled &&
project.feature_available?(:merge_requests, current_user) &&
project.repository.branch_names.include?(from) &&
project.repository.branch_names.include?(to)
end
......
......@@ -412,4 +412,23 @@ module ProjectsHelper
message.strip.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]")
end
def project_feature_options
{
'Disabled' => ProjectFeature::DISABLED,
'Only team members' => ProjectFeature::PRIVATE,
'Everyone with access' => ProjectFeature::ENABLED
}
end
def project_feature_access_select(field)
# Don't show option "everyone with access" if project is private
options = project_feature_options
level = @project.project_feature.public_send(field)
options.delete('Everyone with access') if @project.private? && level != ProjectFeature::ENABLED
options = options_for_select(options, selected: @project.project_feature.public_send(field) || ProjectFeature::ENABLED)
content_tag(:select, options, name: "project[project_feature_attributes][#{field.to_s}]", id: "project_project_feature_attributes_#{field.to_s}", class: "pull-right form-control").html_safe
end
end
# Makes api V3 compatible with old project features permissions methods
#
# After migrating issues_enabled merge_requests_enabled builds_enabled snippets_enabled and wiki_enabled
# fields to a new table "project_features", support for the old fields is still needed in the API.
module ProjectFeaturesCompatibility
extend ActiveSupport::Concern
def wiki_enabled=(value)
write_feature_attribute(:wiki_access_level, value)
end
def builds_enabled=(value)
write_feature_attribute(:builds_access_level, value)
end
def merge_requests_enabled=(value)
write_feature_attribute(:merge_requests_access_level, value)
end
def issues_enabled=(value)
write_feature_attribute(:issues_access_level, value)
end
def snippets_enabled=(value)
write_feature_attribute(:snippets_access_level, value)
end
private
def write_feature_attribute(field, value)
build_project_feature unless project_feature
access_level = value == "true" ? ProjectFeature::ENABLED : ProjectFeature::DISABLED
project_feature.update_attribute(field, access_level)
end
end
......@@ -11,24 +11,23 @@ class Project < ActiveRecord::Base
include AfterCommitQueue
include CaseSensitivity
include TokenAuthenticatable
include ProjectFeaturesCompatibility
extend Gitlab::ConfigHelper
UNKNOWN_IMPORT_URL = 'http://unknown.git'
delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true
default_value_for :archived, false
default_value_for :visibility_level, gitlab_config_features.visibility_level
default_value_for :issues_enabled, gitlab_config_features.issues
default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests
default_value_for :builds_enabled, gitlab_config_features.builds
default_value_for :wiki_enabled, gitlab_config_features.wiki
default_value_for :snippets_enabled, gitlab_config_features.snippets
default_value_for :container_registry_enabled, gitlab_config_features.container_registry
default_value_for(:repository_storage) { current_application_settings.repository_storage }
default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
after_create :ensure_dir_exist
after_save :ensure_dir_exist, if: :namespace_id_changed?
after_initialize :setup_project_feature
# set last_activity_at to the same as created_at
after_create :set_last_activity_at
......@@ -62,10 +61,10 @@ class Project < ActiveRecord::Base
belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id'
belongs_to :namespace
has_one :board, dependent: :destroy
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id'
has_one :board, dependent: :destroy
# Project services
has_many :services
has_one :campfire_service, dependent: :destroy
......@@ -130,6 +129,7 @@ class Project < ActiveRecord::Base
has_many :notification_settings, dependent: :destroy, as: :source
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
has_one :project_feature, dependent: :destroy
has_many :commit_statuses, dependent: :destroy, class_name: 'CommitStatus', foreign_key: :gl_project_id
has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
......@@ -142,6 +142,7 @@ class Project < ActiveRecord::Base
has_many :deployments, dependent: :destroy
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
......@@ -159,8 +160,6 @@ class Project < ActiveRecord::Base
length: { within: 0..255 },
format: { with: Gitlab::Regex.project_path_regex,
message: Gitlab::Regex.project_path_regex_message }
validates :issues_enabled, :merge_requests_enabled,
:wiki_enabled, inclusion: { in: [true, false] }
validates :namespace, presence: true
validates_uniqueness_of :name, scope: :namespace_id
validates_uniqueness_of :path, scope: :namespace_id
......@@ -196,6 +195,9 @@ class Project < ActiveRecord::Base
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
scope :with_builds_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0') }
scope :with_issues_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.issues_access_level IS NULL or project_features.issues_access_level > 0') }
scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') }
scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
......@@ -1121,7 +1123,7 @@ class Project < ActiveRecord::Base
end
def enable_ci
self.builds_enabled = true
project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
end
def any_runners?(&block)
......@@ -1288,6 +1290,11 @@ class Project < ActiveRecord::Base
private
# Prevents the creation of project_feature record for every project
def setup_project_feature
build_project_feature unless project_feature
end
def default_branch_protected?
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
......
class ProjectFeature < ActiveRecord::Base
# == Project features permissions
#
# Grants access level to project tools
#
# Tools can be enabled only for users, everyone or disabled
# Access control is made only for non private projects
#
# levels:
#
# Disabled: not enabled for anyone
# Private: enabled only for team members
# Enabled: enabled for everyone able to access the project
#
# Permision levels
DISABLED = 0
PRIVATE = 10
ENABLED = 20
FEATURES = %i(issues merge_requests wiki snippets builds)
belongs_to :project
def feature_available?(feature, user)
raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature)
get_permission(user, public_send("#{feature}_access_level"))
end
def builds_enabled?
return true unless builds_access_level
builds_access_level > DISABLED
end
def wiki_enabled?
return true unless wiki_access_level
wiki_access_level > DISABLED
end
def merge_requests_enabled?
return true unless merge_requests_access_level
merge_requests_access_level > DISABLED
end
private
def get_permission(user, level)
case level
when DISABLED
false
when PRIVATE
user && (project.team.member?(user) || user.admin?)
when ENABLED
true
else
true
end
end
end
......@@ -433,7 +433,7 @@ class User < ActiveRecord::Base
#
# This logic is duplicated from `Ability#project_abilities` into a SQL form.
def projects_where_can_admin_issues
authorized_projects(Gitlab::Access::REPORTER).non_archived.where.not(issues_enabled: false)
authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
end
def is_admin?
......
......@@ -145,28 +145,28 @@ class ProjectPolicy < BasePolicy
end
def disabled_features!
unless project.issues_enabled
unless project.feature_available?(:issues, user)
cannot!(*named_abilities(:issue))
end
unless project.merge_requests_enabled
unless project.feature_available?(:merge_requests, user)
cannot!(*named_abilities(:merge_request))
end
unless project.issues_enabled || project.merge_requests_enabled
unless project.feature_available?(:issues, user) || project.feature_available?(:merge_requests, user)
cannot!(*named_abilities(:label))
cannot!(*named_abilities(:milestone))
end
unless project.snippets_enabled
unless project.feature_available?(:snippets, user)
cannot!(*named_abilities(:project_snippet))
end
unless project.has_wiki?
unless project.feature_available?(:wiki, user) || project.has_external_wiki?
cannot!(*named_abilities(:wiki))
end
unless project.builds_enabled
unless project.feature_available?(:builds, user)
cannot!(*named_abilities(:build))
cannot!(*named_abilities(:pipeline))
cannot!(*named_abilities(:environment))
......
......@@ -8,16 +8,18 @@ module Ci
builds =
if current_runner.shared?
builds.
# don't run projects which have not enabled shared runners
joins(:project).where(projects: { builds_enabled: true, shared_runners_enabled: true }).
# don't run projects which have not enabled shared runners and builds
joins(:project).where(projects: { shared_runners_enabled: true }).
joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id').
# this returns builds that are ordered by number of running builds
# we prefer projects that don't use shared runners at all
joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id").
where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
else
# do run projects which are only assigned to this runner (FIFO)
builds.where(project: current_runner.projects.where(builds_enabled: true)).order('created_at ASC')
builds.where(project: current_runner.projects.with_builds_enabled).order('created_at ASC')
end
build = builds.find do |build|
......
......@@ -31,7 +31,7 @@ module MergeRequests
def get_branches(changes)
return [] if project.empty_repo?
return [] unless project.merge_requests_enabled
return [] unless project.merge_requests_enabled?
changes_list = Gitlab::ChangesList.new(changes)
changes_list.map do |change|
......
......@@ -7,7 +7,6 @@ module Projects
def execute
forked_from_project_id = params.delete(:forked_from_project_id)
import_data = params.delete(:import_data)
@project = Project.new(params)
# Make sure that the user is allowed to use the specified visibility level
......@@ -81,8 +80,7 @@ module Projects
log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"")
unless @project.gitlab_project_import?
@project.create_wiki if @project.wiki_enabled?
@project.create_wiki if @project.feature_available?(:wiki, current_user)
@project.build_missing_services
@project.create_labels
......
......@@ -8,7 +8,6 @@ module Projects
name: @project.name,
path: @project.path,
shared_runners_enabled: @project.shared_runners_enabled,
builds_enabled: @project.builds_enabled,
namespace_id: @params[:namespace].try(:id) || current_user.namespace.id
}
......@@ -17,6 +16,9 @@ module Projects
end
new_project = CreateService.new(current_user, new_params).execute
builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
new_project
end
......
......@@ -26,7 +26,7 @@
%span
Protected Branches
- if @project.builds_enabled?
- if @project.feature_available?(:builds, current_user)
= nav_link(controller: :runners) do
= link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do
%span
......
......@@ -44,52 +44,56 @@
%hr
%fieldset.features.append-bottom-0
%h5.prepend-top-0
Features
.form-group
.checkbox
= f.label :issues_enabled do
= f.check_box :issues_enabled
%strong Issues
%br
%span.descr Lightweight issue tracking system for this project
.form-group
.checkbox
= f.label :merge_requests_enabled do
= f.check_box :merge_requests_enabled
%strong Merge Requests
%br
%span.descr Submit changes to be merged upstream
.form-group
.checkbox
= f.label :builds_enabled do
= f.check_box :builds_enabled
%strong Builds
%br
%span.descr Test and deploy your changes before merge
.form-group
.checkbox
= f.label :wiki_enabled do
= f.check_box :wiki_enabled
%strong Wiki
%br
%span.descr Pages for project documentation
.form-group
.checkbox
= f.label :snippets_enabled do
= f.check_box :snippets_enabled
%strong Snippets
%br
%span.descr Share code pastes with others out of git repository
- if Gitlab.config.lfs.enabled && current_user.admin?
.form-group
.checkbox
= f.label :lfs_enabled do
= f.check_box :lfs_enabled, checked: @project.lfs_enabled?
%strong LFS
%br
%span.descr
Git Large File Storage
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
Feature Visibility
= f.fields_for :project_feature do |feature_fields|
.form_group.prepend-top-20
.row
.col-md-9
= feature_fields.label :issues_access_level, "Issues", class: 'label-light'
%span.help-block Lightweight issue tracking system for this project
.col-md-3
= project_feature_access_select(:issues_access_level)
.row
.col-md-9
= feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
%span.help-block Submit changes to be merged upstream
.col-md-3
= project_feature_access_select(:merge_requests_access_level)
.row
.col-md-9
= feature_fields.label :builds_access_level, "Builds", class: 'label-light'
%span.help-block Submit Test and deploy your changes before merge
.col-md-3
= project_feature_access_select(:builds_access_level)
.row
.col-md-9
= feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
%span.help-block Pages for project documentation
.col-md-3
= project_feature_access_select(:wiki_access_level)
.row
.col-md-9
= feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
%span.help-block Share code pastes with others out of Git repository
.col-md-3
= project_feature_access_select(:snippets_access_level)
- if Gitlab.config.lfs.enabled && current_user.admin?
.form-group
.checkbox
= f.label :lfs_enabled do
= f.check_box :lfs_enabled, checked: @project.lfs_enabled?
%strong LFS
%br
%span.descr
Git Large File Storage
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
- if Gitlab.config.registry.enabled
.form-group
.checkbox
......@@ -98,7 +102,7 @@
%strong Container Registry
%br
%span.descr Enable Container Registry for this repository
%hr
= render 'merge_request_settings', f: f
%hr
%fieldset.features.append-bottom-default
......
......@@ -12,7 +12,7 @@
= link_to 'Commits', commits_namespace_project_graph_path
= nav_link(action: :languages) do
= link_to 'Languages', languages_namespace_project_graph_path
- if @project.builds_enabled?
- if @project.feature_available?(:builds, current_user)
= nav_link(action: :ci) do
= link_to ci_namespace_project_graph_path do
Continuous Integration
class CreateProjectFeatures < ActiveRecord::Migration
DOWNTIME = false
def change
create_table :project_features do |t|
t.belongs_to :project, index: true
t.integer :merge_requests_access_level
t.integer :issues_access_level
t.integer :wiki_access_level
t.integer :snippets_access_level
t.integer :builds_access_level
t.timestamps
end
end
end
class MigrateProjectFeatures < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = true
DOWNTIME_REASON =
<<-EOT
Migrating issues_enabled, merge_requests_enabled, wiki_enabled, builds_enabled, snippets_enabled fields from projects to
a new table called project_features.
EOT
def up
sql =
%Q{
INSERT INTO project_features(project_id, issues_access_level, merge_requests_access_level, wiki_access_level,
builds_access_level, snippets_access_level, created_at, updated_at)
SELECT
id AS project_id,
CASE WHEN issues_enabled IS true THEN 20 ELSE 0 END AS issues_access_level,
CASE WHEN merge_requests_enabled IS true THEN 20 ELSE 0 END AS merge_requests_access_level,
CASE WHEN wiki_enabled IS true THEN 20 ELSE 0 END AS wiki_access_level,
CASE WHEN builds_enabled IS true THEN 20 ELSE 0 END AS builds_access_level,
CASE WHEN snippets_enabled IS true THEN 20 ELSE 0 END AS snippets_access_level,
created_at,
updated_at
FROM projects
}
execute(sql)
end
def down
sql = %Q{
UPDATE projects
SET
issues_enabled = COALESCE((SELECT CASE WHEN issues_access_level = 20 THEN true ELSE false END AS issues_enabled FROM project_features WHERE project_features.project_id = projects.id), true),
merge_requests_enabled = COALESCE((SELECT CASE WHEN merge_requests_access_level = 20 THEN true ELSE false END AS merge_requests_enabled FROM project_features WHERE project_features.project_id = projects.id),true),
wiki_enabled = COALESCE((SELECT CASE WHEN wiki_access_level = 20 THEN true ELSE false END AS wiki_enabled FROM project_features WHERE project_features.project_id = projects.id), true),
builds_enabled = COALESCE((SELECT CASE WHEN builds_access_level = 20 THEN true ELSE false END AS builds_enabled FROM project_features WHERE project_features.project_id = projects.id), true),
snippets_enabled = COALESCE((SELECT CASE WHEN snippets_access_level = 20 THEN true ELSE false END AS snippets_enabled FROM project_features WHERE project_features.project_id = projects.id),true)
}
execute(sql)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemoveFeaturesEnabledFromProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
# Set this constant to true if this migration requires downtime.
DOWNTIME = true
DOWNTIME_REASON = "Removing fields from database requires downtine."
def up
remove_column :projects, :issues_enabled
remove_column :projects, :merge_requests_enabled
remove_column :projects, :builds_enabled
remove_column :projects, :wiki_enabled
remove_column :projects, :snippets_enabled
end