Merge branch 'issue_19734' into 'master'

Project tools visibility level

## part of #19734   

![project_features_access_level](/uploads/81ec7185d4e61d7578652020209af925/project_features_access_level.png)

## Does this MR meet the acceptance criteria?

- [x] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG) entry added
- [x] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md)
- [x] API support added
- Tests
  - [x] Added for this feature/bug
  - [x] All builds are passing
- [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides)
- [x] Branch has no merge conflicts with `master` (if you do - rebase it please)
- [x] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)

See merge request !5606
This commit is contained in:
Douwe Maan 2016-09-01 15:28:14 +00:00
commit d308a3f433
60 changed files with 713 additions and 143 deletions

View file

@ -36,6 +36,7 @@ v 8.12.0 (unreleased)
- Added project specific enable/disable setting for LFS !5997 - Added project specific enable/disable setting for LFS !5997
- Don't expose a user's token in the `/api/v3/user` API (!6047) - Don't expose a user's token in the `/api/v3/user` API (!6047)
- Remove redundant js-timeago-pending from user activity log (ClemMakesApps) - 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 - Added tests for diff notes
- Add a button to download latest successful artifacts for branches and tags !5142 - Add a button to download latest successful artifacts for branches and tags !5142
- Remove redundant pipeline tooltips (ClemMakesApps) - Remove redundant pipeline tooltips (ClemMakesApps)

View file

@ -37,7 +37,7 @@ class JwtController < ApplicationController
def authenticate_project(login, password) def authenticate_project(login, password)
if login == 'gitlab-ci-token' 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
end end

View file

@ -88,6 +88,6 @@ class Projects::ApplicationController < ApplicationController
end end
def builds_enabled def builds_enabled
return render_404 unless @project.builds_enabled? return render_404 unless @project.feature_available?(:builds, current_user)
end end
end end

View file

@ -38,6 +38,6 @@ class Projects::DiscussionsController < Projects::ApplicationController
end end
def module_enabled def module_enabled
render_404 unless @project.merge_requests_enabled render_404 unless @project.feature_available?(:merge_requests, current_user)
end end
end end

View file

@ -201,7 +201,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def module_enabled 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 end
def redirect_to_external_issue_tracker def redirect_to_external_issue_tracker

View file

@ -99,7 +99,7 @@ class Projects::LabelsController < Projects::ApplicationController
protected protected
def module_enabled 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 return render_404
end end
end end

View file

@ -413,7 +413,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
def module_enabled def module_enabled
return render_404 unless @project.merge_requests_enabled return render_404 unless @project.feature_available?(:merge_requests, current_user)
end end
def validates_merge_request def validates_merge_request

View file

@ -106,7 +106,7 @@ class Projects::MilestonesController < Projects::ApplicationController
end end
def module_enabled 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 return render_404
end end
end end

View file

@ -94,7 +94,7 @@ class Projects::SnippetsController < Projects::ApplicationController
end end
def module_enabled def module_enabled
return render_404 unless @project.snippets_enabled return render_404 unless @project.feature_available?(:snippets, current_user)
end end
def snippet_params def snippet_params

View file

@ -303,13 +303,23 @@ class ProjectsController < Projects::ApplicationController
end end
def project_params 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( params.require(:project).permit(
:name, :path, :description, :issues_tracker, :tag_list, :runners_token, :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, :issues_tracker_id, :default_branch,
:wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
:builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
:public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled, :lfs_enabled :public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled,
:lfs_enabled, project_feature_attributes
) )
end end

View file

@ -110,7 +110,7 @@ module ApplicationHelper
project = event.project project = event.project
# Skip if project repo is empty or MR disabled # 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 # Skip if user already created appropriate MR
return false if project.merge_requests.where(source_branch: event.branch_name).opened.any? return false if project.merge_requests.where(source_branch: event.branch_name).opened.any?

View file

@ -3,7 +3,7 @@ module CompareHelper
from.present? && from.present? &&
to.present? && to.present? &&
from != to && from != to &&
project.merge_requests_enabled && project.feature_available?(:merge_requests, current_user) &&
project.repository.branch_names.include?(from) && project.repository.branch_names.include?(from) &&
project.repository.branch_names.include?(to) project.repository.branch_names.include?(to)
end end

View file

@ -412,4 +412,23 @@ module ProjectsHelper
message.strip.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]") message.strip.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]")
end 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 end

View file

@ -0,0 +1,37 @@
# 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

View file

@ -11,24 +11,23 @@ class Project < ActiveRecord::Base
include AfterCommitQueue include AfterCommitQueue
include CaseSensitivity include CaseSensitivity
include TokenAuthenticatable include TokenAuthenticatable
include ProjectFeaturesCompatibility
extend Gitlab::ConfigHelper extend Gitlab::ConfigHelper
UNKNOWN_IMPORT_URL = 'http://unknown.git' 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 :archived, false
default_value_for :visibility_level, gitlab_config_features.visibility_level 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 :container_registry_enabled, gitlab_config_features.container_registry
default_value_for(:repository_storage) { current_application_settings.repository_storage } default_value_for(:repository_storage) { current_application_settings.repository_storage }
default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
after_create :ensure_dir_exist after_create :ensure_dir_exist
after_save :ensure_dir_exist, if: :namespace_id_changed? after_save :ensure_dir_exist, if: :namespace_id_changed?
after_initialize :setup_project_feature
# set last_activity_at to the same as created_at # set last_activity_at to the same as created_at
after_create :set_last_activity_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 :group, -> { where(type: Group) }, foreign_key: 'namespace_id'
belongs_to :namespace 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 :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id'
has_one :board, dependent: :destroy
# Project services # Project services
has_many :services has_many :services
has_one :campfire_service, dependent: :destroy has_one :campfire_service, dependent: :destroy
@ -130,6 +129,7 @@ class Project < ActiveRecord::Base
has_many :notification_settings, dependent: :destroy, as: :source has_many :notification_settings, dependent: :destroy, as: :source
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" 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 :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 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 has_many :deployments, dependent: :destroy
accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true delegate :members, to: :team, prefix: true
@ -159,8 +160,6 @@ class Project < ActiveRecord::Base
length: { within: 0..255 }, length: { within: 0..255 },
format: { with: Gitlab::Regex.project_path_regex, format: { with: Gitlab::Regex.project_path_regex,
message: Gitlab::Regex.project_path_regex_message } message: Gitlab::Regex.project_path_regex_message }
validates :issues_enabled, :merge_requests_enabled,
:wiki_enabled, inclusion: { in: [true, false] }
validates :namespace, presence: true validates :namespace, presence: true
validates_uniqueness_of :name, scope: :namespace_id validates_uniqueness_of :name, scope: :namespace_id
validates_uniqueness_of :path, 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 :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) } 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 :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) } scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
@ -1121,7 +1123,7 @@ class Project < ActiveRecord::Base
end end
def enable_ci def enable_ci
self.builds_enabled = true project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
end end
def any_runners?(&block) def any_runners?(&block)
@ -1288,6 +1290,11 @@ class Project < ActiveRecord::Base
private 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? def default_branch_protected?
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE

View file

@ -0,0 +1,63 @@
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

View file

@ -433,7 +433,7 @@ class User < ActiveRecord::Base
# #
# This logic is duplicated from `Ability#project_abilities` into a SQL form. # This logic is duplicated from `Ability#project_abilities` into a SQL form.
def projects_where_can_admin_issues 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 end
def is_admin? def is_admin?

View file

@ -145,28 +145,28 @@ class ProjectPolicy < BasePolicy
end end
def disabled_features! def disabled_features!
unless project.issues_enabled unless project.feature_available?(:issues, user)
cannot!(*named_abilities(:issue)) cannot!(*named_abilities(:issue))
end end
unless project.merge_requests_enabled unless project.feature_available?(:merge_requests, user)
cannot!(*named_abilities(:merge_request)) cannot!(*named_abilities(:merge_request))
end 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(:label))
cannot!(*named_abilities(:milestone)) cannot!(*named_abilities(:milestone))
end end
unless project.snippets_enabled unless project.feature_available?(:snippets, user)
cannot!(*named_abilities(:project_snippet)) cannot!(*named_abilities(:project_snippet))
end end
unless project.has_wiki? unless project.feature_available?(:wiki, user) || project.has_external_wiki?
cannot!(*named_abilities(:wiki)) cannot!(*named_abilities(:wiki))
end end
unless project.builds_enabled unless project.feature_available?(:builds, user)
cannot!(*named_abilities(:build)) cannot!(*named_abilities(:build))
cannot!(*named_abilities(:pipeline)) cannot!(*named_abilities(:pipeline))
cannot!(*named_abilities(:environment)) cannot!(*named_abilities(:environment))

View file

@ -8,16 +8,18 @@ module Ci
builds = builds =
if current_runner.shared? if current_runner.shared?
builds. builds.
# don't run projects which have not enabled shared runners # don't run projects which have not enabled shared runners and builds
joins(:project).where(projects: { builds_enabled: true, shared_runners_enabled: true }). 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 # this returns builds that are ordered by number of running builds
# we prefer projects that don't use shared runners at all # 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"). 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') order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
else else
# do run projects which are only assigned to this runner (FIFO) # 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 end
build = builds.find do |build| build = builds.find do |build|

View file

@ -31,7 +31,7 @@ module MergeRequests
def get_branches(changes) def get_branches(changes)
return [] if project.empty_repo? 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 = Gitlab::ChangesList.new(changes)
changes_list.map do |change| changes_list.map do |change|

View file

@ -7,7 +7,6 @@ module Projects
def execute def execute
forked_from_project_id = params.delete(:forked_from_project_id) forked_from_project_id = params.delete(:forked_from_project_id)
import_data = params.delete(:import_data) import_data = params.delete(:import_data)
@project = Project.new(params) @project = Project.new(params)
# Make sure that the user is allowed to use the specified visibility level # 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}\"") log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"")
unless @project.gitlab_project_import? 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.build_missing_services
@project.create_labels @project.create_labels

View file

@ -8,7 +8,6 @@ module Projects
name: @project.name, name: @project.name,
path: @project.path, path: @project.path,
shared_runners_enabled: @project.shared_runners_enabled, shared_runners_enabled: @project.shared_runners_enabled,
builds_enabled: @project.builds_enabled,
namespace_id: @params[:namespace].try(:id) || current_user.namespace.id namespace_id: @params[:namespace].try(:id) || current_user.namespace.id
} }
@ -17,6 +16,9 @@ module Projects
end end
new_project = CreateService.new(current_user, new_params).execute 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 new_project
end end

View file

@ -26,7 +26,7 @@
%span %span
Protected Branches Protected Branches
- if @project.builds_enabled? - if @project.feature_available?(:builds, current_user)
= nav_link(controller: :runners) do = nav_link(controller: :runners) do
= link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do
%span %span

View file

@ -44,52 +44,56 @@
%hr %hr
%fieldset.features.append-bottom-0 %fieldset.features.append-bottom-0
%h5.prepend-top-0 %h5.prepend-top-0
Features Feature Visibility
.form-group
.checkbox = f.fields_for :project_feature do |feature_fields|
= f.label :issues_enabled do .form_group.prepend-top-20
= f.check_box :issues_enabled .row
%strong Issues .col-md-9
%br = feature_fields.label :issues_access_level, "Issues", class: 'label-light'
%span.descr Lightweight issue tracking system for this project %span.help-block Lightweight issue tracking system for this project
.form-group .col-md-3
.checkbox = project_feature_access_select(:issues_access_level)
= f.label :merge_requests_enabled do
= f.check_box :merge_requests_enabled .row
%strong Merge Requests .col-md-9
%br = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
%span.descr Submit changes to be merged upstream %span.help-block Submit changes to be merged upstream
.form-group .col-md-3
.checkbox = project_feature_access_select(:merge_requests_access_level)
= f.label :builds_enabled do
= f.check_box :builds_enabled .row
%strong Builds .col-md-9
%br = feature_fields.label :builds_access_level, "Builds", class: 'label-light'
%span.descr Test and deploy your changes before merge %span.help-block Submit Test and deploy your changes before merge
.form-group .col-md-3
.checkbox = project_feature_access_select(:builds_access_level)
= f.label :wiki_enabled do
= f.check_box :wiki_enabled .row
%strong Wiki .col-md-9
%br = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
%span.descr Pages for project documentation %span.help-block Pages for project documentation
.form-group .col-md-3
.checkbox = project_feature_access_select(:wiki_access_level)
= f.label :snippets_enabled do
= f.check_box :snippets_enabled .row
%strong Snippets .col-md-9
%br = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
%span.descr Share code pastes with others out of git repository %span.help-block Share code pastes with others out of Git repository
- if Gitlab.config.lfs.enabled && current_user.admin? .col-md-3
.form-group = project_feature_access_select(:snippets_access_level)
.checkbox
= f.label :lfs_enabled do - if Gitlab.config.lfs.enabled && current_user.admin?
= f.check_box :lfs_enabled, checked: @project.lfs_enabled? .form-group
%strong LFS .checkbox
%br = f.label :lfs_enabled do
%span.descr = f.check_box :lfs_enabled, checked: @project.lfs_enabled?
Git Large File Storage %strong LFS
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_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 - if Gitlab.config.registry.enabled
.form-group .form-group
.checkbox .checkbox
@ -98,7 +102,7 @@
%strong Container Registry %strong Container Registry
%br %br
%span.descr Enable Container Registry for this repository %span.descr Enable Container Registry for this repository
%hr
= render 'merge_request_settings', f: f = render 'merge_request_settings', f: f
%hr %hr
%fieldset.features.append-bottom-default %fieldset.features.append-bottom-default

View file

@ -12,7 +12,7 @@
= link_to 'Commits', commits_namespace_project_graph_path = link_to 'Commits', commits_namespace_project_graph_path
= nav_link(action: :languages) do = nav_link(action: :languages) do
= link_to 'Languages', languages_namespace_project_graph_path = link_to 'Languages', languages_namespace_project_graph_path
- if @project.builds_enabled? - if @project.feature_available?(:builds, current_user)
= nav_link(action: :ci) do = nav_link(action: :ci) do
= link_to ci_namespace_project_graph_path do = link_to ci_namespace_project_graph_path do
Continuous Integration Continuous Integration

View file

@ -0,0 +1,16 @@
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

View file

@ -0,0 +1,44 @@
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

View file

@ -0,0 +1,29 @@
# 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
# Ugly SQL but the only way i found to make it work on both Postgres and Mysql
# It will be slow but it is ok since it is a revert method
def down
add_column_with_default(:projects, :issues_enabled, :boolean, default: true, allow_null: false)
add_column_with_default(:projects, :merge_requests_enabled, :boolean, default: true, allow_null: false)
add_column_with_default(:projects, :builds_enabled, :boolean, default: true, allow_null: false)
add_column_with_default(:projects, :wiki_enabled, :boolean, default: true, allow_null: false)
add_column_with_default(:projects, :snippets_enabled, :boolean, default: true, allow_null: false)
end
end

View file

@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160830232601) do ActiveRecord::Schema.define(version: 20160831223750) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -766,6 +766,19 @@ ActiveRecord::Schema.define(version: 20160830232601) do
add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
create_table "project_features", force: :cascade do |t|
t.integer "project_id"
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.datetime "created_at"
t.datetime "updated_at"
end
add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", using: :btree
create_table "project_group_links", force: :cascade do |t| create_table "project_group_links", force: :cascade do |t|
t.integer "project_id", null: false t.integer "project_id", null: false
t.integer "group_id", null: false t.integer "group_id", null: false
@ -790,11 +803,7 @@ ActiveRecord::Schema.define(version: 20160830232601) do
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.integer "creator_id" t.integer "creator_id"
t.boolean "issues_enabled", default: true, null: false
t.boolean "merge_requests_enabled", default: true, null: false
t.boolean "wiki_enabled", default: true, null: false
t.integer "namespace_id" t.integer "namespace_id"
t.boolean "snippets_enabled", default: true, null: false
t.datetime "last_activity_at" t.datetime "last_activity_at"
t.string "import_url" t.string "import_url"
t.integer "visibility_level", default: 0, null: false t.integer "visibility_level", default: 0, null: false
@ -808,7 +817,6 @@ ActiveRecord::Schema.define(version: 20160830232601) do
t.integer "commit_count", default: 0 t.integer "commit_count", default: 0
t.text "import_error" t.text "import_error"
t.integer "ci_id" t.integer "ci_id"
t.boolean "builds_enabled", default: true, null: false
t.boolean "shared_runners_enabled", default: true, null: false t.boolean "shared_runners_enabled", default: true, null: false
t.string "runners_token" t.string "runners_token"
t.string "build_coverage_regex" t.string "build_coverage_regex"

View file

@ -104,6 +104,15 @@ will find the option to flag the user as external.
By default new users are not set as external users. This behavior can be changed By default new users are not set as external users. This behavior can be changed
by an administrator under **Admin > Application Settings**. by an administrator under **Admin > Application Settings**.
## Project features
Project features like wiki and issues can be hidden from users depending on
which visibility level you select on project settings.
- Disabled: disabled for everyone
- Only team members: only team members will see even if your project is public or internal
- Everyone with access: everyone can see depending on your project visibility level
## GitLab CI ## GitLab CI
GitLab CI permissions rely on the role the user has in GitLab. There are four GitLab CI permissions rely on the role the user has in GitLab. There are four

View file

@ -5,7 +5,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps
step 'change project settings' do step 'change project settings' do
fill_in 'project_name_edit', with: 'NewName' fill_in 'project_name_edit', with: 'NewName'
uncheck 'project_issues_enabled' select 'Disabled', from: 'project_project_feature_attributes_issues_access_level'
end end
step 'I save project' do step 'I save project' do

View file

@ -15,7 +15,7 @@ module SharedProject
# Create a specific project called "Shop" # Create a specific project called "Shop"
step 'I own project "Shop"' do step 'I own project "Shop"' do
@project = Project.find_by(name: "Shop") @project = Project.find_by(name: "Shop")
@project ||= create(:project, name: "Shop", namespace: @user.namespace, snippets_enabled: true) @project ||= create(:project, name: "Shop", namespace: @user.namespace)
@project.team << [@user, :master] @project.team << [@user, :master]
end end
@ -41,6 +41,8 @@ module SharedProject
step 'I own project "Forum"' do step 'I own project "Forum"' do
@project = Project.find_by(name: "Forum") @project = Project.find_by(name: "Forum")
@project ||= create(:project, name: "Forum", namespace: @user.namespace, path: 'forum_project') @project ||= create(:project, name: "Forum", namespace: @user.namespace, path: 'forum_project')
@project.build_project_feature
@project.project_feature.save
@project.team << [@user, :master] @project.team << [@user, :master]
end end
@ -95,7 +97,7 @@ module SharedProject
step 'I should see project settings' do step 'I should see project settings' do
expect(current_path).to eq edit_namespace_project_path(@project.namespace, @project) expect(current_path).to eq edit_namespace_project_path(@project.namespace, @project)
expect(page).to have_content("Project name") expect(page).to have_content("Project name")
expect(page).to have_content("Features") expect(page).to have_content("Feature Visibility")
end end
def current_project def current_project

View file

@ -76,7 +76,15 @@ module API
expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group }
expose :name, :name_with_namespace expose :name, :name_with_namespace
expose :path, :path_with_namespace expose :path, :path_with_namespace
expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :container_registry_enabled expose :container_registry_enabled
# Expose old field names with the new permissions methods to keep API compatible
expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:user]) }
expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:user]) }
expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:user]) }
expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:user]) }
expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:user]) }
expose :created_at, :last_activity_at expose :created_at, :last_activity_at
expose :shared_runners_enabled, :lfs_enabled expose :shared_runners_enabled, :lfs_enabled
expose :creator_id expose :creator_id
@ -84,7 +92,7 @@ module API
expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? } expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? }
expose :avatar_url expose :avatar_url
expose :star_count, :forks_count expose :star_count, :forks_count
expose :open_issues_count, if: lambda { |project, options| project.issues_enabled? && project.default_issues_tracker? } expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:user]) && project.default_issues_tracker? }
expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
expose :public_builds expose :public_builds
expose :shared_with_groups do |project, options| expose :shared_with_groups do |project, options|

View file

@ -97,7 +97,7 @@ module API
group = find_group(params[:id]) group = find_group(params[:id])
projects = GroupProjectsFinder.new(group).execute(current_user) projects = GroupProjectsFinder.new(group).execute(current_user)
projects = paginate projects projects = paginate projects
present projects, with: Entities::Project present projects, with: Entities::Project, user: current_user
end end
# Transfer a project to the Group namespace # Transfer a project to the Group namespace

View file

@ -51,7 +51,7 @@ module API
@projects = current_user.viewable_starred_projects @projects = current_user.viewable_starred_projects
@projects = filter_projects(@projects) @projects = filter_projects(@projects)
@projects = paginate @projects @projects = paginate @projects
present @projects, with: Entities::Project present @projects, with: Entities::Project, user: current_user
end end
# Get all projects for admin user # Get all projects for admin user

View file

@ -168,7 +168,7 @@ module Gitlab
unless project.wiki_enabled? unless project.wiki_enabled?
wiki = WikiFormatter.new(project) wiki = WikiFormatter.new(project)
gitlab_shell.import_repository(project.repository_storage_path, wiki.path_with_namespace, wiki.import_url) gitlab_shell.import_repository(project.repository_storage_path, wiki.path_with_namespace, wiki.import_url)
project.update_attribute(:wiki_enabled, true) project.project.update_attribute(:wiki_access_level, ProjectFeature::ENABLED)
end end
rescue Gitlab::Shell::Error => e rescue Gitlab::Shell::Error => e
# GitHub error message when the wiki repo has not been created, # GitHub error message when the wiki repo has not been created,

View file

@ -11,7 +11,7 @@ module Gitlab
end end
def execute def execute
::Projects::CreateService.new( project = ::Projects::CreateService.new(
current_user, current_user,
name: repo.name, name: repo.name,
path: repo.name, path: repo.name,
@ -20,9 +20,15 @@ module Gitlab
visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility, visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility,
import_type: "github", import_type: "github",
import_source: repo.full_name, import_source: repo.full_name,
import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@"), import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@")
wiki_enabled: !repo.has_wiki? # If repo has wiki we'll import it later
).execute ).execute
# If repo has wiki we'll import it later
if repo.has_wiki? && project
project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
end
project
end end
end end
end end

View file

@ -39,15 +39,12 @@ project_tree:
- :labels - :labels
- milestones: - milestones:
- :events - :events
- :project_feature
# Only include the following attributes for the models specified. # Only include the following attributes for the models specified.
included_attributes: included_attributes:
project: project:
- :description - :description
- :issues_enabled
- :merge_requests_enabled
- :wiki_enabled
- :snippets_enabled
- :visibility_level - :visibility_level
- :archived - :archived
user: user:

View file

@ -1,7 +1,7 @@
require 'spec_helper' require 'spec_helper'
describe Projects::SnippetsController do describe Projects::SnippetsController do
let(:project) { create(:project_empty_repo, :public, snippets_enabled: true) } let(:project) { create(:project_empty_repo, :public) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }

View file

@ -8,7 +8,6 @@ FactoryGirl.define do
path { name.downcase.gsub(/\s/, '_') } path { name.downcase.gsub(/\s/, '_') }
namespace namespace
creator creator
snippets_enabled true
trait :public do trait :public do
visibility_level Gitlab::VisibilityLevel::PUBLIC visibility_level Gitlab::VisibilityLevel::PUBLIC
@ -27,6 +26,26 @@ FactoryGirl.define do
project.create_repository project.create_repository
end end
end end
# Nest Project Feature attributes
transient do
wiki_access_level ProjectFeature::ENABLED
builds_access_level ProjectFeature::ENABLED
snippets_access_level ProjectFeature::ENABLED
issues_access_level ProjectFeature::ENABLED
merge_requests_access_level ProjectFeature::ENABLED
end
after(:create) do |project, evaluator|
project.project_feature.
update_attributes(
wiki_access_level: evaluator.wiki_access_level,
builds_access_level: evaluator.builds_access_level,
snippets_access_level: evaluator.snippets_access_level,
issues_access_level: evaluator.issues_access_level,
merge_requests_access_level: evaluator.merge_requests_access_level,
)
end
end end
# Project with empty repository # Project with empty repository

View file

@ -0,0 +1,122 @@
require 'spec_helper'
include WaitForAjax
describe 'Edit Project Settings', feature: true do
let(:member) { create(:user) }
let!(:project) { create(:project, :public, path: 'gitlab', name: 'sample') }
let(:non_member) { create(:user) }
describe 'project features visibility selectors', js: true do
before do
project.team << [member, :master]
login_as(member)
end
tools = { builds: "pipelines", issues: "issues", wiki: "wiki", snippets: "snippets", merge_requests: "merge_requests" }
tools.each do |tool_name, shortcut_name|
describe "feature #{tool_name}" do
it 'toggles visibility' do
visit edit_namespace_project_path(project.namespace, project)
select 'Disabled', from: "project_project_feature_attributes_#{tool_name}_access_level"
click_button 'Save changes'
wait_for_ajax
expect(page).not_to have_selector(".shortcuts-#{shortcut_name}")
select 'Everyone with access', from: "project_project_feature_attributes_#{tool_name}_access_level"
click_button 'Save changes'
wait_for_ajax
expect(page).to have_selector(".shortcuts-#{shortcut_name}")
select 'Only team members', from: "project_project_feature_attributes_#{tool_name}_access_level"
click_button 'Save changes'
wait_for_ajax
expect(page).to have_selector(".shortcuts-#{shortcut_name}")
sleep 0.1
end
end
end
end
describe 'project features visibility pages' do
before do
@tools =
{
builds: namespace_project_pipelines_path(project.namespace, project),
issues: namespace_project_issues_path(project.namespace, project),
wiki: namespace_project_wiki_path(project.namespace, project, :home),
snippets: namespace_project_snippets_path(project.namespace, project),
merge_requests: namespace_project_merge_requests_path(project.namespace, project),
}
end
context 'normal user' do
it 'renders 200 if tool is enabled' do
@tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::ENABLED)
visit url
expect(page.status_code).to eq(200)
end
end
it 'renders 404 if feature is disabled' do
@tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED)
visit url
expect(page.status_code).to eq(404)
end
end
it 'renders 404 if feature is enabled only for team members' do
project.team.truncate
@tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE)
visit url
expect(page.status_code).to eq(404)
end
end
it 'renders 200 if users is member of group' do
group = create(:group)
project.group = group
project.save
group.add_owner(member)
@tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE)
visit url
expect(page.status_code).to eq(200)
end
end
end
context 'admin user' do
before do
non_member.update_attribute(:admin, true)
login_as(non_member)
end
it 'renders 404 if feature is disabled' do
@tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED)
visit url
expect(page.status_code).to eq(404)
end
end
it 'renders 200 if feature is enabled only for team members' do
project.team.truncate
@tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE)
visit url
expect(page.status_code).to eq(200)
end
end
end
end
end

View file

@ -7,7 +7,8 @@ describe Gitlab::Auth, lib: true do
it 'recognizes CI' do it 'recognizes CI' do
token = '123' token = '123'
project = create(:empty_project) project = create(:empty_project)
project.update_attributes(runners_token: token, builds_enabled: true) project.update_attributes(runners_token: token)
ip = 'ip' ip = 'ip'
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'gitlab-ci-token') expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'gitlab-ci-token')

View file

@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::GithubImport::Importer, lib: true do describe Gitlab::GithubImport::Importer, lib: true do
describe '#execute' do describe '#execute' do
context 'when an error occurs' do context 'when an error occurs' do
let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_enabled: false) } let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_access_level: ProjectFeature::DISABLED) }
let(:octocat) { double(id: 123456, login: 'octocat') } let(:octocat) { double(id: 123456, login: 'octocat') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }

View file

@ -1,9 +1,5 @@
{ {
"description": "Nisi et repellendus ut enim quo accusamus vel magnam.", "description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"visibility_level": 10, "visibility_level": 10,
"archived": false, "archived": false,
"issues": [ "issues": [

View file

@ -5,7 +5,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:namespace) { create(:namespace, owner: user) } let(:namespace) { create(:namespace, owner: user) }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') }
let(:project) { create(:empty_project, name: 'project', path: 'project') } let!(:project) { create(:empty_project, name: 'project', path: 'project', builds_access_level: ProjectFeature::DISABLED, issues_access_level: ProjectFeature::DISABLED) }
let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
let(:restored_project_json) { project_tree_restorer.restore } let(:restored_project_json) { project_tree_restorer.restore }
@ -18,6 +18,17 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(restored_project_json).to be true expect(restored_project_json).to be true
end end
it 'restore correct project features' do
restored_project_json
project = Project.find_by_path('project')
expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED)
expect(project.project_feature.builds_access_level).to eq(ProjectFeature::DISABLED)
expect(project.project_feature.snippets_access_level).to eq(ProjectFeature::ENABLED)
expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::ENABLED)
expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::ENABLED)
end
it 'creates a valid pipeline note' do it 'creates a valid pipeline note' do
restored_project_json restored_project_json

View file

@ -111,6 +111,14 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty
end end
it 'has project feature' do
project_feature = saved_project_json['project_feature']
expect(project_feature).not_to be_empty
expect(project_feature["issues_access_level"]).to eq(ProjectFeature::DISABLED)
expect(project_feature["wiki_access_level"]).to eq(ProjectFeature::ENABLED)
expect(project_feature["builds_access_level"]).to eq(ProjectFeature::PRIVATE)
end
it 'does not complain about non UTF-8 characters in MR diffs' do it 'does not complain about non UTF-8 characters in MR diffs' do
ActiveRecord::Base.connection.execute("UPDATE merge_request_diffs SET st_diffs = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'") ActiveRecord::Base.connection.execute("UPDATE merge_request_diffs SET st_diffs = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'")
@ -154,6 +162,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
create(:event, target: milestone, project: project, action: Event::CREATED, author: user) create(:event, target: milestone, project: project, action: Event::CREATED, author: user)
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED)
project.project_feature.update_attribute(:builds_access_level, ProjectFeature::PRIVATE)
project project
end end

View file

@ -32,6 +32,12 @@ describe Gitlab::ImportExport::Reader, lib: true do
expect(described_class.new(shared: shared).project_tree).to match(include: [:issues]) expect(described_class.new(shared: shared).project_tree).to match(include: [:issues])
end end
it 'generates the correct hash for a single project feature relation' do
setup_yaml(project_tree: [:project_feature])
expect(described_class.new(shared: shared).project_tree).to match(include: [:project_feature])
end
it 'generates the correct hash for a multiple project relation' do it 'generates the correct hash for a multiple project relation' do
setup_yaml(project_tree: [:issues, :snippets]) setup_yaml(project_tree: [:issues, :snippets])

View file

@ -220,13 +220,13 @@ describe Ability, lib: true do
end end
describe '.project_disabled_features_rules' do describe '.project_disabled_features_rules' do
let(:project) { build(:project) } let(:project) { create(:project, wiki_access_level: ProjectFeature::DISABLED) }
subject { described_class.allowed(project.owner, project) } subject { described_class.allowed(project.owner, project) }
context 'wiki named abilities' do context 'wiki named abilities' do
it 'disables wiki abilities if the project has no wiki' do it 'disables wiki abilities if the project has no wiki' do
expect(project).to receive(:has_wiki?).and_return(false) expect(project).to receive(:has_external_wiki?).and_return(false)
expect(subject).not_to include(:read_wiki, :create_wiki, :update_wiki, :admin_wiki) expect(subject).not_to include(:read_wiki, :create_wiki, :update_wiki, :admin_wiki)
end end
end end

View file

@ -0,0 +1,25 @@
require 'spec_helper'
describe ProjectFeaturesCompatibility do
let(:project) { create(:project) }
let(:features) { %w(issues wiki builds merge_requests snippets) }
# 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
# This spec tests if the described concern makes sure parameters received by the API are correctly parsed to the new table
# So we can keep it compatible
it "converts fields from 'true' to ProjectFeature::ENABLED" do
features.each do |feature|
project.update_attribute("#{feature}_enabled".to_sym, "true")
expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::ENABLED)
end
end
it "converts fields from 'false' to ProjectFeature::DISABLED" do
features.each do |feature|
project.update_attribute("#{feature}_enabled".to_sym, "false")
expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED)
end
end
end

View file

@ -0,0 +1,91 @@
require 'spec_helper'
describe ProjectFeature do
let(:project) { create(:project) }
let(:user) { create(:user) }
describe '#feature_available?' do
let(:features) { %w(issues wiki builds merge_requests snippets) }
context 'when features are disabled' do
it "returns false" do
features.each do |feature|
project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::DISABLED)
expect(project.feature_available?(:issues, user)).to eq(false)
end
end
end
context 'when features are enabled only for team members' do
it "returns false when user is not a team member" do
features.each do |feature|
project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
expect(project.feature_available?(:issues, user)).to eq(false)
end
end
it "returns true when user is a team member" do
project.team << [user, :developer]
features.each do |feature|
project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
expect(project.feature_available?(:issues, user)).to eq(true)
end
end
it "returns true when user is a member of project group" do
group = create(:group)
project = create(:project, namespace: group)
group.add_developer(user)
features.each do |feature|
project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
expect(project.feature_available?(:issues, user)).to eq(true)
end
end
it "returns true if user is an admin" do
user.update_attribute(:admin, true)
features.each do |feature|
project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
expect(project.feature_available?(:issues, user)).to eq(true)
end
end
end
context 'when feature is enabled for everyone' do
it "returns true" do
features.each do |feature|
project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED)
expect(project.feature_available?(:issues, user)).to eq(true)
end
end
end
end
describe '#*_enabled?' do
let(:features) { %w(wiki builds merge_requests) }
it "returns false when feature is disabled" do
features.each do |feature|
project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::DISABLED)
expect(project.public_send("#{feature}_enabled?")).to eq(false)
end
end
it "returns true when feature is enabled only for team members" do
features.each do |feature|
project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
expect(project.public_send("#{feature}_enabled?")).to eq(true)
end
end
it "returns true when feature is enabled for everyone" do
features.each do |feature|
project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED)
expect(project.public_send("#{feature}_enabled?")).to eq(true)
end
end
end
end

View file

@ -508,7 +508,7 @@ describe Project, models: true do
describe '#has_wiki?' do describe '#has_wiki?' do
let(:no_wiki_project) { build(:project, wiki_enabled: false, has_external_wiki: false) } let(:no_wiki_project) { build(:project, wiki_enabled: false, has_external_wiki: false) }
let(:wiki_enabled_project) { build(:project, wiki_enabled: true) } let(:wiki_enabled_project) { build(:project) }
let(:external_wiki_project) { build(:project, has_external_wiki: true) } let(:external_wiki_project) { build(:project, has_external_wiki: true) }
it 'returns true if project is wiki enabled or has external wiki' do it 'returns true if project is wiki enabled or has external wiki' do
@ -734,8 +734,6 @@ describe Project, models: true do
describe '#builds_enabled' do describe '#builds_enabled' do
let(:project) { create :project } let(:project) { create :project }
before { project.builds_enabled = true }
subject { project.builds_enabled } subject { project.builds_enabled }
it { expect(project.builds_enabled?).to be_truthy } it { expect(project.builds_enabled?).to be_truthy }

View file

@ -1006,8 +1006,7 @@ describe User, models: true do
end end
it 'does not include projects for which issues are disabled' do it 'does not include projects for which issues are disabled' do
project = create(:project) project = create(:project, issues_access_level: ProjectFeature::DISABLED)
project.update_attributes(issues_enabled: false)
expect(user.projects_where_can_admin_issues.to_a).to be_empty expect(user.projects_where_can_admin_issues.to_a).to be_empty
expect(user.can?(:admin_issue, project)).to eq(false) expect(user.can?(:admin_issue, project)).to eq(false)

View file

@ -73,7 +73,7 @@ describe API::API, api: true do
end end
it 'does not include open_issues_count' do it 'does not include open_issues_count' do
project.update_attributes( { issues_enabled: false } ) project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
get api('/projects', user) get api('/projects', user)
expect(response.status).to eq 200 expect(response.status).to eq 200
@ -231,8 +231,15 @@ describe API::API, api: true do
post api('/projects', user), project post api('/projects', user), project
project.each_pair do |k, v| project.each_pair do |k, v|
next if %i{ issues_enabled merge_requests_enabled wiki_enabled }.include?(k)
expect(json_response[k.to_s]).to eq(v) expect(json_response[k.to_s]).to eq(v)
end end
# Check feature permissions attributes
project = Project.find_by_path(project[:path])
expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED)
expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED)
expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED)
end end
it 'sets a project as public' do it 'sets a project as public' do

View file

@ -289,7 +289,8 @@ describe 'Git HTTP requests', lib: true do
let(:project) { FactoryGirl.create :empty_project } let(:project) { FactoryGirl.create :empty_project }
before do before do
project.update_attributes(runners_token: token, builds_enabled: true) project.update_attributes(runners_token: token)
project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED)
end end
it "downloads get status 200" do it "downloads get status 200" do

View file

@ -22,19 +22,20 @@ describe JwtController do
context 'when using authorized request' do context 'when using authorized request' do
context 'using CI token' do context 'using CI token' do
let(:project) { create(:empty_project, runners_token: 'token', builds_enabled: builds_enabled) } let(:project) { create(:empty_project, runners_token: 'token') }
let(:headers) { { authorization: credentials('gitlab-ci-token', project.runners_token) } } let(:headers) { { authorization: credentials('gitlab-ci-token', project.runners_token) } }
subject! { get '/jwt/auth', parameters, headers }
context 'project with enabled CI' do context 'project with enabled CI' do
let(:builds_enabled) { true } subject! { get '/jwt/auth', parameters, headers }
it { expect(service_class).to have_received(:new).with(project, nil, parameters) } it { expect(service_class).to have_received(:new).with(project, nil, parameters) }
end end
context 'project with disabled CI' do context 'project with disabled CI' do
let(:builds_enabled) { false } before do
project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
end
subject! { get '/jwt/auth', parameters, headers }
it { expect(response).to have_http_status(403) } it { expect(response).to have_http_status(403) }
end end

View file

@ -151,6 +151,25 @@ module Ci
it { expect(build.runner).to eq(specific_runner) } it { expect(build.runner).to eq(specific_runner) }
end end
end end
context 'disallow when builds are disabled' do
before do
project.update(shared_runners_enabled: true)
project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
end
context 'and uses shared runner' do
let(:build) { service.execute(shared_runner) }
it { expect(build).to be_nil }
end
context 'and uses specific runner' do
let(:build) { service.execute(specific_runner) }
it { expect(build).to be_nil }
end
end
end end
end end
end end

View file

@ -50,7 +50,7 @@ describe MergeRequests::GetUrlsService do
let(:changes) { new_branch_changes } let(:changes) { new_branch_changes }
before do before do
project.merge_requests_enabled = false project.project_feature.update_attribute(:merge_requests_access_level, ProjectFeature::DISABLED)
end end
it_behaves_like 'no_merge_request_url' it_behaves_like 'no_merge_request_url'

View file

@ -69,7 +69,7 @@ describe Projects::CreateService, services: true do
context 'wiki_enabled false does not create wiki repository directory' do context 'wiki_enabled false does not create wiki repository directory' do
before do before do
@opts.merge!(wiki_enabled: false) @opts.merge!( { project_feature_attributes: { wiki_access_level: ProjectFeature::DISABLED } })
@project = create_project(@user, @opts) @project = create_project(@user, @opts)
@path = ProjectWiki.new(@project, @user).send(:path_to_repo) @path = ProjectWiki.new(@project, @user).send(:path_to_repo)
end end
@ -85,7 +85,7 @@ describe Projects::CreateService, services: true do
context 'global builds_enabled false does not enable CI by default' do context 'global builds_enabled false does not enable CI by default' do
before do before do
@opts.merge!(builds_enabled: false) project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
end end
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
@ -93,7 +93,7 @@ describe Projects::CreateService, services: true do
context 'global builds_enabled true does enable CI by default' do context 'global builds_enabled true does enable CI by default' do
before do before do
@opts.merge!(builds_enabled: true) project.project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
end end
it { is_expected.to be_truthy } it { is_expected.to be_truthy }

View file

@ -5,7 +5,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
subject { described_class.new } subject { described_class.new }
it 'passes when the project has no push events' do it 'passes when the project has no push events' do
project = create(:project_empty_repo, wiki_enabled: false) project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED)
project.events.destroy_all project.events.destroy_all
break_repo(project) break_repo(project)
@ -25,7 +25,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
end end
it 'fails if the wiki repository is broken' do it 'fails if the wiki repository is broken' do
project = create(:project_empty_repo, wiki_enabled: true) project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED)
project.create_wiki project.create_wiki
# Test sanity: everything should be fine before the wiki repo is broken # Test sanity: everything should be fine before the wiki repo is broken
@ -39,7 +39,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
end end
it 'skips wikis when disabled' do it 'skips wikis when disabled' do
project = create(:project_empty_repo, wiki_enabled: false) project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED)
# Make sure the test would fail if the wiki repo was checked # Make sure the test would fail if the wiki repo was checked
break_wiki(project) break_wiki(project)
@ -49,7 +49,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
end end
it 'creates missing wikis' do it 'creates missing wikis' do
project = create(:project_empty_repo, wiki_enabled: true) project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED)
FileUtils.rm_rf(wiki_path(project)) FileUtils.rm_rf(wiki_path(project))
subject.perform(project.id) subject.perform(project.id)