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:
commit
d308a3f433
60 changed files with 713 additions and 143 deletions
|
@ -36,6 +36,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
|
||||
|
|
37
app/models/concerns/project_features_compatibility.rb
Normal file
37
app/models/concerns/project_features_compatibility.rb
Normal 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
|
|
@ -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
|
||||
|
|
63
app/models/project_feature.rb
Normal file
63
app/models/project_feature.rb
Normal 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
|
|
@ -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,42 +44,45 @@
|
|||
%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
|
||||
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
|
||||
|
@ -90,6 +93,7 @@
|
|||
%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
|
||||
|
|
16
db/migrate/20160831214002_create_project_features.rb
Normal file
16
db/migrate/20160831214002_create_project_features.rb
Normal 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
|
44
db/migrate/20160831214543_migrate_project_features.rb
Normal file
44
db/migrate/20160831214543_migrate_project_features.rb
Normal 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
|
|
@ -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
|
20
db/schema.rb
20
db/schema.rb
|
@ -11,7 +11,7 @@
|
|||
#
|
||||
# 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
|
||||
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", ["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|
|
||||
t.integer "project_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 "updated_at"
|
||||
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.boolean "snippets_enabled", default: true, null: false
|
||||
t.datetime "last_activity_at"
|
||||
t.string "import_url"
|
||||
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.text "import_error"
|
||||
t.integer "ci_id"
|
||||
t.boolean "builds_enabled", default: true, null: false
|
||||
t.boolean "shared_runners_enabled", default: true, null: false
|
||||
t.string "runners_token"
|
||||
t.string "build_coverage_regex"
|
||||
|
|
|
@ -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 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 permissions rely on the role the user has in GitLab. There are four
|
||||
|
|
|
@ -5,7 +5,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps
|
|||
|
||||
step 'change project settings' do
|
||||
fill_in 'project_name_edit', with: 'NewName'
|
||||
uncheck 'project_issues_enabled'
|
||||
select 'Disabled', from: 'project_project_feature_attributes_issues_access_level'
|
||||
end
|
||||
|
||||
step 'I save project' do
|
||||
|
|
|
@ -15,7 +15,7 @@ module SharedProject
|
|||
# Create a specific project called "Shop"
|
||||
step 'I own project "Shop"' do
|
||||
@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]
|
||||
end
|
||||
|
||||
|
@ -41,6 +41,8 @@ module SharedProject
|
|||
step 'I own project "Forum"' do
|
||||
@project = Project.find_by(name: "Forum")
|
||||
@project ||= create(:project, name: "Forum", namespace: @user.namespace, path: 'forum_project')
|
||||
@project.build_project_feature
|
||||
@project.project_feature.save
|
||||
@project.team << [@user, :master]
|
||||
end
|
||||
|
||||
|
@ -95,7 +97,7 @@ module SharedProject
|
|||
step 'I should see project settings' do
|
||||
expect(current_path).to eq edit_namespace_project_path(@project.namespace, @project)
|
||||
expect(page).to have_content("Project name")
|
||||
expect(page).to have_content("Features")
|
||||
expect(page).to have_content("Feature Visibility")
|
||||
end
|
||||
|
||||
def current_project
|
||||
|
|
|
@ -76,7 +76,15 @@ module API
|
|||
expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group }
|
||||
expose :name, :name_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 :shared_runners_enabled, :lfs_enabled
|
||||
expose :creator_id
|
||||
|
@ -84,7 +92,7 @@ module API
|
|||
expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? }
|
||||
expose :avatar_url
|
||||
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 :public_builds
|
||||
expose :shared_with_groups do |project, options|
|
||||
|
|
|
@ -97,7 +97,7 @@ module API
|
|||
group = find_group(params[:id])
|
||||
projects = GroupProjectsFinder.new(group).execute(current_user)
|
||||
projects = paginate projects
|
||||
present projects, with: Entities::Project
|
||||
present projects, with: Entities::Project, user: current_user
|
||||
end
|
||||
|
||||
# Transfer a project to the Group namespace
|
||||
|
|
|
@ -51,7 +51,7 @@ module API
|
|||
@projects = current_user.viewable_starred_projects
|
||||
@projects = filter_projects(@projects)
|
||||
@projects = paginate @projects
|
||||
present @projects, with: Entities::Project
|
||||
present @projects, with: Entities::Project, user: current_user
|
||||
end
|
||||
|
||||
# Get all projects for admin user
|
||||
|
|
|
@ -168,7 +168,7 @@ module Gitlab
|
|||
unless project.wiki_enabled?
|
||||
wiki = WikiFormatter.new(project)
|
||||
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
|
||||
rescue Gitlab::Shell::Error => e
|
||||
# GitHub error message when the wiki repo has not been created,
|
||||
|
|
|
@ -11,7 +11,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def execute
|
||||
::Projects::CreateService.new(
|
||||
project = ::Projects::CreateService.new(
|
||||
current_user,
|
||||
name: repo.name,
|
||||
path: repo.name,
|
||||
|
@ -20,9 +20,15 @@ module Gitlab
|
|||
visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility,
|
||||
import_type: "github",
|
||||
import_source: repo.full_name,
|
||||
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
|
||||
import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@")
|
||||
).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
|
||||
|
|
|
@ -39,15 +39,12 @@ project_tree:
|
|||
- :labels
|
||||
- milestones:
|
||||
- :events
|
||||
- :project_feature
|
||||
|
||||
# Only include the following attributes for the models specified.
|
||||
included_attributes:
|
||||
project:
|
||||
- :description
|
||||
- :issues_enabled
|
||||
- :merge_requests_enabled
|
||||
- :wiki_enabled
|
||||
- :snippets_enabled
|
||||
- :visibility_level
|
||||
- :archived
|
||||
user:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
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(:user2) { create(:user) }
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ FactoryGirl.define do
|
|||
path { name.downcase.gsub(/\s/, '_') }
|
||||
namespace
|
||||
creator
|
||||
snippets_enabled true
|
||||
|
||||
trait :public do
|
||||
visibility_level Gitlab::VisibilityLevel::PUBLIC
|
||||
|
@ -27,6 +26,26 @@ FactoryGirl.define do
|
|||
project.create_repository
|
||||
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
|
||||
|
||||
# Project with empty repository
|
||||
|
|
122
spec/features/projects/features_visibility_spec.rb
Normal file
122
spec/features/projects/features_visibility_spec.rb
Normal 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
|
Binary file not shown.
|
@ -7,7 +7,8 @@ describe Gitlab::Auth, lib: true do
|
|||
it 'recognizes CI' do
|
||||
token = '123'
|
||||
project = create(:empty_project)
|
||||
project.update_attributes(runners_token: token, builds_enabled: true)
|
||||
project.update_attributes(runners_token: token)
|
||||
|
||||
ip = 'ip'
|
||||
|
||||
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'gitlab-ci-token')
|
||||
|
|
|
@ -3,7 +3,7 @@ require 'spec_helper'
|
|||
describe Gitlab::GithubImport::Importer, lib: true do
|
||||
describe '#execute' 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(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
|
||||
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
{
|
||||
"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,
|
||||
"archived": false,
|
||||
"issues": [
|
||||
|
|
|
@ -5,7 +5,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
|
|||
let(:user) { create(:user) }
|
||||
let(:namespace) { create(:namespace, owner: user) }
|
||||
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(: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
|
||||
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
|
||||
restored_project_json
|
||||
|
||||
|
|
|
@ -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
|
||||
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
|
||||
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)
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
|
|
@ -32,6 +32,12 @@ describe Gitlab::ImportExport::Reader, lib: true do
|
|||
expect(described_class.new(shared: shared).project_tree).to match(include: [:issues])
|
||||
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
|
||||
setup_yaml(project_tree: [:issues, :snippets])
|
||||
|
||||
|
|
|
@ -220,13 +220,13 @@ describe Ability, lib: true do
|
|||
end
|
||||
|
||||
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) }
|
||||
|
||||
context 'wiki named abilities' 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)
|
||||
end
|
||||
end
|
||||
|
|
25
spec/models/concerns/project_features_compatibility_spec.rb
Normal file
25
spec/models/concerns/project_features_compatibility_spec.rb
Normal 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
|
91
spec/models/project_feature_spec.rb
Normal file
91
spec/models/project_feature_spec.rb
Normal 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
|
|
@ -508,7 +508,7 @@ describe Project, models: true do
|
|||
|
||||
describe '#has_wiki?' do
|
||||
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) }
|
||||
|
||||
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
|
||||
let(:project) { create :project }
|
||||
|
||||
before { project.builds_enabled = true }
|
||||
|
||||
subject { project.builds_enabled }
|
||||
|
||||
it { expect(project.builds_enabled?).to be_truthy }
|
||||
|
|
|
@ -1006,8 +1006,7 @@ describe User, models: true do
|
|||
end
|
||||
|
||||
it 'does not include projects for which issues are disabled' do
|
||||
project = create(:project)
|
||||
project.update_attributes(issues_enabled: false)
|
||||
project = create(:project, issues_access_level: ProjectFeature::DISABLED)
|
||||
|
||||
expect(user.projects_where_can_admin_issues.to_a).to be_empty
|
||||
expect(user.can?(:admin_issue, project)).to eq(false)
|
||||
|
|
|
@ -73,7 +73,7 @@ describe API::API, api: true do
|
|||
end
|
||||
|
||||
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)
|
||||
expect(response.status).to eq 200
|
||||
|
@ -231,8 +231,15 @@ describe API::API, api: true do
|
|||
post api('/projects', user), project
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
it 'sets a project as public' do
|
||||
|
|
|
@ -289,7 +289,8 @@ describe 'Git HTTP requests', lib: true do
|
|||
let(:project) { FactoryGirl.create :empty_project }
|
||||
|
||||
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
|
||||
|
||||
it "downloads get status 200" do
|
||||
|
|
|
@ -22,19 +22,20 @@ describe JwtController do
|
|||
|
||||
context 'when using authorized request' 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) } }
|
||||
|
||||
subject! { get '/jwt/auth', parameters, headers }
|
||||
|
||||
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) }
|
||||
end
|
||||
|
||||
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) }
|
||||
end
|
||||
|
|
|
@ -151,6 +151,25 @@ module Ci
|
|||
it { expect(build.runner).to eq(specific_runner) }
|
||||
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
|
||||
|
|
|
@ -50,7 +50,7 @@ describe MergeRequests::GetUrlsService do
|
|||
let(:changes) { new_branch_changes }
|
||||
|
||||
before do
|
||||
project.merge_requests_enabled = false
|
||||
project.project_feature.update_attribute(:merge_requests_access_level, ProjectFeature::DISABLED)
|
||||
end
|
||||
|
||||
it_behaves_like 'no_merge_request_url'
|
||||
|
|
|
@ -69,7 +69,7 @@ describe Projects::CreateService, services: true do
|
|||
|
||||
context 'wiki_enabled false does not create wiki repository directory' do
|
||||
before do
|
||||
@opts.merge!(wiki_enabled: false)
|
||||
@opts.merge!( { project_feature_attributes: { wiki_access_level: ProjectFeature::DISABLED } })
|
||||
@project = create_project(@user, @opts)
|
||||
@path = ProjectWiki.new(@project, @user).send(:path_to_repo)
|
||||
end
|
||||
|
@ -85,7 +85,7 @@ describe Projects::CreateService, services: true do
|
|||
|
||||
context 'global builds_enabled false does not enable CI by default' do
|
||||
before do
|
||||
@opts.merge!(builds_enabled: false)
|
||||
project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
|
||||
end
|
||||
|
||||
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
|
||||
before do
|
||||
@opts.merge!(builds_enabled: true)
|
||||
project.project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
|
|
|
@ -5,7 +5,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
|
|||
subject { described_class.new }
|
||||
|
||||
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
|
||||
break_repo(project)
|
||||
|
||||
|
@ -25,7 +25,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
# Test sanity: everything should be fine before the wiki repo is broken
|
||||
|
@ -39,7 +39,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
|
|||
end
|
||||
|
||||
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
|
||||
break_wiki(project)
|
||||
|
||||
|
@ -49,7 +49,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
|
|||
end
|
||||
|
||||
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))
|
||||
|
||||
subject.perform(project.id)
|
||||
|
|
Loading…
Reference in a new issue