summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-06-08 15:08:20 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-06-08 15:08:20 +0000
commit5693fb6ba7d21ba7b79775543a3f195eb989664b (patch)
treea9038b20872f0f3bd35de034bfb40e533435bfa5
parentf149549c3432ffb179f6904e4ba0ea64027202d0 (diff)
downloadgitlab-ce-5693fb6ba7d21ba7b79775543a3f195eb989664b.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock6
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js2
-rw-r--r--app/assets/stylesheets/framework/filters.scss14
-rw-r--r--app/graphql/mutations/jira_import/import_users.rb44
-rw-r--r--app/graphql/types/jira_user_type.rb19
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/services/jira_import/users_importer.rb2
-rw-r--r--app/services/service_response.rb6
-rw-r--r--changelogs/unreleased/195692-too-much-x-axis-padding-on-the-environments-dashboard-content.yml5
-rw-r--r--changelogs/unreleased/216145-jira-users-import-endpoint.yml5
-rw-r--r--changelogs/unreleased/219658-add-route-to-ghost-lost-and-found-group.yml5
-rw-r--r--changelogs/unreleased/220616-group-code-icons-in-toolbar.yml5
-rw-r--r--changelogs/unreleased/update-validates-hostname-gem.yml5
-rw-r--r--db/post_migrate/20200602143020_update_routes_for_lost_and_found_group_and_orphaned_projects.rb167
-rw-r--r--db/structure.sql1
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql63
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json224
-rw-r--r--doc/api/graphql/reference/index.md19
-rw-r--r--doc/development/i18n/proofreader.md2
-rw-r--r--doc/user/group/index.md11
-rw-r--r--locale/gitlab.pot18
-rw-r--r--spec/migrations/cleanup_projects_with_missing_namespace_spec.rb32
-rw-r--r--spec/migrations/update_routes_for_lost_and_found_group_and_orphaned_projects_spec.rb96
-rw-r--r--spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb104
-rw-r--r--spec/services/jira_import/users_importer_spec.rb4
-rw-r--r--spec/services/service_response_spec.rb10
28 files changed, 847 insertions, 32 deletions
diff --git a/Gemfile b/Gemfile
index 8137b66c283..c2d2b12e56d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -63,7 +63,7 @@ gem 'attr_encrypted', '~> 3.1.0'
gem 'u2f', '~> 0.2.1'
# GitLab Pages
-gem 'validates_hostname', '~> 1.0.6'
+gem 'validates_hostname', '~> 1.0.10'
gem 'rubyzip', '~> 2.0.0', require: 'zip'
# GitLab Pages letsencrypt support
gem 'acme-client', '~> 2.0.5'
diff --git a/Gemfile.lock b/Gemfile.lock
index 8af96ada4d6..607551164ab 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -530,7 +530,7 @@ GEM
mime-types (~> 3.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
- i18n (1.8.2)
+ i18n (1.8.3)
concurrent-ruby (~> 1.0)
i18n_data (0.8.0)
icalendar (2.4.1)
@@ -1115,7 +1115,7 @@ GEM
validate_url (1.0.8)
activemodel (>= 3.0.0)
public_suffix
- validates_hostname (1.0.6)
+ validates_hostname (1.0.10)
activerecord (>= 3.0)
activesupport (>= 3.0)
version_sorter (2.2.4)
@@ -1401,7 +1401,7 @@ DEPENDENCIES
unicorn-worker-killer (~> 0.4.4)
unleash (~> 0.1.5)
valid_email (~> 0.1)
- validates_hostname (~> 1.0.6)
+ validates_hostname (~> 1.0.10)
version_sorter (~> 2.2.4)
vmstat (~> 2.3.0)
webmock (~> 3.5.1)
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index 14252c50d03..a858ffdbed5 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -221,7 +221,7 @@ export default {
</script>
<template>
- <div class="vue-filtered-search-bar-container d-flex">
+ <div class="vue-filtered-search-bar-container d-md-flex">
<gl-filtered-search
v-model="filterValue"
:placeholder="searchInputPlaceholder"
@@ -230,8 +230,8 @@ export default {
class="flex-grow-1"
@submit="handleFilterSubmit"
/>
- <gl-button-group class="ml-2">
- <gl-dropdown :text="selectedSortOption.title" :right="true">
+ <gl-button-group class="sort-dropdown-container d-flex">
+ <gl-dropdown :text="selectedSortOption.title" :right="true" class="w-100">
<gl-dropdown-item
v-for="sortBy in sortOptions"
:key="sortBy.id"
@@ -245,6 +245,7 @@ export default {
v-gl-tooltip
:title="sortDirectionTooltip"
:icon="sortDirectionIcon"
+ class="flex-shrink-1"
@click="handleSortDirectionClick"
/>
</gl-button-group>
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js
index fb690b5e378..1dbbda10156 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js
@@ -14,7 +14,6 @@ const TOOLBAR_ITEM_CONFIGS = [
{ isDivider: true },
{ icon: 'quote', command: 'Blockquote', tooltip: __('Insert a quote') },
{ icon: 'link', event: 'openPopupAddLink', tooltip: __('Add a link') },
- { icon: 'doc-code', command: 'CodeBlock', tooltip: __('Insert a code block') },
{ isDivider: true },
{ icon: 'list-bulleted', command: 'UL', tooltip: __('Add a bullet list') },
{ icon: 'list-numbered', command: 'OL', tooltip: __('Add a numbered list') },
@@ -27,6 +26,7 @@ const TOOLBAR_ITEM_CONFIGS = [
{ icon: 'doc-image', event: CUSTOM_EVENTS.openAddImageModal, tooltip: __('Insert an image') },
{ isDivider: true },
{ icon: 'code', command: 'Code', tooltip: __('Insert inline code') },
+ { icon: 'doc-code', command: 'CodeBlock', tooltip: __('Insert a code block') },
];
export const EDITOR_OPTIONS = {
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 5f6a26d0a14..7eb2b3acba4 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -449,3 +449,17 @@
font-size: 13px;
}
}
+
+.vue-filtered-search-bar-container {
+ @include media-breakpoint-up(md) {
+ .sort-dropdown-container {
+ margin-left: 10px;
+ }
+ }
+
+ @include media-breakpoint-down(sm) {
+ .sort-dropdown-container {
+ margin-top: 10px;
+ }
+ }
+}
diff --git a/app/graphql/mutations/jira_import/import_users.rb b/app/graphql/mutations/jira_import/import_users.rb
new file mode 100644
index 00000000000..c7225e1a99c
--- /dev/null
+++ b/app/graphql/mutations/jira_import/import_users.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Mutations
+ module JiraImport
+ class ImportUsers < BaseMutation
+ include ResolvesProject
+
+ graphql_name 'JiraImportUsers'
+
+ field :jira_users,
+ [Types::JiraUserType],
+ null: true,
+ description: 'Users returned from Jira, matched by email and name if possible.'
+
+ argument :project_path, GraphQL::ID_TYPE,
+ required: true,
+ description: 'The project to import the Jira users into'
+ argument :start_at, GraphQL::INT_TYPE,
+ required: false,
+ description: 'The index of the record the import should started at, default 0 (50 records returned)'
+
+ def resolve(project_path:, start_at:)
+ project = authorized_find!(full_path: project_path)
+
+ service_response = ::JiraImport::UsersImporter.new(context[:current_user], project, start_at).execute
+
+ {
+ jira_users: service_response.payload,
+ errors: service_response.errors
+ }
+ end
+
+ private
+
+ def find_object(full_path:)
+ resolve_project(full_path: full_path)
+ end
+
+ def authorized_resource?(project)
+ Ability.allowed?(context[:current_user], :admin_project, project)
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/jira_user_type.rb b/app/graphql/types/jira_user_type.rb
new file mode 100644
index 00000000000..8aa21ce669b
--- /dev/null
+++ b/app/graphql/types/jira_user_type.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Types
+ # rubocop: disable Graphql/AuthorizeTypes
+ # Authorization is at project level for owners or admins on mutation level
+ class JiraUserType < BaseObject
+ graphql_name 'JiraUser'
+
+ field :jira_account_id, GraphQL::STRING_TYPE, null: false,
+ description: 'Account id of the Jira user'
+ field :jira_display_name, GraphQL::STRING_TYPE, null: false,
+ description: 'Display name of the Jira user'
+ field :jira_email, GraphQL::STRING_TYPE, null: true,
+ description: 'Email of the Jira user, returned only for users with public emails'
+ field :gitlab_id, GraphQL::INT_TYPE, null: true,
+ description: 'Id of the matched GitLab user'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index a1dee351475..8d4368cc30a 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -48,6 +48,7 @@ module Types
mount_mutation Mutations::Snippets::Create
mount_mutation Mutations::Snippets::MarkAsSpam
mount_mutation Mutations::JiraImport::Start
+ mount_mutation Mutations::JiraImport::ImportUsers
mount_mutation Mutations::DesignManagement::Upload, calls_gitaly: true
mount_mutation Mutations::DesignManagement::Delete, calls_gitaly: true
mount_mutation Mutations::ContainerExpirationPolicies::Update
diff --git a/app/services/jira_import/users_importer.rb b/app/services/jira_import/users_importer.rb
index b7644ca54eb..579d3675073 100644
--- a/app/services/jira_import/users_importer.rb
+++ b/app/services/jira_import/users_importer.rb
@@ -22,6 +22,8 @@ module JiraImport
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => error
Gitlab::ErrorTracking.track_exception(error, project_id: project.id, request: url)
ServiceResponse.error(message: "There was an error when communicating to Jira: #{error.message}")
+ rescue Projects::ImportService::Error => error
+ ServiceResponse.error(message: error.message)
end
private
diff --git a/app/services/service_response.rb b/app/services/service_response.rb
index 08b7e9d0831..74c0be22d46 100644
--- a/app/services/service_response.rb
+++ b/app/services/service_response.rb
@@ -26,6 +26,12 @@ class ServiceResponse
status == :error
end
+ def errors
+ return [] unless error?
+
+ Array.wrap(message)
+ end
+
private
attr_writer :status, :message, :http_status, :payload
diff --git a/changelogs/unreleased/195692-too-much-x-axis-padding-on-the-environments-dashboard-content.yml b/changelogs/unreleased/195692-too-much-x-axis-padding-on-the-environments-dashboard-content.yml
new file mode 100644
index 00000000000..0ee3d01d030
--- /dev/null
+++ b/changelogs/unreleased/195692-too-much-x-axis-padding-on-the-environments-dashboard-content.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve incorrect x-axis padding on the Environments Dashboard
+merge_request: 32533
+author:
+type: fixed
diff --git a/changelogs/unreleased/216145-jira-users-import-endpoint.yml b/changelogs/unreleased/216145-jira-users-import-endpoint.yml
new file mode 100644
index 00000000000..54c3a130b07
--- /dev/null
+++ b/changelogs/unreleased/216145-jira-users-import-endpoint.yml
@@ -0,0 +1,5 @@
+---
+title: Create graphQL endpoint for Jira users import
+merge_request: 33501
+author:
+type: added
diff --git a/changelogs/unreleased/219658-add-route-to-ghost-lost-and-found-group.yml b/changelogs/unreleased/219658-add-route-to-ghost-lost-and-found-group.yml
new file mode 100644
index 00000000000..8e9bdf63866
--- /dev/null
+++ b/changelogs/unreleased/219658-add-route-to-ghost-lost-and-found-group.yml
@@ -0,0 +1,5 @@
+---
+title: Add route for the lost-and-found group and update the route of orphaned projects
+merge_request: 33653
+author:
+type: fixed
diff --git a/changelogs/unreleased/220616-group-code-icons-in-toolbar.yml b/changelogs/unreleased/220616-group-code-icons-in-toolbar.yml
new file mode 100644
index 00000000000..15e671bad56
--- /dev/null
+++ b/changelogs/unreleased/220616-group-code-icons-in-toolbar.yml
@@ -0,0 +1,5 @@
+---
+title: Update Static Site Editor toolbar to group inline-code and code-block buttons together
+merge_request: 34006
+author:
+type: changed
diff --git a/changelogs/unreleased/update-validates-hostname-gem.yml b/changelogs/unreleased/update-validates-hostname-gem.yml
new file mode 100644
index 00000000000..4665ebe2724
--- /dev/null
+++ b/changelogs/unreleased/update-validates-hostname-gem.yml
@@ -0,0 +1,5 @@
+---
+title: Update validates_hostname gem with support for more TLDs
+merge_request: 34010
+author:
+type: fixed
diff --git a/db/post_migrate/20200602143020_update_routes_for_lost_and_found_group_and_orphaned_projects.rb b/db/post_migrate/20200602143020_update_routes_for_lost_and_found_group_and_orphaned_projects.rb
new file mode 100644
index 00000000000..b351940092a
--- /dev/null
+++ b/db/post_migrate/20200602143020_update_routes_for_lost_and_found_group_and_orphaned_projects.rb
@@ -0,0 +1,167 @@
+# frozen_string_literal: true
+
+# This migration adds or updates the routes for all the entities affected by
+# post-migration '20200511083541_cleanup_projects_with_missing_namespace'
+# - A route is added for the 'lost-and-found' group
+# - A route is added for the Ghost user (if not already defined)
+# - The routes for all the orphaned projects that were moved under the 'lost-and-found'
+# group are updated to reflect the new path
+class UpdateRoutesForLostAndFoundGroupAndOrphanedProjects < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ class User < ActiveRecord::Base
+ self.table_name = 'users'
+
+ LOST_AND_FOUND_GROUP = 'lost-and-found'
+ USER_TYPE_GHOST = 5
+ ACCESS_LEVEL_OWNER = 50
+
+ has_one :namespace, -> { where(type: nil) },
+ foreign_key: :owner_id, inverse_of: :owner, autosave: true,
+ class_name: 'UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Namespace'
+
+ def lost_and_found_group
+ # Find the 'lost-and-found' group
+ # There should only be one Group owned by the Ghost user starting with 'lost-and-found'
+ Group
+ .joins('INNER JOIN members ON namespaces.id = members.source_id')
+ .where('namespaces.type = ?', 'Group')
+ .where('members.type = ?', 'GroupMember')
+ .where('members.source_type = ?', 'Namespace')
+ .where('members.user_id = ?', self.id)
+ .where('members.access_level = ?', ACCESS_LEVEL_OWNER)
+ .find_by(Group.arel_table[:name].matches("#{LOST_AND_FOUND_GROUP}%"))
+ end
+
+ class << self
+ # Return the ghost user
+ def ghost
+ User.find_by(user_type: USER_TYPE_GHOST)
+ end
+ end
+ end
+
+ # Temporary Concern to not repeat the same methods twice
+ module HasPath
+ extend ActiveSupport::Concern
+
+ def full_path
+ if parent && path
+ parent.full_path + '/' + path
+ else
+ path
+ end
+ end
+
+ def full_name
+ if parent && name
+ parent.full_name + ' / ' + name
+ else
+ name
+ end
+ end
+ end
+
+ class Namespace < ActiveRecord::Base
+ include HasPath
+
+ self.table_name = 'namespaces'
+
+ belongs_to :owner, class_name: 'UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::User'
+ belongs_to :parent, class_name: "UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Namespace"
+ has_many :children, foreign_key: :parent_id,
+ class_name: "UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Namespace"
+ has_many :projects, class_name: "UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Project"
+
+ def ensure_route!
+ unless Route.for_source('Namespace', self.id)
+ Route.create!(
+ source_id: self.id,
+ source_type: 'Namespace',
+ path: self.full_path,
+ name: self.full_name
+ )
+ end
+ end
+ end
+
+ class Group < Namespace
+ # Disable STI to allow us to manually set "type = 'Group'"
+ # Otherwise rails forces "type = CleanupProjectsWithMissingNamespace::Group"
+ self.inheritance_column = :_type_disabled
+ end
+
+ class Route < ActiveRecord::Base
+ self.table_name = 'routes'
+
+ def self.for_source(source_type, source_id)
+ Route.find_by(source_type: source_type, source_id: source_id)
+ end
+ end
+
+ class Project < ActiveRecord::Base
+ include HasPath
+
+ self.table_name = 'projects'
+
+ belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id',
+ class_name: "UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Group"
+ belongs_to :namespace,
+ class_name: "UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Namespace"
+
+ alias_method :parent, :namespace
+ alias_attribute :parent_id, :namespace_id
+
+ def ensure_route!
+ Route.find_or_initialize_by(source_type: 'Project', source_id: self.id).tap do |record|
+ record.path = self.full_path
+ record.name = self.full_name
+ record.save!
+ end
+ end
+ end
+
+ def up
+ # Reset the column information of all the models that update the database
+ # to ensure the Active Record's knowledge of the table structure is current
+ Namespace.reset_column_information
+ Route.reset_column_information
+
+ # Find the ghost user, its namespace and the "lost and found" group
+ ghost_user = User.ghost
+ return unless ghost_user # No reason to continue if there is no Ghost user
+
+ ghost_namespace = ghost_user.namespace
+ lost_and_found_group = ghost_user.lost_and_found_group
+
+ # No reason to continue if there is no 'lost-and-found' group
+ # 1. No orphaned projects were found in this instance, or
+ # 2. The 'lost-and-found' group and the orphaned projects have been already deleted
+ return unless lost_and_found_group
+
+ # Update the 'lost-and-found' group description to be more self-explanatory
+ lost_and_found_group.description =
+ 'Group for storing projects that were not properly deleted. '\
+ 'It should be considered as a system level Group with non-working '\
+ 'projects inside it. The contents may be deleted with a future update. '\
+ 'More info: gitlab.com/gitlab-org/gitlab/-/issues/198603'
+ lost_and_found_group.save!
+
+ # Update the routes for the Ghost user, the "lost and found" group
+ # and all the orphaned projects
+ ghost_namespace.ensure_route!
+ lost_and_found_group.ensure_route!
+
+ # The following does a fast index scan by namespace_id
+ # No reason to process in batches:
+ # - 66 projects in GitLab.com, less than 1ms execution time to fetch them
+ # with a constant update time for each
+ lost_and_found_group.projects.each do |project|
+ project.ensure_route!
+ end
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 85f8e278ccf..64bcdb577ac 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -13797,6 +13797,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200528054112
20200528123703
20200528125905
+20200602143020
20200603073101
\.
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 9a70d71a202..14aeb7f8268 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -5933,6 +5933,46 @@ type JiraImportStartPayload {
jiraImport: JiraImport
}
+"""
+Autogenerated input type of JiraImportUsers
+"""
+input JiraImportUsersInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The project to import the Jira users into
+ """
+ projectPath: ID!
+
+ """
+ The index of the record the import should started at, default 0 (50 records returned)
+ """
+ startAt: Int
+}
+
+"""
+Autogenerated return type of JiraImportUsers
+"""
+type JiraImportUsersPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Errors encountered during execution of the mutation.
+ """
+ errors: [String!]!
+
+ """
+ Users returned from Jira, matched by email and name if possible.
+ """
+ jiraUsers: [JiraUser!]
+}
+
type JiraProject {
"""
Key of the Jira project
@@ -6027,6 +6067,28 @@ type JiraService implements Service {
type: String
}
+type JiraUser {
+ """
+ Id of the matched GitLab user
+ """
+ gitlabId: Int
+
+ """
+ Account id of the Jira user
+ """
+ jiraAccountId: String!
+
+ """
+ Display name of the Jira user
+ """
+ jiraDisplayName: String!
+
+ """
+ Email of the Jira user, returned only for users with public emails
+ """
+ jiraEmail: String
+}
+
type Label {
"""
Background color of the label
@@ -7271,6 +7333,7 @@ type Mutation {
issueSetIteration(input: IssueSetIterationInput!): IssueSetIterationPayload
issueSetWeight(input: IssueSetWeightInput!): IssueSetWeightPayload
jiraImportStart(input: JiraImportStartInput!): JiraImportStartPayload
+ jiraImportUsers(input: JiraImportUsersInput!): JiraImportUsersPayload
markAsSpamSnippet(input: MarkAsSpamSnippetInput!): MarkAsSpamSnippetPayload
mergeRequestCreate(input: MergeRequestCreateInput!): MergeRequestCreatePayload
mergeRequestSetAssignees(input: MergeRequestSetAssigneesInput!): MergeRequestSetAssigneesPayload
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 7c31d5ab41c..4b62ac22db8 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -16359,6 +16359,126 @@
"possibleTypes": null
},
{
+ "kind": "INPUT_OBJECT",
+ "name": "JiraImportUsersInput",
+ "description": "Autogenerated input type of JiraImportUsers",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "projectPath",
+ "description": "The project to import the Jira users into",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "startAt",
+ "description": "The index of the record the import should started at, default 0 (50 records returned)",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "JiraImportUsersPayload",
+ "description": "Autogenerated return type of JiraImportUsers",
+ "fields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Errors encountered during execution of the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "jiraUsers",
+ "description": "Users returned from Jira, matched by email and name if possible.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "JiraUser",
+ "ofType": null
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
"kind": "OBJECT",
"name": "JiraProject",
"description": null,
@@ -16643,6 +16763,83 @@
},
{
"kind": "OBJECT",
+ "name": "JiraUser",
+ "description": null,
+ "fields": [
+ {
+ "name": "gitlabId",
+ "description": "Id of the matched GitLab user",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "jiraAccountId",
+ "description": "Account id of the Jira user",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "jiraDisplayName",
+ "description": "Display name of the Jira user",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "jiraEmail",
+ "description": "Email of the Jira user, returned only for users with public emails",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "Label",
"description": null,
"fields": [
@@ -21054,6 +21251,33 @@
"deprecationReason": null
},
{
+ "name": "jiraImportUsers",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "JiraImportUsersInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "JiraImportUsersPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "markAsSpamSnippet",
"description": null,
"args": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 00429423bb4..f5b5a8816d1 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -869,6 +869,16 @@ Autogenerated return type of JiraImportStart
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `jiraImport` | JiraImport | The Jira import data after mutation |
+## JiraImportUsersPayload
+
+Autogenerated return type of JiraImportUsers
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
+| `jiraUsers` | JiraUser! => Array | Users returned from Jira, matched by email and name if possible. |
+
## JiraProject
| Name | Type | Description |
@@ -885,6 +895,15 @@ Autogenerated return type of JiraImportStart
| `projects` | JiraProjectConnection | List of Jira projects fetched through Jira REST API |
| `type` | String | Class name of the service |
+## JiraUser
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `gitlabId` | Int | Id of the matched GitLab user |
+| `jiraAccountId` | String! | Account id of the Jira user |
+| `jiraDisplayName` | String! | Display name of the Jira user |
+| `jiraEmail` | String | Email of the Jira user, returned only for users with public emails |
+
## Label
| Name | Type | Description |
diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md
index 370a7e041fe..4b1f7a68f7e 100644
--- a/doc/development/i18n/proofreader.md
+++ b/doc/development/i18n/proofreader.md
@@ -109,7 +109,7 @@ are very appreciative of the work done by translators and proofreaders!
- Volodymyr Sobotovych - [GitLab](https://gitlab.com/wheleph), [CrowdIn](https://crowdin.com/profile/wheleph)
- Andrew Vityuk - [GitLab](https://gitlab.com/3_1_3_u), [CrowdIn](https://crowdin.com/profile/andruwa13)
- Welsh
- - Proofreaders needed.
+ - Delyth Prys - [GitLab](https://gitlab.com/Delyth), [CrowdIn](https://crowdin.com/profile/DelythPrys)
<!-- vale gitlab.Spelling = YES -->
## Become a proofreader
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index ae12235daae..3cd24546a30 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -528,13 +528,12 @@ the group regardless of the IP restriction.
#### Allowed domain restriction **(PREMIUM)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7297) in [GitLab Premium and Silver](https://about.gitlab.com/pricing/) 12.2.
+>- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7297) in [GitLab Premium and Silver](https://about.gitlab.com/pricing/) 12.2.
+>- Support for specifying multiple email domains [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33143) in GitLab 13.1
-You can restrict access to groups by
-allowing only users with email addresses in particular domains to be added to the group.
+You can restrict access to groups by allowing only users with email addresses in particular domains to be added to the group.
-Add email domains you want to allow and users with emails from different
-domains won't be allowed to be added to this group.
+Add email domains you want to allow and users with emails from different domains won't be allowed to be added to this group.
Some domains cannot be restricted. These are the most popular public email domains, such as:
@@ -552,7 +551,7 @@ Some domains cannot be restricted. These are the most popular public email domai
To enable this feature:
1. Navigate to the group's **Settings > General** page.
-1. Expand the **Permissions, LFS, 2FA** section, and enter the domain name into **Restrict membership by email** field.
+1. Expand the **Permissions, LFS, 2FA** section, and enter the domain names into **Restrict membership by email** field. You can enter multiple domains by separating each domain with a comma (,).
1. Click **Save changes**.
This will enable the domain-checking for all new users added to the group from this moment on.
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 11b4be0f766..69118b53220 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8468,7 +8468,7 @@ msgstr ""
msgid "EnvironmentsDashboard|More actions"
msgstr ""
-msgid "EnvironmentsDashboard|Read more."
+msgid "EnvironmentsDashboard|More information"
msgstr ""
msgid "EnvironmentsDashboard|Remove"
@@ -9050,6 +9050,9 @@ msgstr ""
msgid "Exactly one of %{attributes} is required"
msgstr ""
+msgid "Example: <code>acme.com,acme.co.in,acme.uk</code>."
+msgstr ""
+
msgid "Example: @sub\\.company\\.com$"
msgstr ""
@@ -11219,6 +11222,9 @@ msgstr ""
msgid "GroupSettings|You will need to update your local repositories to point to the new location."
msgstr ""
+msgid "GroupSettings|cannot be changed by you"
+msgstr ""
+
msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
msgstr ""
@@ -14346,6 +14352,9 @@ msgstr ""
msgid "MrDeploymentActions|Stop environment"
msgstr ""
+msgid "Multiple domains are supported with comma delimiters."
+msgstr ""
+
msgid "Multiple issue boards"
msgstr ""
@@ -15208,7 +15217,7 @@ msgstr ""
msgid "Only project members will be imported. Group members will be skipped."
msgstr ""
-msgid "Only users with an email address in this domain can be added to the group.<br>Example: <code>gitlab.com</code>. Some common domains are not allowed. %{read_more_link}."
+msgid "Only verified users with an email address in any of these domains can be added to the group."
msgstr ""
msgid "Only ‘Reporter’ roles and above on tiers Premium / Silver and above can see Productivity Analytics."
@@ -20470,6 +20479,9 @@ msgstr ""
msgid "Some child epics may be hidden due to applied filters"
msgstr ""
+msgid "Some common domains are not allowed. %{read_more_link}."
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -26363,7 +26375,7 @@ msgstr ""
msgid "element is not a hierarchy"
msgstr ""
-msgid "email '%{email}' does not match the allowed domain of '%{email_domain}'"
+msgid "email '%{email}' does not match the allowed domains of %{email_domains}"
msgstr ""
msgid "email '%{email}' is not a verified email."
diff --git a/spec/migrations/cleanup_projects_with_missing_namespace_spec.rb b/spec/migrations/cleanup_projects_with_missing_namespace_spec.rb
index 06b6d5e3b46..27c954d2984 100644
--- a/spec/migrations/cleanup_projects_with_missing_namespace_spec.rb
+++ b/spec/migrations/cleanup_projects_with_missing_namespace_spec.rb
@@ -5,10 +5,6 @@ require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200511080113_add_projects_foreign_key_to_namespaces.rb')
require Rails.root.join('db', 'post_migrate', '20200511083541_cleanup_projects_with_missing_namespace.rb')
-LOST_AND_FOUND_GROUP = 'lost-and-found'
-USER_TYPE_GHOST = 5
-ACCESS_LEVEL_OWNER = 50
-
# In order to test the CleanupProjectsWithMissingNamespace migration, we need
# to first create an orphaned project (one with an invalid namespace_id)
# and then run the migration to check that the project was properly cleaned up
@@ -77,31 +73,39 @@ describe CleanupProjectsWithMissingNamespace, :migration, schema: SchemaVersionF
end
it 'creates the ghost user' do
- expect(users.where(user_type: USER_TYPE_GHOST).count).to eq(0)
+ expect(users.where(user_type: described_class::User::USER_TYPE_GHOST).count).to eq(0)
disable_migrations_output { migrate! }
- expect(users.where(user_type: USER_TYPE_GHOST).count).to eq(1)
+ expect(users.where(user_type: described_class::User::USER_TYPE_GHOST).count).to eq(1)
end
it 'creates the lost-and-found group, owned by the ghost user' do
expect(
- Group.where(Group.arel_table[:name].matches("#{LOST_AND_FOUND_GROUP}%")).count
+ described_class::Group.where(
+ described_class::Group
+ .arel_table[:name]
+ .matches("#{described_class::User::LOST_AND_FOUND_GROUP}%")
+ ).count
).to eq(0)
disable_migrations_output { migrate! }
- ghost_user = users.find_by(user_type: USER_TYPE_GHOST)
+ ghost_user = users.find_by(user_type: described_class::User::USER_TYPE_GHOST)
expect(
- Group
+ described_class::Group
.joins('INNER JOIN members ON namespaces.id = members.source_id')
.where('namespaces.type = ?', 'Group')
.where('members.type = ?', 'GroupMember')
.where('members.source_type = ?', 'Namespace')
.where('members.user_id = ?', ghost_user.id)
.where('members.requested_at IS NULL')
- .where('members.access_level = ?', ACCESS_LEVEL_OWNER)
- .where(Group.arel_table[:name].matches("#{LOST_AND_FOUND_GROUP}%"))
+ .where('members.access_level = ?', described_class::ACCESS_LEVEL_OWNER)
+ .where(
+ described_class::Group
+ .arel_table[:name]
+ .matches("#{described_class::User::LOST_AND_FOUND_GROUP}%")
+ )
.count
).to eq(1)
end
@@ -114,7 +118,11 @@ describe CleanupProjectsWithMissingNamespace, :migration, schema: SchemaVersionF
disable_migrations_output { migrate! }
- lost_and_found_group = Group.find_by(Group.arel_table[:name].matches("#{LOST_AND_FOUND_GROUP}%"))
+ lost_and_found_group = described_class::Group.find_by(
+ described_class::Group
+ .arel_table[:name]
+ .matches("#{described_class::User::LOST_AND_FOUND_GROUP}%")
+ )
orphaned_project = projects.find_by(id: orphaned_project.id)
expect(orphaned_project.visibility_level).to eq(0)
diff --git a/spec/migrations/update_routes_for_lost_and_found_group_and_orphaned_projects_spec.rb b/spec/migrations/update_routes_for_lost_and_found_group_and_orphaned_projects_spec.rb
new file mode 100644
index 00000000000..a7e03a7fb75
--- /dev/null
+++ b/spec/migrations/update_routes_for_lost_and_found_group_and_orphaned_projects_spec.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require Rails.root.join('db', 'post_migrate', '20200602143020_update_routes_for_lost_and_found_group_and_orphaned_projects.rb')
+
+describe UpdateRoutesForLostAndFoundGroupAndOrphanedProjects, :migration do
+ let(:users) { table(:users) }
+ let(:namespaces) { table(:namespaces) }
+ let(:members) { table(:members) }
+ let(:projects) { table(:projects) }
+ let(:routes) { table(:routes) }
+
+ before do
+ # Create a Ghost User and its namnespace, but skip the route
+ ghost_user = users.create!(
+ name: 'Ghost User',
+ username: 'ghost',
+ email: 'ghost@example.com',
+ user_type: described_class::User::USER_TYPE_GHOST,
+ projects_limit: 100,
+ state: :active,
+ bio: 'This is a "Ghost User"'
+ )
+
+ namespaces.create!(
+ name: 'Ghost User',
+ path: 'ghost',
+ owner_id: ghost_user.id,
+ visibility_level: 20
+ )
+
+ # Create the 'lost-and-found', owned by the Ghost user, but with no route
+ lost_and_found_group = namespaces.create!(
+ name: described_class::User::LOST_AND_FOUND_GROUP,
+ path: described_class::User::LOST_AND_FOUND_GROUP,
+ type: 'Group',
+ description: 'Group to store orphaned projects',
+ visibility_level: 0
+ )
+
+ members.create!(
+ type: 'GroupMember',
+ source_id: lost_and_found_group.id,
+ user_id: ghost_user.id,
+ source_type: 'Namespace',
+ access_level: described_class::User::ACCESS_LEVEL_OWNER,
+ notification_level: 3
+ )
+
+ # Add an orphaned project under 'lost-and-found' but with the wrong path in its route
+ orphaned_project = projects.create!(
+ name: 'orphaned_project',
+ path: 'orphaned_project',
+ visibility_level: 20,
+ archived: false,
+ namespace_id: lost_and_found_group.id
+ )
+
+ routes.create!(
+ source_id: orphaned_project.id,
+ source_type: 'Project',
+ path: 'orphaned_project',
+ name: 'orphaned_project',
+ created_at: Time.zone.now,
+ updated_at: Time.zone.now
+ )
+ end
+
+ it 'creates the route for the ghost user namespace' do
+ expect(routes.where(path: 'ghost').count).to eq(0)
+
+ disable_migrations_output { migrate! }
+
+ expect(routes.where(path: 'ghost').count).to eq(1)
+ end
+
+ it 'creates the route for the lost-and-found group' do
+ expect(routes.where(path: described_class::User::LOST_AND_FOUND_GROUP).count).to eq(0)
+
+ disable_migrations_output { migrate! }
+
+ expect(routes.where(path: described_class::User::LOST_AND_FOUND_GROUP).count).to eq(1)
+ end
+
+ it 'updates the route for the orphaned project' do
+ orphaned_project_route = routes.find_by(path: 'orphaned_project')
+ expect(orphaned_project_route.name).to eq('orphaned_project')
+
+ disable_migrations_output { migrate! }
+
+ updated_route = routes.find_by(id: orphaned_project_route.id)
+ expect(updated_route.path).to eq("#{described_class::User::LOST_AND_FOUND_GROUP}/orphaned_project")
+ expect(updated_route.name).to eq("#{described_class::User::LOST_AND_FOUND_GROUP} / orphaned_project")
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb b/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb
new file mode 100644
index 00000000000..be0d843d5ff
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Importing Jira Users' do
+ include JiraServiceHelper
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let(:project_path) { project.full_path }
+ let(:start_at) { 7 }
+
+ let(:mutation) do
+ variables = {
+ start_at: start_at,
+ project_path: project_path
+ }
+
+ graphql_mutation(:jira_import_users, variables)
+ end
+
+ def mutation_response
+ graphql_mutation_response(:jira_import_users)
+ end
+
+ def jira_import
+ mutation_response['jiraUsers']
+ end
+
+ context 'with anonymous user' do
+ let(:current_user) { nil }
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ end
+
+ context 'with user without permissions' do
+ let(:current_user) { user }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ end
+
+ context 'when the user has permissions' do
+ let(:current_user) { user }
+
+ before do
+ project.add_maintainer(current_user)
+ end
+
+ context 'when the project path is invalid' do
+ let(:project_path) { 'foobar' }
+
+ it 'returns an an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ errors = json_response['errors']
+
+ expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
+ end
+ end
+
+ context 'when all params and permissions are ok' do
+ let(:importer) { instance_double(JiraImport::UsersImporter) }
+
+ before do
+ expect(JiraImport::UsersImporter).to receive(:new).with(current_user, project, 7)
+ .and_return(importer)
+ end
+
+ context 'when service returns a successful response' do
+ it 'returns imported users' do
+ users = [{ jira_account_id: '12a', jira_display_name: 'user 1' }]
+ result = ServiceResponse.success(payload: users)
+
+ expect(importer).to receive(:execute).and_return(result)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(jira_import.length).to eq(1)
+ expect(jira_import.first['jiraAccountId']).to eq('12a')
+ expect(jira_import.first['jiraDisplayName']).to eq('user 1')
+ end
+ end
+
+ context 'when service returns an error response' do
+ it 'returns an error messaege' do
+ result = ServiceResponse.error(message: 'Some error')
+
+ expect(importer).to receive(:execute).and_return(result)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['errors']).to eq(['Some error'])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/jira_import/users_importer_spec.rb b/spec/services/jira_import/users_importer_spec.rb
index e57a14a8f80..28ce5f1b44b 100644
--- a/spec/services/jira_import/users_importer_spec.rb
+++ b/spec/services/jira_import/users_importer_spec.rb
@@ -20,8 +20,8 @@ describe JiraImport::UsersImporter do
end
context 'when Jira import is not configured properly' do
- it 'raises an error' do
- expect { subject }.to raise_error(Projects::ImportService::Error)
+ it 'returns an error' do
+ expect(subject.errors).to eq(['Jira integration not configured.'])
end
end
diff --git a/spec/services/service_response_spec.rb b/spec/services/service_response_spec.rb
index a6567f52c6f..2c944a63ebb 100644
--- a/spec/services/service_response_spec.rb
+++ b/spec/services/service_response_spec.rb
@@ -84,4 +84,14 @@ describe ServiceResponse do
expect(described_class.error(message: 'Bad apple').error?).to eq(true)
end
end
+
+ describe '#errors' do
+ it 'returns an empty array for a successful response' do
+ expect(described_class.success.errors).to be_empty
+ end
+
+ it 'returns an array with a correct message for an error response' do
+ expect(described_class.error(message: 'error message').errors).to eq(['error message'])
+ end
+ end
end