diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index e0ecdb0c0e9..15f7ef881c8 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -89,7 +89,8 @@ class Admin::GroupsController < Admin::ApplicationController :request_access_enabled, :visibility_level, :require_two_factor_authentication, - :two_factor_grace_period + :two_factor_grace_period, + :project_creation_level ] end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 87b8ef03313..e936d771502 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -187,7 +187,8 @@ class GroupsController < Groups::ApplicationController :create_chat_team, :chat_team_name, :require_two_factor_authentication, - :two_factor_grace_period + :two_factor_grace_period, + :project_creation_level ] end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index e635f608237..e275e4278a4 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -137,6 +137,7 @@ module ApplicationSettingsHelper :default_artifacts_expire_in, :default_branch_protection, :default_group_visibility, + :default_project_creation, :default_project_visibility, :default_projects_limit, :default_snippet_visibility, diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index ea3bcfc791a..572d68cb4a3 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -49,6 +49,13 @@ module NamespacesHelper end end + def namespaces_options_with_developer_maintainer_access(options = {}) + selected = options.delete(:selected) || :current_user + options[:groups] = current_user.manageable_groups_with_routes(include_groups_with_developer_maintainer_access: true) + + namespaces_options(selected, options) + end + private # Many importers create a temporary Group, so use the real diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 265aa1d4965..b413ffddb9d 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -26,6 +26,7 @@ module ApplicationSettingImplementation default_artifacts_expire_in: '30 days', default_branch_protection: Settings.gitlab['default_branch_protection'], default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'], + default_project_creation: Settings.gitlab['default_project_creation'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_projects_limit: Settings.gitlab['default_projects_limit'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], diff --git a/app/models/group.rb b/app/models/group.rb index ac66815705c..8bc9b75f0a9 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -404,6 +404,10 @@ class Group < Namespace Feature.enabled?(:group_clusters, root_ancestor, default_enabled: true) end + def project_creation_level + super || ::Gitlab::CurrentSettings.default_project_creation + end + private def update_two_factor_requirement diff --git a/app/models/user.rb b/app/models/user.rb index 259889995d3..d3524bfd6ae 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -105,6 +105,7 @@ class User < ApplicationRecord has_many :groups, through: :group_members has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :group has_many :maintainers_groups, -> { where(members: { access_level: Gitlab::Access::MAINTAINER }) }, through: :group_members, source: :group + has_many :developer_groups, -> { where(members: { access_level: ::Gitlab::Access::DEVELOPER }) }, through: :group_members, source: :group has_many :owned_or_maintainers_groups, -> { where(members: { access_level: [Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) }, through: :group_members, @@ -883,7 +884,12 @@ class User < ApplicationRecord # rubocop: enable CodeReuse/ServiceClass def several_namespaces? - owned_groups.any? || maintainers_groups.any? + union_sql = ::Gitlab::SQL::Union.new( + [owned_groups, + maintainers_groups, + groups_with_developer_maintainer_project_access]).to_sql + + ::Group.from("(#{union_sql}) #{::Group.table_name}").any? end def namespace_id @@ -1169,12 +1175,24 @@ class User < ApplicationRecord @manageable_namespaces ||= [namespace] + manageable_groups end - def manageable_groups - Gitlab::ObjectHierarchy.new(owned_or_maintainers_groups).base_and_descendants + def manageable_groups(include_groups_with_developer_maintainer_access: false) + owned_and_maintainer_group_hierarchy = Gitlab::ObjectHierarchy.new(owned_or_maintainers_groups).base_and_descendants + + if include_groups_with_developer_maintainer_access + union_sql = ::Gitlab::SQL::Union.new( + [owned_and_maintainer_group_hierarchy, + groups_with_developer_maintainer_project_access]).to_sql + + ::Group.from("(#{union_sql}) #{::Group.table_name}") + else + owned_and_maintainer_group_hierarchy + end end - def manageable_groups_with_routes - manageable_groups.eager_load(:route).order('routes.path') + def manageable_groups_with_routes(include_groups_with_developer_maintainer_access: false) + manageable_groups(include_groups_with_developer_maintainer_access: include_groups_with_developer_maintainer_access) + .eager_load(:route) + .order('routes.path') end def namespaces @@ -1573,4 +1591,16 @@ class User < ApplicationRecord ensure Gitlab::ExclusiveLease.cancel(lease_key, uuid) end + + def groups_with_developer_maintainer_project_access + project_creation_levels = [::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS] + + if ::Gitlab::CurrentSettings.default_project_creation == ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS + project_creation_levels << nil + end + + developer_groups_hierarchy = ::Gitlab::ObjectHierarchy.new(developer_groups).base_and_descendants + ::Group.where(id: developer_groups_hierarchy.select(:id), + project_creation_level: project_creation_levels) + end end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index db49d3bed9c..eb2e536e8e9 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -35,6 +35,14 @@ class GroupPolicy < BasePolicy with_options scope: :subject, score: 0 condition(:request_access_enabled) { @subject.request_access_enabled } + condition(:create_projects_disabled) do + @subject.project_creation_level == ::Gitlab::Access::NO_ONE_PROJECT_ACCESS + end + + condition(:developer_maintainer_access) do + @subject.project_creation_level == ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS + end + rule { public_group }.policy do enable :read_group enable :read_list @@ -115,6 +123,9 @@ class GroupPolicy < BasePolicy rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster + rule { developer & developer_maintainer_access }.enable :create_projects + rule { create_projects_disabled }.prevent :create_projects + def access_level return GroupMember::NO_ACCESS if @user.nil? diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml index 8122d81f578..03ef2924617 100644 --- a/app/views/admin/application_settings/_visibility_and_access.html.haml +++ b/app/views/admin/application_settings/_visibility_and_access.html.haml @@ -5,7 +5,9 @@ .form-group = f.label :default_branch_protection, class: 'label-bold' = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control' - = render_if_exists 'admin/application_settings/project_creation_level', form: f, application_setting: @application_setting + .form-group + = f.label s_('ProjectCreationLevel|Default project creation protection'), class: 'label-bold' + = f.select :default_project_creation, options_for_select(Gitlab::Access.project_creation_options, @application_setting.default_project_creation), {}, class: 'form-control' .form-group.visibility-level-setting = f.label :default_project_visibility, class: 'label-bold' = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new) diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml index ff59013ed67..7390c42aba2 100644 --- a/app/views/groups/_group_admin_settings.html.haml +++ b/app/views/groups/_group_admin_settings.html.haml @@ -9,6 +9,10 @@ = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') %br/ %span.descr This setting can be overridden in each project. +.form-group.row + = f.label s_('ProjectCreationLevel|Allowed to create projects'), class: 'col-form-label col-sm-2' + .col-sm-10 + = f.select :project_creation_level, options_for_select(::Gitlab::Access.project_creation_options, @group.project_creation_level), {}, class: 'form-control' .form-group.row = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2 pt-0' diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index 6b0a6e7ed99..0a14830c666 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -18,6 +18,7 @@ %span.descr.text-muted= share_with_group_lock_help_text(@group) = render 'groups/settings/lfs', f: f + = render 'groups/settings/project_creation_level', f: f, group: @group = render 'groups/settings/two_factor_auth', f: f = render_if_exists 'groups/member_lock_setting', f: f, group: @group diff --git a/app/views/groups/settings/_project_creation_level.html.haml b/app/views/groups/settings/_project_creation_level.html.haml new file mode 100644 index 00000000000..9f711e6aade --- /dev/null +++ b/app/views/groups/settings/_project_creation_level.html.haml @@ -0,0 +1,3 @@ +.form-group + = f.label s_('ProjectCreationLevel|Allowed to create projects'), class: 'label-bold' + = f.select :project_creation_level, options_for_select(::Gitlab::Access.project_creation_options, group.project_creation_level), {}, class: 'form-control' diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index 5129f11875c..1c1c7d832bd 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -19,9 +19,9 @@ = root_url - namespace_id = namespace_id_from(params) = f.select(:namespace_id, - namespaces_options(namespace_id || :current_user, - display_path: true, - extra_group: namespace_id), + namespaces_options_with_developer_maintainer_access(selected: namespace_id, + display_path: true, + extra_group: namespace_id), {}, { class: 'select2 js-select-namespace qa-project-namespace-select block-truncated', tabindex: 1, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_path", track_value: "" }}) diff --git a/changelogs/unreleased/move-allow-developers-to-create-projects-in-groups-to-core.yml b/changelogs/unreleased/move-allow-developers-to-create-projects-in-groups-to-core.yml new file mode 100644 index 00000000000..34fd0c1b787 --- /dev/null +++ b/changelogs/unreleased/move-allow-developers-to-create-projects-in-groups-to-core.yml @@ -0,0 +1,5 @@ +--- +title: Move allow developers to create projects in groups to Core +merge_request: 25975 +author: +type: added diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 01ffcade931..3c426cdb969 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -126,6 +126,7 @@ Settings['issues_tracker'] ||= {} # GitLab # Settings['gitlab'] ||= Settingslogic.new({}) +Settings.gitlab['default_project_creation'] ||= ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS Settings.gitlab['default_projects_limit'] ||= 100000 Settings.gitlab['default_branch_protection'] ||= 2 Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil? diff --git a/db/migrate/20190311132500_add_default_project_creation_application_setting.rb b/db/migrate/20190311132500_add_default_project_creation_application_setting.rb new file mode 100644 index 00000000000..87427ad2930 --- /dev/null +++ b/db/migrate/20190311132500_add_default_project_creation_application_setting.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddDefaultProjectCreationApplicationSetting < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + unless column_exists?(:application_settings, :default_project_creation) + add_column(:application_settings, :default_project_creation, :integer, default: 2, null: false) + end + end + + def down + if column_exists?(:application_settings, :default_project_creation) + remove_column(:application_settings, :default_project_creation) + end + end +end diff --git a/db/migrate/20190311132527_add_project_creation_level_to_namespaces.rb b/db/migrate/20190311132527_add_project_creation_level_to_namespaces.rb new file mode 100644 index 00000000000..159e0a95ace --- /dev/null +++ b/db/migrate/20190311132527_add_project_creation_level_to_namespaces.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddProjectCreationLevelToNamespaces < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + unless column_exists?(:namespaces, :project_creation_level) + add_column :namespaces, :project_creation_level, :integer + end + end + + def down + unless column_exists?(:namespaces, :project_creation_level) + remove_column :namespaces, :project_creation_level, :integer + end + end +end diff --git a/db/schema.rb b/db/schema.rb index b20fe4b3d39..1a50c6efbc7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -177,6 +177,7 @@ ActiveRecord::Schema.define(version: 20190325165127) do t.string "runners_registration_token_encrypted" t.integer "local_markdown_version", default: 0, null: false t.integer "first_day_of_week", default: 0, null: false + t.integer "default_project_creation", default: 2, null: false t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree end @@ -1391,6 +1392,7 @@ ActiveRecord::Schema.define(version: 20190325165127) do t.integer "cached_markdown_version" t.string "runners_token" t.string "runners_token_encrypted" + t.integer "project_creation_level" t.boolean "auto_devops_enabled" t.index ["created_at"], name: "index_namespaces_on_created_at", using: :btree t.index ["name", "parent_id"], name: "index_namespaces_on_name_and_parent_id", unique: true, using: :btree diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 3bcfd30079d..2f1cadb2bbc 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -151,6 +151,17 @@ There are two different ways to add a new project to a group: ![Select group](img/select_group_dropdown.png) +### Default project creation level + +Group owners or administrators can allow users with the +Developer role to create projects under groups. + +By default, [Developers and Maintainers](../permissions.md##group-members-permissions) can create projects under agroup, but this can be changed either within the group settings for a group, or +be set globally by a GitLab administrator in the Admin area +at **Settings > General > Visibility and access controls**. + +Available settings are `No one`, `Maintainers`, or `Developers + Maintainers`. + ## Transfer projects into groups Learn how to [transfer a project into a group](../project/settings/index.md#transferring-an-existing-project-into-another-namespace). diff --git a/lib/api/settings.rb b/lib/api/settings.rb index d742c6c97c1..d96cdc31212 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -40,7 +40,8 @@ module API end optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts" - optional :default_branch_protection, type: Integer, values: Gitlab::Access.protection_values, desc: 'Determine if developers can push to master' + optional :default_project_creation, type: Integer, values: ::Gitlab::Access.project_creation_values, desc: 'Determine if developers can create projects in the group' + optional :default_branch_protection, type: Integer, values: ::Gitlab::Access.protection_values, desc: 'Determine if developers can push to master' optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default group visibility' optional :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility' optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects' diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb index 6c8ca8f219c..6eb08f674c2 100644 --- a/lib/gitlab/access.rb +++ b/lib/gitlab/access.rb @@ -24,6 +24,11 @@ module Gitlab PROTECTION_FULL = 2 PROTECTION_DEV_CAN_MERGE = 3 + # Default project creation level + NO_ONE_PROJECT_ACCESS = 0 + MAINTAINER_PROJECT_ACCESS = 1 + DEVELOPER_MAINTAINER_PROJECT_ACCESS = 2 + class << self delegate :values, to: :options @@ -85,6 +90,22 @@ module Gitlab def human_access_with_none(access) options_with_none.key(access) end + + def project_creation_options + { + s_('ProjectCreationLevel|No one') => NO_ONE_PROJECT_ACCESS, + s_('ProjectCreationLevel|Maintainers') => MAINTAINER_PROJECT_ACCESS, + s_('ProjectCreationLevel|Developers + Maintainers') => DEVELOPER_MAINTAINER_PROJECT_ACCESS + } + end + + def project_creation_values + project_creation_options.values + end + + def project_creation_level_name(name) + project_creation_options.key(name) + end end def human_access diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 554bb55f9ef..1166134347c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6418,6 +6418,21 @@ msgstr "" msgid "ProjectActivityRSS|Subscribe" msgstr "" +msgid "ProjectCreationLevel|Allowed to create projects" +msgstr "" + +msgid "ProjectCreationLevel|Default project creation protection" +msgstr "" + +msgid "ProjectCreationLevel|Developers + Maintainers" +msgstr "" + +msgid "ProjectCreationLevel|Maintainers" +msgstr "" + +msgid "ProjectCreationLevel|No one" +msgstr "" + msgid "ProjectFileTree|Name" msgstr "" diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb index 9af472df74e..1a7be4c9a85 100644 --- a/spec/controllers/admin/application_settings_controller_spec.rb +++ b/spec/controllers/admin/application_settings_controller_spec.rb @@ -85,6 +85,13 @@ describe Admin::ApplicationSettingsController do expect(response).to redirect_to(admin_application_settings_path) expect(ApplicationSetting.current.receive_max_input_size).to eq(1024) end + + it 'updates the default_project_creation for string value' do + put :update, params: { application_setting: { default_project_creation: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS } } + + expect(response).to redirect_to(admin_application_settings_path) + expect(ApplicationSetting.current.default_project_creation).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS) + end end describe 'PUT #reset_registration_token' do diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb index 647fce0ecef..22165faa625 100644 --- a/spec/controllers/admin/groups_controller_spec.rb +++ b/spec/controllers/admin/groups_controller_spec.rb @@ -60,5 +60,11 @@ describe Admin::GroupsController do expect(response).to redirect_to(admin_group_path(group)) expect(group.users).not_to include group_user end + + it 'updates the project_creation_level successfully' do + expect do + post :update, params: { id: group.to_param, group: { project_creation_level: ::Gitlab::Access::NO_ONE_PROJECT_ACCESS } } + end.to change { group.reload.project_creation_level }.to(::Gitlab::Access::NO_ONE_PROJECT_ACCESS) + end end end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 2b803e7151f..4a28a27da79 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -349,6 +349,13 @@ describe GroupsController do expect(assigns(:group).errors).not_to be_empty expect(assigns(:group).path).not_to eq('new_path') end + + it 'updates the project_creation_level successfully' do + post :update, params: { id: group.to_param, group: { project_creation_level: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS } } + + expect(response).to have_gitlab_http_status(302) + expect(group.reload.project_creation_level).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS) + end end describe '#ensure_canonical_path' do diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index dcef8571f41..18a0c2ec731 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -4,6 +4,7 @@ FactoryBot.define do path { name.downcase.gsub(/\s/, '_') } type 'Group' owner nil + project_creation_level ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS after(:create) do |group| if group.owner diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb index 378e4d5febc..5cef5f0521f 100644 --- a/spec/features/groups/group_settings_spec.rb +++ b/spec/features/groups/group_settings_spec.rb @@ -77,6 +77,14 @@ describe 'Edit group settings' do end end + describe 'project creation level menu' do + it 'shows the selection menu' do + visit edit_group_path(group) + + expect(page).to have_content('Allowed to create projects') + end + end + describe 'edit group avatar' do before do visit edit_group_path(group) diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index 75c72a68069..b54ea929978 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -252,4 +252,23 @@ describe 'New project' do end end end + + context 'Namespace selector' do + context 'with group with DEVELOPER_MAINTAINER_PROJECT_ACCESS project_creation_level' do + let(:group) { create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) } + + before do + group.add_developer(user) + visit new_project_path(namespace_id: group.id) + end + + it 'selects the group namespace' do + page.within('#blank-project-pane') do + namespace = find('#project_namespace_id option[selected]') + + expect(namespace.text).to eq group.full_path + end + end + end + end end diff --git a/spec/features/projects/user_creates_project_spec.rb b/spec/features/projects/user_creates_project_spec.rb index 8d7e2883b2a..c0932539131 100644 --- a/spec/features/projects/user_creates_project_spec.rb +++ b/spec/features/projects/user_creates_project_spec.rb @@ -54,4 +54,31 @@ describe 'User creates a project', :js do expect(project.namespace).to eq(subgroup) end end + + context 'in a group with DEVELOPER_MAINTAINER_PROJECT_ACCESS project_creation_level' do + let(:group) { create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) } + + before do + group.add_developer(user) + end + + it 'creates a new project' do + visit(new_project_path) + + fill_in :project_name, with: 'a-new-project' + fill_in :project_path, with: 'a-new-project' + + page.find('.js-select-namespace').click + page.find("div[role='option']", text: group.full_path).click + + page.within('#content-body') do + click_button('Create project') + end + + expect(page).to have_content("Project 'a-new-project' was successfully created") + + project = Project.find_by(name: 'a-new-project') + expect(project.namespace).to eq(group) + end + end end diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb index 7ccbdcd1332..601f864ef36 100644 --- a/spec/helpers/namespaces_helper_spec.rb +++ b/spec/helpers/namespaces_helper_spec.rb @@ -1,10 +1,38 @@ require 'spec_helper' -describe NamespacesHelper do +describe NamespacesHelper, :postgresql do let!(:admin) { create(:admin) } - let!(:admin_group) { create(:group, :private) } + let!(:admin_project_creation_level) { nil } + let!(:admin_group) do + create(:group, + :private, + project_creation_level: admin_project_creation_level) + end let!(:user) { create(:user) } - let!(:user_group) { create(:group, :private) } + let!(:user_project_creation_level) { nil } + let!(:user_group) do + create(:group, + :private, + project_creation_level: user_project_creation_level) + end + let!(:subgroup1) do + create(:group, + :private, + parent: admin_group, + project_creation_level: nil) + end + let!(:subgroup2) do + create(:group, + :private, + parent: admin_group, + project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) + end + let!(:subgroup3) do + create(:group, + :private, + parent: admin_group, + project_creation_level: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS) + end before do admin_group.add_owner(admin) @@ -105,5 +133,43 @@ describe NamespacesHelper do helper.namespaces_options end end + + describe 'include_groups_with_developer_maintainer_access parameter' do + context 'when DEVELOPER_MAINTAINER_PROJECT_ACCESS is set for a project' do + let!(:admin_project_creation_level) { ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS } + + it 'returns groups where user is a developer' do + allow(helper).to receive(:current_user).and_return(user) + stub_application_setting(default_project_creation: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS) + admin_group.add_user(user, GroupMember::DEVELOPER) + + options = helper.namespaces_options_with_developer_maintainer_access + + expect(options).to include(admin_group.name) + expect(options).not_to include(subgroup1.name) + expect(options).to include(subgroup2.name) + expect(options).not_to include(subgroup3.name) + expect(options).to include(user_group.name) + expect(options).to include(user.name) + end + end + + context 'when DEVELOPER_MAINTAINER_PROJECT_ACCESS is set globally' do + it 'return groups where default is not overridden' do + allow(helper).to receive(:current_user).and_return(user) + stub_application_setting(default_project_creation: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) + admin_group.add_user(user, GroupMember::DEVELOPER) + + options = helper.namespaces_options_with_developer_maintainer_access + + expect(options).to include(admin_group.name) + expect(options).to include(subgroup1.name) + expect(options).to include(subgroup2.name) + expect(options).not_to include(subgroup3.name) + expect(options).to include(user_group.name) + expect(options).to include(user.name) + end + end + end end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index ad3e3061b9a..e6e7298a043 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -959,4 +959,12 @@ describe Group do end end end + + describe 'project_creation_level' do + it 'outputs the default one if it is nil' do + group = create(:group, project_creation_level: nil) + + expect(group.project_creation_level).to eq(Gitlab::CurrentSettings.default_project_creation) + end + end end diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index dc98baca6dc..59f3a961d50 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -347,6 +347,120 @@ describe GroupPolicy do end end + context "create_projects" do + context 'when group has no project creation level set' do + let(:group) { create(:group, project_creation_level: nil) } + + context 'reporter' do + let(:current_user) { reporter } + + it { is_expected.to be_disallowed(:create_projects) } + end + + context 'developer' do + let(:current_user) { developer } + + it { is_expected.to be_allowed(:create_projects) } + end + + context 'maintainer' do + let(:current_user) { maintainer } + + it { is_expected.to be_allowed(:create_projects) } + end + + context 'owner' do + let(:current_user) { owner } + + it { is_expected.to be_allowed(:create_projects) } + end + end + + context 'when group has project creation level set to no one' do + let(:group) { create(:group, project_creation_level: ::Gitlab::Access::NO_ONE_PROJECT_ACCESS) } + + context 'reporter' do + let(:current_user) { reporter } + + it { is_expected.to be_disallowed(:create_projects) } + end + + context 'developer' do + let(:current_user) { developer } + + it { is_expected.to be_disallowed(:create_projects) } + end + + context 'maintainer' do + let(:current_user) { maintainer } + + it { is_expected.to be_disallowed(:create_projects) } + end + + context 'owner' do + let(:current_user) { owner } + + it { is_expected.to be_disallowed(:create_projects) } + end + end + + context 'when group has project creation level set to maintainer only' do + let(:group) { create(:group, project_creation_level: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS) } + + context 'reporter' do + let(:current_user) { reporter } + + it { is_expected.to be_disallowed(:create_projects) } + end + + context 'developer' do + let(:current_user) { developer } + + it { is_expected.to be_disallowed(:create_projects) } + end + + context 'maintainer' do + let(:current_user) { maintainer } + + it { is_expected.to be_allowed(:create_projects) } + end + + context 'owner' do + let(:current_user) { owner } + + it { is_expected.to be_allowed(:create_projects) } + end + end + + context 'when group has project creation level set to developers + maintainer' do + let(:group) { create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) } + + context 'reporter' do + let(:current_user) { reporter } + + it { is_expected.to be_disallowed(:create_projects) } + end + + context 'developer' do + let(:current_user) { developer } + + it { is_expected.to be_allowed(:create_projects) } + end + + context 'maintainer' do + let(:current_user) { maintainer } + + it { is_expected.to be_allowed(:create_projects) } + end + + context 'owner' do + let(:current_user) { owner } + + it { is_expected.to be_allowed(:create_projects) } + end + end + end + it_behaves_like 'clusterable policies' do let(:clusterable) { create(:group) } let(:cluster) do diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index f33eb5b9e02..f869325e892 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -44,6 +44,7 @@ describe API::Settings, 'Settings' do put api("/application/settings", admin), params: { default_projects_limit: 3, + default_project_creation: 2, password_authentication_enabled_for_web: false, repository_storages: ['custom'], plantuml_enabled: true, @@ -64,12 +65,13 @@ describe API::Settings, 'Settings' do performance_bar_allowed_group_path: group.full_path, instance_statistics_visibility_private: true, diff_max_patch_bytes: 150_000, - default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE, + default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE, local_markdown_version: 3 } expect(response).to have_gitlab_http_status(200) expect(json_response['default_projects_limit']).to eq(3) + expect(json_response['default_project_creation']).to eq(::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) expect(json_response['password_authentication_enabled_for_web']).to be_falsey expect(json_response['repository_storages']).to eq(['custom']) expect(json_response['plantuml_enabled']).to be_truthy