From ea4f3662d412f0297b6505a6a0e00380224d521e Mon Sep 17 00:00:00 2001 From: George Koltsov Date: Wed, 12 Apr 2023 10:21:42 +0100 Subject: [PATCH] Set minimum role for importing projects to Maintainer Update all GitLab's importers to require minimum role of Maintainer and above when importing projects to groups. Developer role and below are now not permitted to import projects. Changelog: added --- .../components/group_dropdown.vue | 4 +- .../import_groups/components/import_table.vue | 4 +- ...ere_user_can_import_projects.query.graphql | 18 +++ .../projects/new/components/app.vue | 24 +++- app/assets/javascripts/projects/new/index.js | 2 + app/controllers/import/base_controller.rb | 2 +- .../import/bitbucket_controller.rb | 4 +- app/controllers/import/gitea_controller.rb | 2 +- app/controllers/import/github_controller.rb | 2 +- .../import/gitlab_projects_controller.rb | 2 +- app/controllers/import/manifest_controller.rb | 4 +- .../projects/imports_controller.rb | 2 +- .../accepting_project_imports_finder.rb | 31 ++++++ app/finders/groups/user_groups_finder.rb | 6 + .../types/permission_types/group_enum.rb | 3 + .../namespaces/user_namespace_policy.rb | 5 +- app/services/import/base_service.rb | 2 +- .../import/bitbucket_server_service.rb | 2 +- app/services/import/fogbugz_service.rb | 4 +- app/services/import/github_service.rb | 2 +- app/services/projects/create_service.rb | 8 ++ app/views/projects/new.html.haml | 3 +- doc/api/graphql/reference/index.md | 1 + locale/gitlab.pot | 8 +- .../import/bitbucket_controller_spec.rb | 13 ++- .../bitbucket_server_controller_spec.rb | 2 +- .../import/fogbugz_controller_spec.rb | 2 +- .../import/gitea_controller_spec.rb | 2 +- .../import/gitlab_controller_spec.rb | 2 +- .../import/manifest_controller_spec.rb | 4 +- .../projects/imports_controller_spec.rb | 14 +-- .../accepting_project_imports_finder_spec.rb | 105 ++++++++++++++++++ .../finders/groups/user_groups_finder_spec.rb | 20 +++- .../components/group_dropdown_spec.js | 4 +- .../components/import_table_spec.js | 4 +- .../components/import_target_cell_spec.js | 4 +- .../projects/new/components/app_spec.js | 16 +++ .../namespaces/user_namespace_policy_spec.rb | 28 ++++- .../import/gitlab_projects_controller_spec.rb | 12 ++ .../import/bitbucket_server_service_spec.rb | 2 +- spec/services/import/fogbugz_service_spec.rb | 2 +- spec/services/import/github_service_spec.rb | 2 +- spec/services/projects/create_service_spec.rb | 17 +++ ...ubish_import_controller_shared_examples.rb | 13 +++ ...mport_controller_status_shared_examples.rb | 22 ++++ 45 files changed, 377 insertions(+), 58 deletions(-) create mode 100644 app/assets/javascripts/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql create mode 100644 app/finders/groups/accepting_project_imports_finder.rb create mode 100644 spec/finders/groups/accepting_project_imports_finder_spec.rb diff --git a/app/assets/javascripts/import_entities/components/group_dropdown.vue b/app/assets/javascripts/import_entities/components/group_dropdown.vue index 5b9e80f9d688cd..1c31c04a416c38 100644 --- a/app/assets/javascripts/import_entities/components/group_dropdown.vue +++ b/app/assets/javascripts/import_entities/components/group_dropdown.vue @@ -4,7 +4,7 @@ import { debounce } from 'lodash'; import { s__ } from '~/locale'; import { createAlert } from '~/alert'; -import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql'; +import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql'; import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; @@ -28,7 +28,7 @@ export default { }, apollo: { namespaces: { - query: searchNamespacesWhereUserCanCreateProjectsQuery, + query: searchNamespacesWhereUserCanImportProjectsQuery, variables() { return { search: this.searchTerm, diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue index 2e6e7cddf8f873..246d27d3b94f25 100644 --- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue +++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue @@ -24,7 +24,7 @@ import { getGroupPathAvailability } from '~/rest_api'; import axios from '~/lib/utils/axios_utils'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { helpPagePath } from '~/helpers/help_page_helper'; -import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql'; +import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { STATUSES } from '../../constants'; @@ -118,7 +118,7 @@ export default { }, }, availableNamespaces: { - query: searchNamespacesWhereUserCanCreateProjectsQuery, + query: searchNamespacesWhereUserCanImportProjectsQuery, update(data) { return data.currentUser.groups.nodes; }, diff --git a/app/assets/javascripts/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql b/app/assets/javascripts/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql new file mode 100644 index 00000000000000..8c41f7116b356a --- /dev/null +++ b/app/assets/javascripts/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql @@ -0,0 +1,18 @@ +query searchNamespacesWhereUserCanImportProjects($search: String) { + currentUser { + id + groups(permissionScope: IMPORT_PROJECTS, search: $search) { + nodes { + id + fullPath + name + visibility + webUrl + } + } + namespace { + id + fullPath + } + } +} diff --git a/app/assets/javascripts/projects/new/components/app.vue b/app/assets/javascripts/projects/new/components/app.vue index 0a160a357e5652..2f58d4468be70e 100644 --- a/app/assets/javascripts/projects/new/components/app.vue +++ b/app/assets/javascripts/projects/new/components/app.vue @@ -9,6 +9,7 @@ import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue' import NewProjectPushTipPopover from './new_project_push_tip_popover.vue'; const CI_CD_PANEL = 'cicd_for_external_repo'; +const IMPORT_PROJECT_PANEL = 'import_project'; const PANELS = [ { key: 'blank', @@ -32,7 +33,7 @@ const PANELS = [ }, { key: 'import', - name: 'import_project', + name: IMPORT_PROJECT_PANEL, selector: '#import-project-pane', title: s__('ProjectsNew|Import project'), description: s__( @@ -92,6 +93,11 @@ export default { required: false, default: '', }, + canImportProjects: { + type: Boolean, + required: false, + default: true, + }, }, computed: { @@ -106,7 +112,21 @@ export default { return breadcrumbs; }, availablePanels() { - return this.isCiCdAvailable ? PANELS : PANELS.filter((p) => p.name !== CI_CD_PANEL); + if (this.isCiCdAvailable && this.canImportProjects) { + return PANELS; + } + + return PANELS.filter((panel) => { + if (!this.canImportProjects && panel.name === IMPORT_PROJECT_PANEL) { + return false; + } + + if (!this.isCiCdAvailable && panel.name === CI_CD_PANEL) { + return false; + } + + return true; + }); }, }, diff --git a/app/assets/javascripts/projects/new/index.js b/app/assets/javascripts/projects/new/index.js index 5ec50355a8218e..a5a833dc73bb14 100644 --- a/app/assets/javascripts/projects/new/index.js +++ b/app/assets/javascripts/projects/new/index.js @@ -19,6 +19,7 @@ export function initNewProjectCreation() { parentGroupName, projectsUrl, rootPath, + canImportProjects, } = el.dataset; const props = { @@ -29,6 +30,7 @@ export function initNewProjectCreation() { parentGroupName, projectsUrl, rootPath, + canImportProjects: parseBoolean(canImportProjects), }; const provide = { diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb index 7ef0703291386c..bcb6aed9e38705 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -18,7 +18,7 @@ def status if params[:namespace_id]&.present? @namespace = Namespace.find_by_id(params[:namespace_id]) - render_404 unless current_user.can?(:create_projects, @namespace) + render_404 unless current_user.can?(:import_projects, @namespace) end end end diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 8a0f4a36781afd..c933b05e0c4489 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -57,7 +57,7 @@ def create extra: { user_role: user_role(current_user, target_namespace), import_type: 'bitbucket' } ) - if current_user.can?(:create_projects, target_namespace) + if current_user.can?(:import_projects, target_namespace) # The token in a session can be expired, we need to get most recent one because # Bitbucket::Connection class refreshes it. session[:bitbucket_token] = bitbucket_client.connection.token @@ -70,7 +70,7 @@ def create render json: { errors: project_save_error(project) }, status: :unprocessable_entity end else - render json: { errors: _('This namespace has already been taken! Please choose another one.') }, status: :unprocessable_entity + render json: { errors: _('You are not allowed to import projects in this namespace.') }, status: :unprocessable_entity end end diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb index 047c273969cc0b..2778b97419a287 100644 --- a/app/controllers/import/gitea_controller.rb +++ b/app/controllers/import/gitea_controller.rb @@ -32,7 +32,7 @@ def status if params[:namespace_id].present? @namespace = Namespace.find_by_id(params[:namespace_id]) - render_404 unless current_user.can?(:create_projects, @namespace) + render_404 unless current_user.can?(:import_projects, @namespace) end end end diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index bd0c0976729187..719cd61e538bd1 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -65,7 +65,7 @@ def status if params[:namespace_id].present? @namespace = Namespace.find_by_id(params[:namespace_id]) - render_404 unless current_user.can?(:create_projects, @namespace) + render_404 unless current_user.can?(:import_projects, @namespace) end end end diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index 9b8c480e529459..d1b182a57d8273 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -8,7 +8,7 @@ class Import::GitlabProjectsController < Import::BaseController def new @namespace = Namespace.find(project_params[:namespace_id]) - return render_404 unless current_user.can?(:create_projects, @namespace) + return render_404 unless current_user.can?(:import_projects, @namespace) @path = project_params[:path] end diff --git a/app/controllers/import/manifest_controller.rb b/app/controllers/import/manifest_controller.rb index 461ba982969521..03884717e549d9 100644 --- a/app/controllers/import/manifest_controller.rb +++ b/app/controllers/import/manifest_controller.rb @@ -20,8 +20,8 @@ def status def upload group = Group.find(params[:group_id]) - unless can?(current_user, :create_projects, group) - @errors = ["You don't have enough permissions to create projects in the selected group"] + unless can?(current_user, :import_projects, group) + @errors = ["You don't have enough permissions to import projects in the selected group"] render :new && return end diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index 41daeddcf7f01e..208fbc40556074 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -56,7 +56,7 @@ def require_no_repo end def require_namespace_project_creation_permission - render_404 unless can?(current_user, :admin_project, @project) || can?(current_user, :create_projects, @project.namespace) + render_404 unless can?(current_user, :admin_project, @project) || can?(current_user, :import_projects, @project.namespace) end def redirect_if_progress diff --git a/app/finders/groups/accepting_project_imports_finder.rb b/app/finders/groups/accepting_project_imports_finder.rb new file mode 100644 index 00000000000000..55d72edf7bbe4d --- /dev/null +++ b/app/finders/groups/accepting_project_imports_finder.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Groups + class AcceptingProjectImportsFinder + def initialize(current_user) + @current_user = current_user + end + + def execute + ::Group.from_union( + [ + current_user.manageable_groups, + managable_groups_originating_from_group_shares + ] + ) + end + + private + + attr_reader :current_user + + def managable_groups_originating_from_group_shares + GroupGroupLink + .with_owner_or_maintainer_access + .groups_accessible_via( + current_user.owned_or_maintainers_groups + .select(:id) + ) + end + end +end diff --git a/app/finders/groups/user_groups_finder.rb b/app/finders/groups/user_groups_finder.rb index 83e012b3dbe7b8..536b81b230009b 100644 --- a/app/finders/groups/user_groups_finder.rb +++ b/app/finders/groups/user_groups_finder.rb @@ -39,6 +39,8 @@ def by_permission_scope Groups::AcceptingProjectCreationsFinder.new(target_user).execute # rubocop: disable CodeReuse/Finder elsif permission_scope_transfer_projects? Groups::AcceptingProjectTransfersFinder.new(target_user).execute # rubocop: disable CodeReuse/Finder + elsif permission_scope_import_projects? + Groups::AcceptingProjectImportsFinder.new(target_user).execute # rubocop: disable CodeReuse/Finder else target_user.groups end @@ -51,5 +53,9 @@ def permission_scope_create_projects? def permission_scope_transfer_projects? params[:permission_scope] == :transfer_projects end + + def permission_scope_import_projects? + params[:permission_scope] == :import_projects + end end end diff --git a/app/graphql/types/permission_types/group_enum.rb b/app/graphql/types/permission_types/group_enum.rb index f636d43790fb4f..6d51d94a70d43a 100644 --- a/app/graphql/types/permission_types/group_enum.rb +++ b/app/graphql/types/permission_types/group_enum.rb @@ -10,6 +10,9 @@ class GroupEnum < BaseEnum value 'TRANSFER_PROJECTS', value: :transfer_projects, description: 'Groups where the user can transfer projects to.' + value 'IMPORT_PROJECTS', + value: :import_projects, + description: 'Groups where the user can import projects to.' end end end diff --git a/app/policies/namespaces/user_namespace_policy.rb b/app/policies/namespaces/user_namespace_policy.rb index 1deeae8241feed..bfed61e72d31c2 100644 --- a/app/policies/namespaces/user_namespace_policy.rb +++ b/app/policies/namespaces/user_namespace_policy.rb @@ -11,6 +11,7 @@ class UserNamespacePolicy < ::NamespacePolicy rule { owner | admin }.policy do enable :owner_access enable :create_projects + enable :import_projects enable :admin_namespace enable :read_namespace enable :read_statistics @@ -20,9 +21,9 @@ class UserNamespacePolicy < ::NamespacePolicy enable :edit_billing end - rule { ~can_create_personal_project }.prevent :create_projects + rule { ~can_create_personal_project }.prevent :create_projects, :import_projects - rule { bot_user_namespace }.prevent :create_projects + rule { bot_user_namespace }.prevent :create_projects, :import_projects rule { (owner | admin) & can?(:create_projects) }.enable :transfer_projects end diff --git a/app/services/import/base_service.rb b/app/services/import/base_service.rb index 6b5adcbc39e849..64cf3cfa04ac20 100644 --- a/app/services/import/base_service.rb +++ b/app/services/import/base_service.rb @@ -9,7 +9,7 @@ def initialize(client, user, params) end def authorized? - can?(current_user, :create_projects, target_namespace) + can?(current_user, :import_projects, target_namespace) end private diff --git a/app/services/import/bitbucket_server_service.rb b/app/services/import/bitbucket_server_service.rb index f7f17f1e53e3e8..5d496dc7cc37e5 100644 --- a/app/services/import/bitbucket_server_service.rb +++ b/app/services/import/bitbucket_server_service.rb @@ -10,7 +10,7 @@ def execute(credentials) end unless authorized? - return log_and_return_error("You don't have permissions to create this project", :unauthorized) + return log_and_return_error("You don't have permissions to import this project", :unauthorized) end unless repo diff --git a/app/services/import/fogbugz_service.rb b/app/services/import/fogbugz_service.rb index d1003823456731..9a8def43312613 100644 --- a/app/services/import/fogbugz_service.rb +++ b/app/services/import/fogbugz_service.rb @@ -13,8 +13,8 @@ def execute(credentials) unless authorized? return log_and_return_error( - "You don't have permissions to create this project", - _("You don't have permissions to create this project"), + "You don't have permissions to import this project", + _("You don't have permissions to import this project"), :unauthorized ) end diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb index b30c344723d6c4..7e7f7ea9810bd4 100644 --- a/app/services/import/github_service.rb +++ b/app/services/import/github_service.rb @@ -103,7 +103,7 @@ def validate_context elsif target_namespace.nil? error(_('Namespace or group to import repository into does not exist.'), :unprocessable_entity) elsif !authorized? - error(_('This namespace has already been taken. Choose a different one.'), :unprocessable_entity) + error(_('You are not allowed to import projects in this namespace.'), :unprocessable_entity) elsif oversized? error(oversize_error_message, :unprocessable_entity) end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index cbea44d6aff15b..63b050faf9c7f3 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -58,6 +58,7 @@ def execute return @project if @project.errors.any? validate_create_permissions + validate_import_permissions return @project if @project.errors.any? @relations_block&.call(@project) @@ -98,6 +99,13 @@ def validate_create_permissions @project.errors.add(:namespace, "is not valid") end + def validate_import_permissions + return unless @project.import? + return if current_user.can?(:import_projects, parent_namespace) + + @project.errors.add(:user, 'is not allowed to import projects') + end + def after_create_actions log_info("#{current_user.name} created a new project \"#{@project.full_name}\"") diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index e64ed2c7b8f448..52ac8b58c9a8a7 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -17,7 +17,8 @@ root_path: root_path, parent_group_url: @project.parent && group_url(@project.parent), parent_group_name: @project.parent&.name, - projects_url: dashboard_projects_url } } + projects_url: dashboard_projects_url, + can_import_projects: params[:namespace_id].presence ? current_user.can?(:import_projects, @namespace).to_s : 'true' } } .row{ 'v-cloak': true } #blank-project-pane.tab-pane.active diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index cb184b1e7a878d..fde79f8947facf 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -23835,6 +23835,7 @@ User permission on groups. | Value | Description | | ----- | ----------- | | `CREATE_PROJECTS` | Groups where the user can create projects. | +| `IMPORT_PROJECTS` | Groups where the user can import projects to. | | `TRANSFER_PROJECTS` | Groups where the user can transfer projects to. | ### `GroupReleaseSort` diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 11b298c8264fdc..5f0d55713b6256 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -45530,9 +45530,6 @@ msgstr "" msgid "This namespace has already been taken! Please choose another one." msgstr "" -msgid "This namespace has already been taken. Choose a different one." -msgstr "" - msgid "This only applies to repository indexing operations." msgstr "" @@ -50577,6 +50574,9 @@ msgstr "" msgid "You are not allowed to download code from this project." msgstr "" +msgid "You are not allowed to import projects in this namespace." +msgstr "" + msgid "You are not allowed to log in using password" msgstr "" @@ -50892,7 +50892,7 @@ msgstr "" msgid "You don't have permission to view this epic" msgstr "" -msgid "You don't have permissions to create this project" +msgid "You don't have permissions to import this project" msgstr "" msgid "You don't have sufficient permission to perform this action." diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index 055c98ebdbce38..906cc5cb33618e 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Import::BitbucketController do +RSpec.describe Import::BitbucketController, feature_category: :importers do include ImportSpecHelper let(:user) { create(:user) } @@ -445,5 +445,16 @@ def assign_session_tokens ) end end + + context 'when user can not import projects' do + let!(:other_namespace) { create(:group, name: 'other_namespace').tap { |other_namespace| other_namespace.add_developer(user) } } + + it 'returns 422 response' do + post :create, params: { target_namespace: other_namespace.name }, format: :json + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(response.parsed_body['errors']).to eq('You are not allowed to import projects in this namespace.') + end + end end end diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb index ac56d3af54f507..b2a5642325328d 100644 --- a/spec/controllers/import/bitbucket_server_controller_spec.rb +++ b/spec/controllers/import/bitbucket_server_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Import::BitbucketServerController do +RSpec.describe Import::BitbucketServerController, feature_category: :importers do let(:user) { create(:user) } let(:project_key) { 'test-project' } let(:repo_slug) { 'some-repo' } diff --git a/spec/controllers/import/fogbugz_controller_spec.rb b/spec/controllers/import/fogbugz_controller_spec.rb index e2d59fc213a22c..40a5c59fa2d840 100644 --- a/spec/controllers/import/fogbugz_controller_spec.rb +++ b/spec/controllers/import/fogbugz_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Import::FogbugzController do +RSpec.describe Import::FogbugzController, feature_category: :importers do include ImportSpecHelper let(:user) { create(:user) } diff --git a/spec/controllers/import/gitea_controller_spec.rb b/spec/controllers/import/gitea_controller_spec.rb index 568712d29cbff3..7466ffb239388b 100644 --- a/spec/controllers/import/gitea_controller_spec.rb +++ b/spec/controllers/import/gitea_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Import::GiteaController do +RSpec.describe Import::GiteaController, feature_category: :importers do include ImportSpecHelper let(:provider) { :gitea } diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb index 7b3978297fb2d7..2c09f8c010e09f 100644 --- a/spec/controllers/import/gitlab_controller_spec.rb +++ b/spec/controllers/import/gitlab_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Import::GitlabController do +RSpec.describe Import::GitlabController, feature_category: :importers do include ImportSpecHelper let(:user) { create(:user) } diff --git a/spec/controllers/import/manifest_controller_spec.rb b/spec/controllers/import/manifest_controller_spec.rb index 6f805b44e89471..23d5d37ed887a7 100644 --- a/spec/controllers/import/manifest_controller_spec.rb +++ b/spec/controllers/import/manifest_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Import::ManifestController, :clean_gitlab_redis_shared_state do +RSpec.describe Import::ManifestController, :clean_gitlab_redis_shared_state, feature_category: :importers do include ImportSpecHelper let_it_be(:user) { create(:user) } @@ -45,7 +45,7 @@ end end - context 'when the user cannot create projects in the group' do + context 'when the user cannot import projects in the group' do it 'displays an error' do sign_in(create(:user)) diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb index b4704d56cd9262..4502f3d7bd950c 100644 --- a/spec/controllers/projects/imports_controller_spec.rb +++ b/spec/controllers/projects/imports_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::ImportsController do +RSpec.describe Projects::ImportsController, feature_category: :importers do let(:user) { create(:user) } let(:project) { create(:project) } @@ -149,17 +149,7 @@ import_state.update!(status: :started) end - context 'when group allows developers to create projects' do - let(:group) { create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) } - - it 'renders template' do - get :show, params: { namespace_id: project.namespace.to_param, project_id: project } - - expect(response).to render_template :show - end - end - - context 'when group prohibits developers to create projects' do + context 'when group prohibits developers to import projects' do let(:group) { create(:group, project_creation_level: Gitlab::Access::MAINTAINER_PROJECT_ACCESS) } it 'returns 404 response' do diff --git a/spec/finders/groups/accepting_project_imports_finder_spec.rb b/spec/finders/groups/accepting_project_imports_finder_spec.rb new file mode 100644 index 00000000000000..4e06c2cbc67cc5 --- /dev/null +++ b/spec/finders/groups/accepting_project_imports_finder_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Groups::AcceptingProjectImportsFinder, feature_category: :importers do + let_it_be(:user) { create(:user) } + let_it_be(:group_where_direct_owner) { create(:group) } + let_it_be(:subgroup_of_group_where_direct_owner) { create(:group, parent: group_where_direct_owner) } + let_it_be(:group_where_direct_maintainer) { create(:group) } + let_it_be(:group_where_direct_maintainer_but_cant_create_projects) do + create(:group, project_creation_level: Gitlab::Access::NO_ONE_PROJECT_ACCESS) + end + + let_it_be(:group_where_direct_developer_but_developers_cannot_create_projects) { create(:group) } + let_it_be(:group_where_direct_developer) do + create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) + end + + let_it_be(:shared_with_group_where_direct_owner_as_owner) { create(:group) } + + let_it_be(:shared_with_group_where_direct_owner_as_developer) do + create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) + end + + let_it_be(:shared_with_group_where_direct_owner_as_developer_but_developers_cannot_create_projects) do + create(:group) + end + + let_it_be(:shared_with_group_where_direct_developer_as_maintainer) do + create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) + end + + let_it_be(:shared_with_group_where_direct_owner_as_guest) { create(:group) } + let_it_be(:shared_with_group_where_direct_owner_as_maintainer) { create(:group) } + let_it_be(:shared_with_group_where_direct_developer_as_owner) do + create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) + end + + let_it_be(:subgroup_of_shared_with_group_where_direct_owner_as_maintainer) do + create(:group, parent: shared_with_group_where_direct_owner_as_maintainer) + end + + before do + group_where_direct_owner.add_owner(user) + group_where_direct_maintainer.add_maintainer(user) + group_where_direct_developer_but_developers_cannot_create_projects.add_developer(user) + group_where_direct_developer.add_developer(user) + + create(:group_group_link, :owner, + shared_with_group: group_where_direct_owner, + shared_group: shared_with_group_where_direct_owner_as_owner + ) + + create(:group_group_link, :developer, + shared_with_group: group_where_direct_owner, + shared_group: shared_with_group_where_direct_owner_as_developer_but_developers_cannot_create_projects + ) + + create(:group_group_link, :maintainer, + shared_with_group: group_where_direct_developer, + shared_group: shared_with_group_where_direct_developer_as_maintainer + ) + + create(:group_group_link, :developer, + shared_with_group: group_where_direct_owner, + shared_group: shared_with_group_where_direct_owner_as_developer + ) + + create(:group_group_link, :guest, + shared_with_group: group_where_direct_owner, + shared_group: shared_with_group_where_direct_owner_as_guest + ) + + create(:group_group_link, :maintainer, + shared_with_group: group_where_direct_owner, + shared_group: shared_with_group_where_direct_owner_as_maintainer + ) + + create(:group_group_link, :owner, + shared_with_group: group_where_direct_developer_but_developers_cannot_create_projects, + shared_group: shared_with_group_where_direct_developer_as_owner + ) + end + + describe '#execute' do + subject(:result) { described_class.new(user).execute } + + it 'only returns groups where the user has access to import projects' do + expect(result).to match_array([ + group_where_direct_owner, + subgroup_of_group_where_direct_owner, + group_where_direct_maintainer, + # groups arising from group shares + shared_with_group_where_direct_owner_as_owner, + shared_with_group_where_direct_owner_as_maintainer, + subgroup_of_shared_with_group_where_direct_owner_as_maintainer + ]) + + expect(result).not_to include(group_where_direct_developer) + expect(result).not_to include(shared_with_group_where_direct_developer_as_owner) + expect(result).not_to include(shared_with_group_where_direct_developer_as_maintainer) + expect(result).not_to include(shared_with_group_where_direct_owner_as_developer) + end + end +end diff --git a/spec/finders/groups/user_groups_finder_spec.rb b/spec/finders/groups/user_groups_finder_spec.rb index 999079468e59a4..f6df396037c40a 100644 --- a/spec/finders/groups/user_groups_finder_spec.rb +++ b/spec/finders/groups/user_groups_finder_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Groups::UserGroupsFinder do +RSpec.describe Groups::UserGroupsFinder, feature_category: :subgroups do describe '#execute' do let_it_be(:user) { create(:user) } let_it_be(:root_group) { create(:group, name: 'Root group', path: 'root-group') } @@ -98,6 +98,24 @@ end end + context 'when permission is :import_projects' do + let(:arguments) { { permission_scope: :import_projects } } + + specify do + is_expected.to contain_exactly( + public_maintainer_group, + public_owner_group, + private_maintainer_group + ) + end + + it_behaves_like 'user group finder searching by name or path' do + let(:keyword_search_expected_groups) do + [public_maintainer_group] + end + end + end + context 'when permission is :transfer_projects' do let(:arguments) { { permission_scope: :transfer_projects } } diff --git a/spec/frontend/import_entities/components/group_dropdown_spec.js b/spec/frontend/import_entities/components/group_dropdown_spec.js index b44bc33de6f2f6..14f39a35387726 100644 --- a/spec/frontend/import_entities/components/group_dropdown_spec.js +++ b/spec/frontend/import_entities/components/group_dropdown_spec.js @@ -6,7 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import GroupDropdown from '~/import_entities/components/group_dropdown.vue'; import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; -import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql'; +import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql'; Vue.use(VueApollo); @@ -49,7 +49,7 @@ describe('Import entities group dropdown component', () => { const createComponent = (propsData) => { const apolloProvider = createMockApollo([ - [searchNamespacesWhereUserCanCreateProjectsQuery, () => SEARCH_NAMESPACES_MOCK], + [searchNamespacesWhereUserCanImportProjectsQuery, () => SEARCH_NAMESPACES_MOCK], ]); namespacesTracker = jest.fn(); diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js index b1aa94cf4189dd..dae5671777ca56 100644 --- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js @@ -15,7 +15,7 @@ import ImportTable from '~/import_entities/import_groups/components/import_table import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql'; import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue'; import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; -import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql'; +import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql'; import { AVAILABLE_NAMESPACES, @@ -74,7 +74,7 @@ describe('import table', () => { apolloProvider = createMockApollo( [ [ - searchNamespacesWhereUserCanCreateProjectsQuery, + searchNamespacesWhereUserCanImportProjectsQuery, () => Promise.resolve(availableNamespacesFixture), ], ], diff --git a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js index a524d9ebdb01fd..a957e85723faaa 100644 --- a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js @@ -8,7 +8,7 @@ import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue import { STATUSES } from '~/import_entities/constants'; import ImportTargetCell from '~/import_entities/import_groups/components/import_target_cell.vue'; import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; -import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql'; +import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql'; import { generateFakeEntry, @@ -42,7 +42,7 @@ describe('import target cell', () => { const createComponent = (props) => { apolloProvider = createMockApollo([ [ - searchNamespacesWhereUserCanCreateProjectsQuery, + searchNamespacesWhereUserCanImportProjectsQuery, () => Promise.resolve(availableNamespacesFixture), ], ]); diff --git a/spec/frontend/projects/new/components/app_spec.js b/spec/frontend/projects/new/components/app_spec.js index 16576523c6648f..60d8385eb91bfc 100644 --- a/spec/frontend/projects/new/components/app_spec.js +++ b/spec/frontend/projects/new/components/app_spec.js @@ -41,6 +41,22 @@ describe('Experimental new project creation app', () => { ).toBe(isCiCdAvailable); }); + it.each` + canImportProjects | outcome + ${false} | ${'do not show Import panel'} + ${true} | ${'show Import panel'} + `('$outcome when canImportProjects is $canImportProjects', ({ canImportProjects }) => { + createComponent({ + canImportProjects, + }); + + expect( + findNewNamespacePage() + .props() + .panels.some((p) => p.name === 'import_project'), + ).toBe(canImportProjects); + }); + it('creates correct breadcrumbs for top-level projects', () => { createComponent(); diff --git a/spec/policies/namespaces/user_namespace_policy_spec.rb b/spec/policies/namespaces/user_namespace_policy_spec.rb index bb821490e301f3..3488f33f15c68a 100644 --- a/spec/policies/namespaces/user_namespace_policy_spec.rb +++ b/spec/policies/namespaces/user_namespace_policy_spec.rb @@ -2,13 +2,13 @@ require 'spec_helper' -RSpec.describe Namespaces::UserNamespacePolicy do +RSpec.describe Namespaces::UserNamespacePolicy, feature_category: :subgroups do let_it_be(:user) { create(:user) } let_it_be(:owner) { create(:user) } let_it_be(:admin) { create(:admin) } let_it_be(:namespace) { create(:user_namespace, owner: owner) } - let(:owner_permissions) { [:owner_access, :create_projects, :admin_namespace, :read_namespace, :read_statistics, :transfer_projects, :admin_package, :read_billing, :edit_billing] } + let(:owner_permissions) { [:owner_access, :create_projects, :admin_namespace, :read_namespace, :read_statistics, :transfer_projects, :admin_package, :read_billing, :edit_billing, :import_projects] } subject { described_class.new(current_user, namespace) } @@ -34,6 +34,7 @@ it { is_expected.to be_disallowed(:create_projects) } it { is_expected.to be_disallowed(:transfer_projects) } + it { is_expected.to be_disallowed(:import_projects) } end context 'bot user' do @@ -41,6 +42,7 @@ it { is_expected.to be_disallowed(:create_projects) } it { is_expected.to be_disallowed(:transfer_projects) } + it { is_expected.to be_disallowed(:import_projects) } end end @@ -103,4 +105,26 @@ it { is_expected.to be_disallowed(:create_projects) } end end + + describe 'import projects' do + context 'when user can import projects' do + let(:current_user) { owner } + + before do + allow(current_user).to receive(:can_import_project?).and_return(true) + end + + it { is_expected.to be_allowed(:import_projects) } + end + + context 'when user cannot create projects' do + let(:current_user) { user } + + before do + allow(current_user).to receive(:can_import_project?).and_return(false) + end + + it { is_expected.to be_disallowed(:import_projects) } + end + end end diff --git a/spec/requests/import/gitlab_projects_controller_spec.rb b/spec/requests/import/gitlab_projects_controller_spec.rb index b2c2d306e53e2a..fe3ea9e9c9e467 100644 --- a/spec/requests/import/gitlab_projects_controller_spec.rb +++ b/spec/requests/import/gitlab_projects_controller_spec.rb @@ -90,4 +90,16 @@ def stub_import(namespace) subject { post authorize_import_gitlab_project_path, headers: workhorse_headers } end end + + describe 'GET new' do + context 'when the user is not allowed to import projects' do + let!(:group) { create(:group).tap { |group| group.add_developer(user) } } + + it 'returns 404' do + get new_import_gitlab_project_path, params: { namespace_id: group.id } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end end diff --git a/spec/services/import/bitbucket_server_service_spec.rb b/spec/services/import/bitbucket_server_service_spec.rb index aea6c45b3a8a67..ca554fb01c354e 100644 --- a/spec/services/import/bitbucket_server_service_spec.rb +++ b/spec/services/import/bitbucket_server_service_spec.rb @@ -93,7 +93,7 @@ result = subject.execute(credentials) expect(result).to include( - message: "You don't have permissions to create this project", + message: "You don't have permissions to import this project", status: :error, http_status: :unauthorized ) diff --git a/spec/services/import/fogbugz_service_spec.rb b/spec/services/import/fogbugz_service_spec.rb index 6953213add73e8..ad02dc31da1c73 100644 --- a/spec/services/import/fogbugz_service_spec.rb +++ b/spec/services/import/fogbugz_service_spec.rb @@ -61,7 +61,7 @@ result = subject.execute(credentials) expect(result).to include( - message: "You don't have permissions to create this project", + message: "You don't have permissions to import this project", status: :error, http_status: :unauthorized ) diff --git a/spec/services/import/github_service_spec.rb b/spec/services/import/github_service_spec.rb index 5d762568a62fd3..a8928fb5c09624 100644 --- a/spec/services/import/github_service_spec.rb +++ b/spec/services/import/github_service_spec.rb @@ -291,7 +291,7 @@ def taken_namespace_error { status: :error, http_status: :unprocessable_entity, - message: 'This namespace has already been taken. Choose a different one.' + message: 'You are not allowed to import projects in this namespace.' } end end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 495e2277d43d44..35b715d82ee27c 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -254,6 +254,23 @@ end it_behaves_like 'has sync-ed traversal_ids' + + context 'when project is an import' do + context 'when user is not allowed to import projects' do + let(:group) do + create(:group).tap do |group| + group.add_developer(user) + end + end + + it 'does not create the project' do + project = create_project(user, opts.merge!(namespace_id: group.id, import_type: 'gitlab_project')) + + expect(project).not_to be_persisted + expect(project.errors.messages[:user].first).to eq('is not allowed to import projects') + end + end + end end context 'group sharing', :sidekiq_inline do diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb index de38d1ff9f863e..af1843bae2857e 100644 --- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb @@ -138,6 +138,19 @@ def assign_session_token(provider) .not_to exceed_all_query_limit(control_count) end + context 'when user is not allowed to import projects' do + let(:user) { create(:user) } + let!(:group) { create(:group).tap { |group| group.add_developer(user) } } + + it 'returns 404' do + expect(stub_client(repos: [], orgs: [])).to receive(:repos) + + get :status, params: { namespace_id: group.id }, format: :html + + expect(response).to have_gitlab_http_status(:not_found) + end + end + context 'when filtering' do let(:repo_2) { repo_fake.new(login: 'emacs', full_name: 'asd/emacs', name: 'emacs', owner: { login: 'owner' }) } let(:project) { create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') } diff --git a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb index 44baadaaade3cb..e94f063399de1b 100644 --- a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb +++ b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb @@ -19,4 +19,26 @@ expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id) end + + context 'when format is html' do + context 'when namespace_id is present' do + let!(:developer_group) { create(:group).tap { |g| g.add_developer(user) } } + + context 'when user cannot import projects' do + it 'returns 404' do + get :status, params: { namespace_id: developer_group.id }, format: :html + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when user can import projects' do + it 'returns 200' do + get :status, params: { namespace_id: group.id }, format: :html + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + end end -- GitLab