diff options
133 files changed, 1300 insertions, 1853 deletions
diff --git a/.gitlab/ci/review-apps/main.gitlab-ci.yml b/.gitlab/ci/review-apps/main.gitlab-ci.yml index 4c0a3579c92..201c32741f9 100644 --- a/.gitlab/ci/review-apps/main.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/main.gitlab-ci.yml @@ -101,6 +101,10 @@ review-deploy: - .review:rules:review-deploy stage: deploy needs: ["review-build-cng"] + cache: + key: "review-deploy-dependencies-charts-${GITLAB_HELM_CHART_REF}-v1" + paths: + - "gitlab-${GITLAB_HELM_CHART_REF}" before_script: - export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION) - export GITALY_VERSION=$(<GITALY_SERVER_VERSION) @@ -111,7 +115,6 @@ review-deploy: script: - check_kube_domain - download_chart - - date - deploy || (display_deployment_debug && exit 1) - verify_deploy || exit 1 - disable_sign_ups || (delete_release && exit 1) diff --git a/app/assets/javascripts/graphql_shared/issuable_client.js b/app/assets/javascripts/graphql_shared/issuable_client.js index e86103c332b..a223c7fa360 100644 --- a/app/assets/javascripts/graphql_shared/issuable_client.js +++ b/app/assets/javascripts/graphql_shared/issuable_client.js @@ -4,15 +4,10 @@ import { concatPagination } from '@apollo/client/utilities'; import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql'; import createDefaultClient from '~/lib/graphql'; import typeDefs from '~/work_items/graphql/typedefs.graphql'; -import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; -import { WIDGET_TYPE_LABELS } from '~/work_items/constants'; export const temporaryConfig = { typeDefs, cacheConfig: { - possibleTypes: { - LocalWorkItemWidget: ['LocalWorkItemLabels'], - }, typePolicies: { Project: { fields: { @@ -23,20 +18,6 @@ export const temporaryConfig = { }, WorkItem: { fields: { - mockWidgets: { - read(widgets) { - return ( - widgets || [ - { - __typename: 'LocalWorkItemLabels', - type: WIDGET_TYPE_LABELS, - allowScopedLabels: true, - nodes: [], - }, - ] - ); - }, - }, widgets: { merge(_, incoming) { return incoming; @@ -62,27 +43,6 @@ export const resolvers = { }); cache.writeQuery({ query: getIssueStateQuery, data }); }, - localUpdateWorkItem(_, { input }, { cache }) { - const sourceData = cache.readQuery({ - query: workItemQuery, - variables: { id: input.id }, - }); - - const data = produce(sourceData, (draftData) => { - if (input.labels) { - const labelsWidget = draftData.workItem.mockWidgets.find( - (widget) => widget.type === WIDGET_TYPE_LABELS, - ); - labelsWidget.nodes = [...input.labels]; - } - }); - - cache.writeQuery({ - query: workItemQuery, - variables: { id: input.id }, - data, - }); - }, }, }; diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue index 6151d2ff85c..80c1fcbacfa 100644 --- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue +++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue @@ -141,8 +141,9 @@ export default { await followUser(this.user.id); this.$emit('follow'); } catch (error) { + const message = error.response?.data?.message || I18N_ERROR_FOLLOW; createAlert({ - message: I18N_ERROR_FOLLOW, + message, error, captureError: true, }); diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index 99b1a73b983..3255c7e5e4d 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -177,7 +177,7 @@ export default { return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_ASSIGNEES); }, workItemLabels() { - return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS); + return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS); }, workItemDueDate() { return this.workItem?.widgets?.find( diff --git a/app/assets/javascripts/work_items/components/work_item_labels.vue b/app/assets/javascripts/work_items/components/work_item_labels.vue index a06c90468de..9a36ddc0204 100644 --- a/app/assets/javascripts/work_items/components/work_item_labels.vue +++ b/app/assets/javascripts/work_items/components/work_item_labels.vue @@ -1,16 +1,21 @@ <script> import { GlTokenSelector, GlLabel, GlSkeletonLoader } from '@gitlab/ui'; -import { debounce, uniqueId } from 'lodash'; +import { debounce, uniqueId, without } from 'lodash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import Tracking from '~/tracking'; import labelSearchQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql'; import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; -import { isScopedLabel, scopedLabelKey } from '~/lib/utils/common_utils'; +import { isScopedLabel } from '~/lib/utils/common_utils'; import workItemQuery from '../graphql/work_item.query.graphql'; -import localUpdateWorkItemMutation from '../graphql/local_update_work_item.mutation.graphql'; +import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql'; -import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_LABELS } from '../constants'; +import { + i18n, + I18N_WORK_ITEM_ERROR_FETCHING_LABELS, + TRACKING_CATEGORY_SHOW, + WIDGET_TYPE_LABELS, +} from '../constants'; function isTokenSelectorElement(el) { return el?.classList.contains('gl-label-close') || el?.classList.contains('dropdown-item'); @@ -52,6 +57,8 @@ export default { localLabels: [], searchKey: '', searchLabels: [], + addLabelIds: [], + removeLabelIds: [], }; }, apollo: { @@ -74,7 +81,7 @@ export default { variables() { return { fullPath: this.fullPath, - search: this.searchKey, + searchTerm: this.searchKey, }; }, skip() { @@ -84,7 +91,7 @@ export default { return data.workspace?.labels?.nodes.map((node) => addClass({ ...node, ...node.label })); }, error() { - this.$emit('error', i18n.fetchError); + this.$emit('error', I18N_WORK_ITEM_ERROR_FETCHING_LABELS); }, }, }, @@ -100,7 +107,7 @@ export default { }; }, allowScopedLabels() { - return this.labelsWidget.allowScopedLabels; + return this.labelsWidget?.allowsScopedLabels; }, containerClass() { return !this.isEditing ? 'gl-shadow-none!' : ''; @@ -109,10 +116,10 @@ export default { return this.$apollo.queries.searchLabels.loading; }, labelsWidget() { - return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS); + return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS); }, labels() { - return this.labelsWidget?.nodes || []; + return this.labelsWidget?.labels?.nodes || []; }, }, watch: { @@ -131,44 +138,74 @@ export default { }, removeLabel({ id }) { this.localLabels = this.localLabels.filter((label) => label.id !== id); + this.removeLabelIds.push(id); + this.setLabels(); }, - setLabels(event) { + async setLabels() { + if (this.addLabelIds.length === 0 && this.removeLabelIds.length === 0) return; + this.searchKey = ''; - if (isTokenSelectorElement(event.relatedTarget) || !this.isEditing) return; this.isEditing = false; - this.$apollo - .mutate({ - mutation: localUpdateWorkItemMutation, + try { + const { + data: { + workItemUpdate: { errors }, + }, + } = await this.$apollo.mutate({ + mutation: updateWorkItemMutation, variables: { input: { id: this.workItemId, - labels: this.localLabels, + labelsWidget: { + addLabelIds: this.addLabelIds, + removeLabelIds: this.removeLabelIds, + }, }, }, - }) - .catch((e) => { - this.$emit('error', e); }); - this.track('updated_labels'); + + if (errors.length > 0) { + this.throwUpdateError(); + return; + } + + this.addLabelIds = []; + this.removeLabelIds = []; + + this.track('updated_labels'); + } catch { + this.throwUpdateError(); + } + }, + throwUpdateError() { + this.$emit('error', i18n.updateError); + // If mutation is rejected, we're rolling back to initial state + this.localLabels = this.labels.map(addClass); + this.addLabelIds = []; + this.removeLabelIds = []; + }, + handleBlur(event) { + if (isTokenSelectorElement(event.relatedTarget) || !this.isEditing) return; + this.setLabels(); }, handleFocus() { this.isEditing = true; this.searchStarted = true; }, async focusTokenSelector(labels) { - if (this.allowScopedLabels) { - const newLabel = labels[labels.length - 1]; - const existingLabels = labels.slice(0, labels.length - 1); - - const newLabelKey = scopedLabelKey(newLabel); + const labelsToAdd = without(labels, ...this.localLabels).map((label) => label.id); + const labelsToRemove = without(this.localLabels, ...labels).map((label) => label.id); - const removeLabelsWithSameScope = existingLabels.filter((label) => { - const sameKey = newLabelKey === scopedLabelKey(label); - return !sameKey; - }); + if (labelsToAdd.length > 0) { + this.addLabelIds.push(...labelsToAdd); + } - this.localLabels = [...removeLabelsWithSameScope, newLabel]; + if (labelsToRemove.length > 0) { + this.removeLabelIds.push(...labelsToRemove); } + + this.localLabels = labels; + this.handleFocus(); await this.$nextTick(); this.$refs.tokenSelector.focusTextInput(); @@ -201,7 +238,7 @@ export default { > <gl-token-selector ref="tokenSelector" - v-model="localLabels" + :selected-tokens="localLabels" :aria-labelledby="labelsTitleId" :container-class="containerClass" :dropdown-items="searchLabels" @@ -212,7 +249,7 @@ export default { @input="focusTokenSelector" @text-input="debouncedSearchKeyUpdate" @focus="handleFocus" - @blur="setLabels" + @blur="handleBlur" @mouseover.native="handleMouseOver" @mouseout.native="handleMouseOut" > diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index 5cd01de44bf..97c445de711 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -36,6 +36,9 @@ export const i18n = { ), }; +export const I18N_WORK_ITEM_ERROR_FETCHING_LABELS = s__( + 'WorkItem|Something went wrong when fetching labels. Please try again.', +); export const I18N_WORK_ITEM_ERROR_CREATING = s__( 'WorkItem|Something went wrong when creating %{workItemType}. Please try again.', ); diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql index 36ffba8a540..d3712da1329 100644 --- a/app/assets/javascripts/work_items/graphql/typedefs.graphql +++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql @@ -1,6 +1,5 @@ enum LocalWidgetType { ASSIGNEES - LABELS } interface LocalWorkItemWidget { @@ -12,16 +11,6 @@ type LocalWorkItemAssignees implements LocalWorkItemWidget { nodes: [UserCore] } -type LocalWorkItemLabels implements LocalWorkItemWidget { - type: LocalWidgetType! - allowScopedLabels: Boolean! - nodes: [Label!] -} - -extend type WorkItem { - mockWidgets: [LocalWorkItemWidget] -} - input LocalUserInput { id: ID! name: String @@ -30,17 +19,9 @@ input LocalUserInput { avatarUrl: String } -input LocalLabelInput { - id: ID! - title: String! - color: String - description: String -} - input LocalUpdateWorkItemInput { id: WorkItemID! assignees: [LocalUserInput!] - labels: [LocalLabelInput] } type LocalWorkItemPayload { diff --git a/app/assets/javascripts/work_items/graphql/work_item.query.graphql b/app/assets/javascripts/work_items/graphql/work_item.query.graphql index 276061af193..3b46fed97ec 100644 --- a/app/assets/javascripts/work_items/graphql/work_item.query.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item.query.graphql @@ -1,17 +1,7 @@ -#import "~/graphql_shared/fragments/label.fragment.graphql" #import "./work_item.fragment.graphql" query workItem($id: WorkItemID!) { workItem(id: $id) { ...WorkItem - mockWidgets @client { - ... on LocalWorkItemLabels { - type - allowScopedLabels - nodes { - ...Label - } - } - } } } diff --git a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql index 3005069f59a..79222d11226 100644 --- a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql @@ -1,3 +1,6 @@ +#import "~/graphql_shared/fragments/label.fragment.graphql" +#import "~/graphql_shared/fragments/user.fragment.graphql" + fragment WorkItemWidgets on WorkItemWidget { ... on WorkItemWidgetDescription { type @@ -14,6 +17,14 @@ fragment WorkItemWidgets on WorkItemWidget { } } } + ... on WorkItemWidgetLabels { + type + labels { + nodes { + ...Label + } + } + } ... on WorkItemWidgetStartAndDueDate { type dueDate diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2ee9832547c..c35aa8e4346 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -174,8 +174,9 @@ class UsersController < ApplicationController end def follow - current_user.follow(user) + followee = current_user.follow(user) + flash[:alert] = followee.errors.full_messages.join(', ') if followee&.errors&.any? redirect_path = referer_path(request) || @user redirect_to redirect_path diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb index de240e40316..df75c557717 100644 --- a/app/models/protected_branch/merge_access_level.rb +++ b/app/models/protected_branch/merge_access_level.rb @@ -2,4 +2,6 @@ class ProtectedBranch::MergeAccessLevel < ApplicationRecord include ProtectedBranchAccess + # default value for the access_level column + GITLAB_DEFAULT_ACCESS_LEVEL = Gitlab::Access::MAINTAINER end diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index 5248834a2f2..6076fab20b7 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -2,6 +2,8 @@ class ProtectedBranch::PushAccessLevel < ApplicationRecord include ProtectedBranchAccess + # default value for the access_level column + GITLAB_DEFAULT_ACCESS_LEVEL = Gitlab::Access::MAINTAINER belongs_to :deploy_key diff --git a/app/models/user.rb b/app/models/user.rb index bb8d5d24b3b..338cebad6a1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1648,8 +1648,9 @@ class User < ApplicationRecord begin followee = Users::UserFollowUser.create(follower_id: self.id, followee_id: user.id) self.followees.reset if followee.persisted? + followee rescue ActiveRecord::RecordNotUnique - false + nil end end diff --git a/app/models/users/user_follow_user.rb b/app/models/users/user_follow_user.rb index a94239a746c..5a82a81364a 100644 --- a/app/models/users/user_follow_user.rb +++ b/app/models/users/user_follow_user.rb @@ -1,7 +1,22 @@ # frozen_string_literal: true module Users class UserFollowUser < ApplicationRecord + MAX_FOLLOWEE_LIMIT = 300 + belongs_to :follower, class_name: 'User' belongs_to :followee, class_name: 'User' + + validate :max_follow_limit + + private + + def max_follow_limit + followee_count = self.class.where(follower_id: follower_id).limit(MAX_FOLLOWEE_LIMIT).count + return if followee_count < MAX_FOLLOWEE_LIMIT + + errors.add(:base, format( + _("You can't follow more than %{limit} users. To follow more users, unfollow some others."), + limit: MAX_FOLLOWEE_LIMIT)) + end end end diff --git a/app/services/clusters/applications/destroy_service.rb b/app/services/clusters/applications/destroy_service.rb deleted file mode 100644 index d666682487b..00000000000 --- a/app/services/clusters/applications/destroy_service.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Applications - class DestroyService < ::Clusters::Applications::BaseService - def execute(_request) - instantiate_application.tap do |application| - break unless application.can_uninstall? - - application.make_scheduled! - - Clusters::Applications::UninstallWorker.perform_async(application.name, application.id) - end - end - - private - - def builder - cluster.public_send(application_class.association_name) # rubocop:disable GitlabSecurity/PublicSend - end - end - end -end diff --git a/app/services/clusters/applications/uninstall_service.rb b/app/services/clusters/applications/uninstall_service.rb deleted file mode 100644 index 50c8d806c14..00000000000 --- a/app/services/clusters/applications/uninstall_service.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Applications - class UninstallService < BaseHelmService - def execute - return unless app.scheduled? - - app.make_uninstalling! - uninstall - end - - private - - def uninstall - helm_api.uninstall(app.uninstall_command) - - Clusters::Applications::WaitForUninstallAppWorker.perform_in( - Clusters::Applications::WaitForUninstallAppWorker::INTERVAL, app.name, app.id) - rescue Kubeclient::HttpError => e - log_error(e) - app.make_errored!("Kubernetes error: #{e.error_code}") - rescue StandardError => e - log_error(e) - app.make_errored!('Failed to uninstall.') - end - end - end -end diff --git a/app/views/projects/settings/access_tokens/index.html.haml b/app/views/projects/settings/access_tokens/index.html.haml index 8f28b31e833..fe3ab3f1448 100644 --- a/app/views/projects/settings/access_tokens/index.html.haml +++ b/app/views/projects/settings/access_tokens/index.html.haml @@ -39,6 +39,7 @@ access_levels: ProjectMember.permissible_access_level_roles(current_user, @project), default_access_level: Gitlab::Access::GUEST, prefix: :resource_access_token, + description_prefix: :project_access_token, help_path: help_page_path('user/project/settings/project_access_tokens', anchor: 'scopes-for-a-project-access-token') = render 'shared/access_tokens/table', diff --git a/app/views/shared/access_tokens/_form.html.haml b/app/views/shared/access_tokens/_form.html.haml index 7f7f8a96625..eada58091b7 100644 --- a/app/views/shared/access_tokens/_form.html.haml +++ b/app/views/shared/access_tokens/_form.html.haml @@ -1,6 +1,7 @@ - ajax = local_assigns.fetch(:ajax, false) - title = local_assigns.fetch(:title, _('Add a %{type}') % { type: type }) - prefix = local_assigns.fetch(:prefix, :personal_access_token) +- description_prefix = local_assigns.fetch(:description_prefix, prefix) - help_path = local_assigns.fetch(:help_path) - resource = local_assigns.fetch(:resource, false) - access_levels = local_assigns.fetch(:access_levels, false) @@ -43,7 +44,7 @@ %p.text-secondary#select_scope_help_text = s_('Tokens|Scopes set the permission levels granted to the token.') = link_to _("Learn more."), help_path, target: '_blank', rel: 'noopener noreferrer' - = render 'shared/tokens/scopes_form', prefix: prefix, token: token, scopes: scopes, f: f + = render 'shared/tokens/scopes_form', prefix: prefix, description_prefix: description_prefix, token: token, scopes: scopes, f: f .gl-mt-3 = f.submit _('Create %{type}') % { type: type }, data: { qa_selector: 'create_token_button' }, pajamas_button: true diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml index 010376464f1..1c63ce490ed 100644 --- a/app/views/shared/tokens/_scopes_form.html.haml +++ b/app/views/shared/tokens/_scopes_form.html.haml @@ -1,11 +1,12 @@ - scopes = local_assigns.fetch(:scopes) - prefix = local_assigns.fetch(:prefix) +- description_prefix = local_assigns.fetch(:description_prefix, prefix) - token = local_assigns.fetch(:token) - f = local_assigns.fetch(:f) %fieldset - scopes.each do |scope| - - help_text = t scope, scope: scope_description(prefix) + - help_text = t scope, scope: scope_description(description_prefix) = f.gitlab_ui_checkbox_component :scopes, scope, help_text: help_text, checkbox_options: { checked: token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", multiple: true, data: { qa_selector: "#{scope}_checkbox" } }, diff --git a/app/workers/clusters/applications/uninstall_worker.rb b/app/workers/clusters/applications/uninstall_worker.rb index da290eaf1f6..b71f87014aa 100644 --- a/app/workers/clusters/applications/uninstall_worker.rb +++ b/app/workers/clusters/applications/uninstall_worker.rb @@ -14,11 +14,7 @@ module Clusters worker_has_external_dependencies! loggable_arguments 0 - def perform(app_name, app_id) - find_application(app_name, app_id) do |app| - Clusters::Applications::UninstallService.new(app).execute - end - end + def perform(app_name, app_id); end end end end diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index 81e4f73e6b2..0b3b5af48a1 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -98,17 +98,17 @@ en: Grants read-only access to the user's primary email address using OpenID Connect. project_access_token_scope_desc: api: - Grants complete read/write access to the scoped project API. + Grants complete read and write access to the scoped project API, including the Package Registry. read_api: - Grants read access to the scoped project API. + Grants read access to the scoped project API, including the Package Registry. read_repository: - Allows read-only access (pull) to the repository. + Grants read access (pull) to the repository. write_repository: - Allows read-write access (pull, push) to the repository. + Grants read and write access (pull and push) to the repository. read_registry: - Allows read-access (pull) to container registry images if the project is private and authorization is required. + Grants read access (pull) to the Container Registry images if a project is private and authorization is required. write_registry: - Allows write-access (push) to container registry. + Grants write access (push) to the Container Registry. flash: applications: create: diff --git a/db/docs/push_rules.yml b/db/docs/push_rules.yml index 6b62013892b..6a51fc79b33 100644 --- a/db/docs/push_rules.yml +++ b/db/docs/push_rules.yml @@ -3,7 +3,7 @@ table_name: push_rules classes: - PushRule feature_categories: -- compliance_management +- source_code_management description: TODO introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/1b98b5ab97ce3e9997df542059cbf3c6ce0bf0e1 milestone: '8.10' diff --git a/db/migrate/20221011162637_add_partial_index_project_incident_management_settings_on_project_id_and_sla_timer.rb b/db/migrate/20221011162637_add_partial_index_project_incident_management_settings_on_project_id_and_sla_timer.rb new file mode 100644 index 00000000000..4238311005c --- /dev/null +++ b/db/migrate/20221011162637_add_partial_index_project_incident_management_settings_on_project_id_and_sla_timer.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddPartialIndexProjectIncidentManagementSettingsOnProjectIdAndSlaTimer < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + INDEX_NAME = 'index_project_incident_management_settings_on_p_id_sla_timer' + + def up + add_concurrent_index :project_incident_management_settings, :project_id, + name: INDEX_NAME, + where: 'sla_timer = TRUE' + end + + def down + remove_concurrent_index_by_name :project_incident_management_settings, name: INDEX_NAME + end +end diff --git a/db/schema_migrations/20221011162637 b/db/schema_migrations/20221011162637 new file mode 100644 index 00000000000..c16e511e516 --- /dev/null +++ b/db/schema_migrations/20221011162637 @@ -0,0 +1 @@ +b39261356f0ca89d543f680e1b28f3e3bdf468b02d6f8ea21c6ea1a1af91420c
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index f36419194c8..826b90a475a 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -29933,6 +29933,8 @@ CREATE INDEX index_project_group_links_on_project_id ON project_group_links USIN CREATE INDEX index_project_import_data_on_project_id ON project_import_data USING btree (project_id); +CREATE INDEX index_project_incident_management_settings_on_p_id_sla_timer ON project_incident_management_settings USING btree (project_id) WHERE (sla_timer = true); + CREATE INDEX index_project_members_on_id_temp ON members USING btree (id) WHERE ((source_type)::text = 'Project'::text); CREATE INDEX index_project_mirror_data_on_last_successful_update_at ON project_mirror_data USING btree (last_successful_update_at); diff --git a/doc/administration/system_hooks.md b/doc/administration/system_hooks.md index f973519bdf3..56e73150616 100644 --- a/doc/administration/system_hooks.md +++ b/doc/administration/system_hooks.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments type: reference diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md index 23ebe4772e9..531d679a34d 100644 --- a/doc/api/api_resources.md +++ b/doc/api/api_resources.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/api/graphql/getting_started.md b/doc/api/graphql/getting_started.md index 1b7aba52a0a..20fb2f030f2 100644 --- a/doc/api/graphql/getting_started.md +++ b/doc/api/graphql/getting_started.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/api/graphql/index.md b/doc/api/graphql/index.md index b7b7eb15c3e..40e1ed115a3 100644 --- a/doc/api/graphql/index.md +++ b/doc/api/graphql/index.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/api/graphql/removed_items.md b/doc/api/graphql/removed_items.md index 278f2b78afe..57f1c49290f 100644 --- a/doc/api/graphql/removed_items.md +++ b/doc/api/graphql/removed_items.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/api/index.md b/doc/api/index.md index 24608c2a7e2..7e14137b0fe 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/api/integrations.md b/doc/api/integrations.md index 3ddec579f0c..176ed931d38 100644 --- a/doc/api/integrations.md +++ b/doc/api/integrations.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/api/openapi/openapi_interactive.md b/doc/api/openapi/openapi_interactive.md index a3e3530fe7a..1cf6ba6482c 100644 --- a/doc/api/openapi/openapi_interactive.md +++ b/doc/api/openapi/openapi_interactive.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/api/saml.md b/doc/api/saml.md new file mode 100644 index 00000000000..810ed382d49 --- /dev/null +++ b/doc/api/saml.md @@ -0,0 +1,78 @@ +--- +stage: Manage +group: Authentication and Authorization +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + +# SAML API **(PREMIUM SAAS)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/227841) in GitLab 15.5. + +API for accessing SAML features. + +## Get SAML identities for a group + +```plaintext +GET /groups/:id/saml/identities +``` + +Fetch SAML identities for a group. + +Supported attributes: + +| Attribute | Type | Required | Description | +|:------------------|:--------|:---------|:----------------------| +| `id` | integer | Yes | Group ID for the group to return SAML identities. | + +If successful, returns [`200`](index.md#status-codes) and the following +response attributes: + +| Attribute | Type | Description | +| ------------ | ------ | ------------------------- | +| `extern_uid` | string | External UID for the user | +| `user_id` | string | ID for the user | + +Example request: + +```shell +curl --location --request GET "https://gdk.test:3443/api/v4/groups/33/saml/identities" \ +--header "<PRIVATE-TOKEN>" \ +--form "extern_uid=<ID_TO_BE_UPDATED>" \ +``` + +Example response: + +```json +[ + { + "extern_uid": "4", + "user_id": 48 + } +] +``` + +## Update `extern_uid` field for a SAML identity + +Update `extern_uid` field for a SAML identity. Field that can be updated are: + +| SAML IdP attribute | GitLab field | +| ------------------ | ------------ | +| `id/externalId` | `extern_uid` | + +```plaintext +PATCH groups/:groups_id/saml/:uid +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ------ | -------- | ------------------------- | +| `uid` | string | yes | External UID of the user. | + +Example request: + +```shell +curl --location --request PATCH "https://gdk.test:3443/api/v4/groups/33/saml/sydney_jones" \ +--header "<PRIVATE TOKEN>" \ +--form "extern_uid=sydney_jones_new" \ +``` diff --git a/doc/api/scim.md b/doc/api/scim.md index 9a745776f65..b1763a44fc4 100644 --- a/doc/api/scim.md +++ b/doc/api/scim.md @@ -4,251 +4,80 @@ stage: Manage group: Authentication and Authorization info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- +# SCIM API **(PREMIUM SAAS)** -# SCIM API (SYSTEM ONLY) **(PREMIUM SAAS)** - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9388) in GitLab 11.10. - -The SCIM API implements the [RFC7644 protocol](https://www.rfc-editor.org/rfc/rfc7644). As this API is for -**system** use for SCIM provider integration, it is subject to change without notice. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98354) in GitLab 15.5. To use this API, [Group SSO](../user/group/saml_sso/index.md) must be enabled for the group. This API is only in use where [SCIM for Group SSO](../user/group/saml_sso/scim_setup.md) is enabled. It's a prerequisite to the creation of SCIM identities. -## Get a list of SCIM provisioned users - -This endpoint is used as part of the SCIM syncing mechanism. It only returns -a single user based on a unique ID which should match the `extern_uid` of the user. - -```plaintext -GET /api/scim/v2/groups/:group_path/Users -``` - -Parameters: - -| Attribute | Type | Required | Description | -|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------| -| `filter` | string | no | A [filter](#available-filters) expression. | -| `group_path` | string | yes | Full path to the group. | -| `startIndex` | integer | no | The 1-based index indicating where to start returning results from. A value of less than one will be interpreted as 1. | -| `count` | integer | no | Desired maximum number of query results. | - -NOTE: -Pagination follows the [SCIM spec](https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2.4) rather than GitLab pagination as used elsewhere. If records change between requests it is possible for a page to either be missing records that have moved to a different page or repeat records from a previous request. - -Example request: - -```shell -curl "https://gitlab.example.com/api/scim/v2/groups/test_group/Users?filter=id%20eq%20%220b1d561c-21ff-4092-beab-8154b17f82f2%22" \ - --header "Authorization: Bearer <your_scim_token>" \ - --header "Content-Type: application/scim+json" -``` - -Example response: +Not to be confused with the [internal SCIM API](../development/internal_api/index.md#scim-api). -```json -{ - "schemas": [ - "urn:ietf:params:scim:api:messages:2.0:ListResponse" - ], - "totalResults": 1, - "itemsPerPage": 20, - "startIndex": 1, - "Resources": [ - { - "schemas": [ - "urn:ietf:params:scim:schemas:core:2.0:User" - ], - "id": "0b1d561c-21ff-4092-beab-8154b17f82f2", - "active": true, - "name.formatted": "Test User", - "userName": "username", - "meta": { "resourceType":"User" }, - "emails": [ - { - "type": "work", - "value": "name@example.com", - "primary": true - } - ] - } - ] -} -``` +## Get SCIM identities for a group -## Get a single SCIM provisioned user +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/227841) in GitLab 15.5. ```plaintext -GET /api/scim/v2/groups/:group_path/Users/:id +GET /groups/:id/scim/identities ``` -Parameters: +Supported attributes: -| Attribute | Type | Required | Description | -|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------| -| `id` | string | yes | External UID of the user. | -| `group_path` | string | yes | Full path to the group. | +| Attribute | Type | Required | Description | +|:------------------|:--------|:---------|:----------------------| +| `id` | integer | Yes | Return SAML identities for the given group ID. | -Example request: +If successful, returns [`200`](index.md#status-codes) and the following +response attributes: -```shell -curl "https://gitlab.example.com/api/scim/v2/groups/test_group/Users/f0b1d561c-21ff-4092-beab-8154b17f82f2" \ - --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json" -``` +| Attribute | Type | Description | +| ------------ | ------ | ------------------------- | +| `extern_uid` | string | External UID for the user | +| `user_id` | string | ID for the user | Example response: ```json -{ - "schemas": [ - "urn:ietf:params:scim:schemas:core:2.0:User" - ], - "id": "0b1d561c-21ff-4092-beab-8154b17f82f2", - "active": true, - "name.formatted": "Test User", - "userName": "username", - "meta": { "resourceType":"User" }, - "emails": [ +[ { - "type": "work", - "value": "name@example.com", - "primary": true + "extern_uid": "4", + "user_id": 48 } - ] -} +] ``` -## Create a SCIM provisioned user - -```plaintext -POST /api/scim/v2/groups/:group_path/Users/ -``` - -Parameters: - -| Attribute | Type | Required | Description | -|:---------------|:----------|:----|:--------------------------| -| `externalId` | string | yes | External UID of the user. | -| `userName` | string | yes | Username of the user. | -| `emails` | JSON string | yes | Work email. | -| `name` | JSON string | yes | Name of the user. | -| `meta` | string | no | Resource type (`User`). | - Example request: ```shell -curl --verbose --request POST "https://gitlab.example.com/api/scim/v2/groups/test_group/Users" \ - --data '{"externalId":"test_uid","active":null,"userName":"username","emails":[{"primary":true,"type":"work","value":"name@example.com"}],"name":{"formatted":"Test User","familyName":"User","givenName":"Test"},"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"meta":{"resourceType":"User"}}' \ - --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json" -``` - -Example response: - -```json -{ - "schemas": [ - "urn:ietf:params:scim:schemas:core:2.0:User" - ], - "id": "0b1d561c-21ff-4092-beab-8154b17f82f2", - "active": true, - "name.formatted": "Test User", - "userName": "username", - "meta": { "resourceType":"User" }, - "emails": [ - { - "type": "work", - "value": "name@example.com", - "primary": true - } - ] -} +curl --location --request GET "https://gdk.test:3443/api/v4/groups/33/scim/identities" \ +--header "<PRIVATE-TOKEN>" \ +--form "extern_uid=<ID_TO_BE_UPDATED>" \ ``` -Returns a `201` status code if successful. +## Update extern_uid field for a SCIM identity -## Update a single SCIM provisioned user +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/227841) in GitLab 15.5. Fields that can be updated are: -| SCIM/IdP field | GitLab field | -|:---------------------------------|:-----------------------------------------------------------------------------| -| `id/externalId` | `extern_uid` | -| `name.formatted` | `name` ([Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/363058)) | -| `emails\[type eq "work"\].value` | `email` ([Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/363058)) | -| `active` | Identity removal if `active` = `false` | -| `userName` | `username` ([Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/363058)) | - -```plaintext -PATCH /api/scim/v2/groups/:group_path/Users/:id -``` - -Parameters: - -| Attribute | Type | Required | Description | -|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------| -| `id` | string | yes | External UID of the user. | -| `group_path` | string | yes | Full path to the group. | -| `Operations` | JSON string | yes | An [operations](#available-operations) expression. | - -Example request: - -```shell -curl --verbose --request PATCH "https://gitlab.example.com/api/scim/v2/groups/test_group/Users/f0b1d561c-21ff-4092-beab-8154b17f82f2" \ - --data '{ "Operations": [{"op":"Add","path":"name.formatted","value":"New Name"}] }' \ - --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json" -``` - -Returns an empty response with a `204` status code if successful. - -## Remove a single SCIM provisioned user - -Removes the user's SSO identity and group membership. +| SCIM/IdP field | GitLab field | +| --------------- | ------------ | +| `id/externalId` | `extern_uid` | ```plaintext -DELETE /api/scim/v2/groups/:group_path/Users/:id +PATCH groups/:groups_id/scim/:uid ``` Parameters: -| Attribute | Type | Required | Description | -|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------| -| `id` | string | yes | External UID of the user. | -| `group_path` | string | yes | Full path to the group. | +| Attribute | Type | Required | Description | +| --------- | ------ | -------- | ------------------------- | +| `uid` | string | yes | External UID of the user. | Example request: ```shell -curl --verbose --request DELETE "https://gitlab.example.com/api/scim/v2/groups/test_group/Users/f0b1d561c-21ff-4092-beab-8154b17f82f2" \ - --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json" -``` - -Returns an empty response with a `204` status code if successful. - -## Available filters - -They match an expression as specified in [the RFC7644 filtering section](https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2.2). - -| Filter | Description | -| ----- | ----------- | -| `eq` | The attribute matches exactly the specified value. | - -Example: - -```plaintext -id eq a-b-c-d -``` - -## Available operations - -They perform an operation as specified in [the RFC7644 update section](https://www.rfc-editor.org/rfc/rfc7644#section-3.5.2). - -| Operator | Description | -| ----- | ----------- | -| `Replace` | The attribute's value is updated. | -| `Add` | The attribute has a new value. | - -Example: - -```json -{ "op": "Add", "path": "name.formatted", "value": "New Name" } +curl --location --request PATCH "https://gdk.test:3443/api/v4/groups/33/scim/sydney_jones" \ +--header "<PRIVATE TOKEN>" \ +--form "extern_uid=sydney_jones_new" \ ``` diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md index ef5e069b251..1b72ef1da53 100644 --- a/doc/api/system_hooks.md +++ b/doc/api/system_hooks.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/development/fe_guide/view_component.md b/doc/development/fe_guide/view_component.md index 118f1cd9efb..662d1ad32fc 100644 --- a/doc/development/fe_guide/view_component.md +++ b/doc/development/fe_guide/view_component.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Foundations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/development/graphql_guide/graphql_pro.md b/doc/development/graphql_guide/graphql_pro.md index 0b17f9c14b6..ec28ceb4f20 100644 --- a/doc/development/graphql_guide/graphql_pro.md +++ b/doc/development/graphql_guide/graphql_pro.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/development/graphql_guide/index.md b/doc/development/graphql_guide/index.md index 320de483a4c..9c6a2559e5c 100644 --- a/doc/development/graphql_guide/index.md +++ b/doc/development/graphql_guide/index.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/development/graphql_guide/monitoring.md b/doc/development/graphql_guide/monitoring.md index 59c2a00eeaf..1e4c083653e 100644 --- a/doc/development/graphql_guide/monitoring.md +++ b/doc/development/graphql_guide/monitoring.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/development/graphql_guide/pagination.md b/doc/development/graphql_guide/pagination.md index 28884135ca1..7b9d2158c60 100644 --- a/doc/development/graphql_guide/pagination.md +++ b/doc/development/graphql_guide/pagination.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/development/integrations/index.md b/doc/development/integrations/index.md index 1be96d31f66..a0a81775391 100644 --- a/doc/development/integrations/index.md +++ b/doc/development/integrations/index.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments description: "GitLab's development guidelines for Integrations" diff --git a/doc/development/integrations/jenkins.md b/doc/development/integrations/jenkins.md index 4167094cded..f314f4536e2 100644 --- a/doc/development/integrations/jenkins.md +++ b/doc/development/integrations/jenkins.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/development/integrations/jira_connect.md b/doc/development/integrations/jira_connect.md index ff37748d82c..fb0d239db46 100644 --- a/doc/development/integrations/jira_connect.md +++ b/doc/development/integrations/jira_connect.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/development/internal_api/index.md b/doc/development/internal_api/index.md index 151ad3df074..9b77a20a0d6 100644 --- a/doc/development/internal_api/index.md +++ b/doc/development/internal_api/index.md @@ -965,3 +965,253 @@ Example response: ### Known consumers - CustomersDot + +## SCIM API **(PREMIUM SAAS)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9388) in GitLab 11.10. + +The SCIM API implements the [RFC7644 protocol](https://www.rfc-editor.org/rfc/rfc7644). As this API is for +**system** use for SCIM provider integration, it is subject to change without notice. + +To use this API, [Group SSO](../../user/group/saml_sso/index.md) must be enabled for the group. +This API is only in use where [SCIM for Group SSO](../../user/group/saml_sso/scim_setup.md) is enabled. It's a prerequisite to the creation of SCIM identities. + +Not to be confused with the [main SCIM API](../../api/scim.md). + +### Get a list of SCIM provisioned users + +This endpoint is used as part of the SCIM syncing mechanism. It only returns +a single user based on a unique ID which should match the `extern_uid` of the user. + +```plaintext +GET /api/scim/v2/groups/:group_path/Users +``` + +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------| +| `filter` | string | no | A [filter](#available-filters) expression. | +| `group_path` | string | yes | Full path to the group. | +| `startIndex` | integer | no | The 1-based index indicating where to start returning results from. A value of less than one will be interpreted as 1. | +| `count` | integer | no | Desired maximum number of query results. | + +NOTE: +Pagination follows the [SCIM spec](https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2.4) rather than GitLab pagination as used elsewhere. If records change between requests it is possible for a page to either be missing records that have moved to a different page or repeat records from a previous request. + +Example request: + +```shell +curl "https://gitlab.example.com/api/scim/v2/groups/test_group/Users?filter=id%20eq%20%220b1d561c-21ff-4092-beab-8154b17f82f2%22" \ + --header "Authorization: Bearer <your_scim_token>" \ + --header "Content-Type: application/scim+json" +``` + +Example response: + +```json +{ + "schemas": [ + "urn:ietf:params:scim:api:messages:2.0:ListResponse" + ], + "totalResults": 1, + "itemsPerPage": 20, + "startIndex": 1, + "Resources": [ + { + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:User" + ], + "id": "0b1d561c-21ff-4092-beab-8154b17f82f2", + "active": true, + "name.formatted": "Test User", + "userName": "username", + "meta": { "resourceType":"User" }, + "emails": [ + { + "type": "work", + "value": "name@example.com", + "primary": true + } + ] + } + ] +} +``` + +### Get a single SCIM provisioned user + +```plaintext +GET /api/scim/v2/groups/:group_path/Users/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------| +| `id` | string | yes | External UID of the user. | +| `group_path` | string | yes | Full path to the group. | + +Example request: + +```shell +curl "https://gitlab.example.com/api/scim/v2/groups/test_group/Users/f0b1d561c-21ff-4092-beab-8154b17f82f2" \ + --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json" +``` + +Example response: + +```json +{ + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:User" + ], + "id": "0b1d561c-21ff-4092-beab-8154b17f82f2", + "active": true, + "name.formatted": "Test User", + "userName": "username", + "meta": { "resourceType":"User" }, + "emails": [ + { + "type": "work", + "value": "name@example.com", + "primary": true + } + ] +} +``` + +### Create a SCIM provisioned user + +```plaintext +POST /api/scim/v2/groups/:group_path/Users/ +``` + +Parameters: + +| Attribute | Type | Required | Description | +|:---------------|:----------|:----|:--------------------------| +| `externalId` | string | yes | External UID of the user. | +| `userName` | string | yes | Username of the user. | +| `emails` | JSON string | yes | Work email. | +| `name` | JSON string | yes | Name of the user. | +| `meta` | string | no | Resource type (`User`). | + +Example request: + +```shell +curl --verbose --request POST "https://gitlab.example.com/api/scim/v2/groups/test_group/Users" \ + --data '{"externalId":"test_uid","active":null,"userName":"username","emails":[{"primary":true,"type":"work","value":"name@example.com"}],"name":{"formatted":"Test User","familyName":"User","givenName":"Test"},"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"meta":{"resourceType":"User"}}' \ + --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json" +``` + +Example response: + +```json +{ + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:User" + ], + "id": "0b1d561c-21ff-4092-beab-8154b17f82f2", + "active": true, + "name.formatted": "Test User", + "userName": "username", + "meta": { "resourceType":"User" }, + "emails": [ + { + "type": "work", + "value": "name@example.com", + "primary": true + } + ] +} +``` + +Returns a `201` status code if successful. + +### Update a single SCIM provisioned user + +Fields that can be updated are: + +| SCIM/IdP field | GitLab field | +|:---------------------------------|:-----------------------------------------------------------------------------| +| `id/externalId` | `extern_uid` | +| `name.formatted` | `name` ([Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/363058)) | +| `emails\[type eq "work"\].value` | `email` ([Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/363058)) | +| `active` | Identity removal if `active` = `false` | +| `userName` | `username` ([Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/363058)) | + +```plaintext +PATCH /api/scim/v2/groups/:group_path/Users/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------| +| `id` | string | yes | External UID of the user. | +| `group_path` | string | yes | Full path to the group. | +| `Operations` | JSON string | yes | An [operations](#available-operations) expression. | + +Example request: + +```shell +curl --verbose --request PATCH "https://gitlab.example.com/api/scim/v2/groups/test_group/Users/f0b1d561c-21ff-4092-beab-8154b17f82f2" \ + --data '{ "Operations": [{"op":"Add","path":"name.formatted","value":"New Name"}] }' \ + --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json" +``` + +Returns an empty response with a `204` status code if successful. + +### Remove a single SCIM provisioned user + +Removes the user's SSO identity and group membership. + +```plaintext +DELETE /api/scim/v2/groups/:group_path/Users/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| ------------ | ------ | -------- | ------------------------- | +| `id` | string | yes | External UID of the user. | +| `group_path` | string | yes | Full path to the group. | + +Example request: + +```shell +curl --verbose --request DELETE "https://gitlab.example.com/api/scim/v2/groups/test_group/Users/f0b1d561c-21ff-4092-beab-8154b17f82f2" \ + --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json" +``` + +Returns an empty response with a `204` status code if successful. + +### Available filters + +They match an expression as specified in [the RFC7644 filtering section](https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2.2). + +| Filter | Description | +| ----- | ----------- | +| `eq` | The attribute matches exactly the specified value. | + +Example: + +```plaintext +id eq a-b-c-d +``` + +### Available operations + +They perform an operation as specified in [the RFC7644 update section](https://www.rfc-editor.org/rfc/rfc7644#section-3.5.2). + +| Operator | Description | +| ----- | ----------- | +| `Replace` | The attribute's value is updated. | +| `Add` | The attribute has a new value. | + +Example: + +```json +{ "op": "Add", "path": "name.formatted", "value": "New Name" } +``` diff --git a/doc/integration/datadog.md b/doc/integration/datadog.md index d4fe9f57aac..31e254658c1 100644 --- a/doc/integration/datadog.md +++ b/doc/integration/datadog.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index 4b596c12f1c..a3c206176b9 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/integration/gmail_action_buttons_for_gitlab.md b/doc/integration/gmail_action_buttons_for_gitlab.md index 7463f9659b5..42b89670a68 100644 --- a/doc/integration/gmail_action_buttons_for_gitlab.md +++ b/doc/integration/gmail_action_buttons_for_gitlab.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/integration/index.md b/doc/integration/index.md index 17fba380168..06a7620f477 100644 --- a/doc/integration/index.md +++ b/doc/integration/index.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments comments: false diff --git a/doc/integration/jenkins.md b/doc/integration/jenkins.md index d5620126468..8a438dde52e 100644 --- a/doc/integration/jenkins.md +++ b/doc/integration/jenkins.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/integration/jenkins_deprecated.md b/doc/integration/jenkins_deprecated.md index 418d476a0cb..53f7162402b 100644 --- a/doc/integration/jenkins_deprecated.md +++ b/doc/integration/jenkins_deprecated.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments remove_date: '2022-10-29' diff --git a/doc/integration/jira/configure.md b/doc/integration/jira/configure.md index f65b9133237..66339d5ec27 100644 --- a/doc/integration/jira/configure.md +++ b/doc/integration/jira/configure.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/integration/jira/connect-app.md b/doc/integration/jira/connect-app.md index 1a3082bc007..171c1cbe484 100644 --- a/doc/integration/jira/connect-app.md +++ b/doc/integration/jira/connect-app.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/integration/jira/development_panel.md b/doc/integration/jira/development_panel.md index 654608387d2..bdb79d65d5e 100644 --- a/doc/integration/jira/development_panel.md +++ b/doc/integration/jira/development_panel.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/integration/jira/dvcs.md b/doc/integration/jira/dvcs.md index b9f365617a7..f33536b7b91 100644 --- a/doc/integration/jira/dvcs.md +++ b/doc/integration/jira/dvcs.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/integration/jira/index.md b/doc/integration/jira/index.md index fb91289b8aa..3db897fbb94 100644 --- a/doc/integration/jira/index.md +++ b/doc/integration/jira/index.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/integration/jira/issues.md b/doc/integration/jira/issues.md index aab068193ad..3a5d8e66b2d 100644 --- a/doc/integration/jira/issues.md +++ b/doc/integration/jira/issues.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/integration/jira/jira_cloud_configuration.md b/doc/integration/jira/jira_cloud_configuration.md index 3360f2f12da..d47c84df5e5 100644 --- a/doc/integration/jira/jira_cloud_configuration.md +++ b/doc/integration/jira/jira_cloud_configuration.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/integration/jira/jira_server_configuration.md b/doc/integration/jira/jira_server_configuration.md index 96a25ccad78..42de883753c 100644 --- a/doc/integration/jira/jira_server_configuration.md +++ b/doc/integration/jira/jira_server_configuration.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/integration/slash_commands.md b/doc/integration/slash_commands.md index eaa8d2d1167..ff892f006a5 100644 --- a/doc/integration/slash_commands.md +++ b/doc/integration/slash_commands.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/integration/trello_power_up.md b/doc/integration/trello_power_up.md index c19a402f5a3..df3755dbf31 100644 --- a/doc/integration/trello_power_up.md +++ b/doc/integration/trello_power_up.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/subscriptions/self_managed/index.md b/doc/subscriptions/self_managed/index.md index 2149f846926..4538e8587c9 100644 --- a/doc/subscriptions/self_managed/index.md +++ b/doc/subscriptions/self_managed/index.md @@ -261,7 +261,7 @@ It also displays the following information: ## Export your license usage -> Introduced in GitLab 14.6. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66826) in GitLab 14.2. If you are an administrator, you can export your license usage into a CSV: diff --git a/doc/user/admin_area/settings/project_integration_management.md b/doc/user/admin_area/settings/project_integration_management.md index d50e02a14d4..5be9081d9b2 100644 --- a/doc/user/admin_area/settings/project_integration_management.md +++ b/doc/user/admin_area/settings/project_integration_management.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/group/saml_sso/scim_setup.md b/doc/user/group/saml_sso/scim_setup.md index 896d338603a..55990336a50 100644 --- a/doc/user/group/saml_sso/scim_setup.md +++ b/doc/user/group/saml_sso/scim_setup.md @@ -15,7 +15,7 @@ GitLab SAML SSO SCIM doesn't support updating users. When SCIM is enabled for a GitLab group, membership of that group is synchronized between GitLab and an identity provider. -The GitLab [SCIM API](../../../api/scim.md) implements part of [the RFC7644 protocol](https://www.rfc-editor.org/rfc/rfc7644). +The [internal GitLab SCIM API](../../../development/internal_api/index.md#scim-api) implements part of [the RFC7644 protocol](https://www.rfc-editor.org/rfc/rfc7644). ## Configure GitLab @@ -121,7 +121,7 @@ attributes and modify them accordingly. In particular, the `objectId` source att target attribute. If a mapping is not listed in the table, use the Azure Active Directory defaults. For a list of required attributes, -refer to the [SCIM API documentation](../../../api/scim.md). +refer to the [internal SCIM API](../../../development/internal_api/index.md#scim-api) documentation. ### Configure Okta diff --git a/doc/user/group/saml_sso/troubleshooting_scim.md b/doc/user/group/saml_sso/troubleshooting_scim.md index f98b6c61e11..6f8aed4b386 100644 --- a/doc/user/group/saml_sso/troubleshooting_scim.md +++ b/doc/user/group/saml_sso/troubleshooting_scim.md @@ -34,7 +34,7 @@ Administrators can use the Admin Area to [list SCIM identities for a user](../.. Group owners can see the list of users and the `externalId` stored for each user in the group SAML SSO Settings page. -A possible alternative is to use the [SCIM API](../../../api/scim.md#get-a-list-of-scim-provisioned-users) to manually retrieve the `externalId` we have stored for users, also called the `external_uid` or `NameId`. +A possible alternative is to use the [SCIM API](../../../api/scim.md) to manually retrieve the `externalId` we have stored for users, also called the `external_uid` or `NameId`. To see how the `external_uid` compares to the value returned as the SAML NameId, you can have the user use a [SAML Tracer](troubleshooting.md#saml-debugging-tools). @@ -53,7 +53,7 @@ you can address the problem in the following ways: - You can have users unlink and relink themselves, based on the ["SAML authentication failed: User has already been taken"](troubleshooting.md#message-saml-authentication-failed-user-has-already-been-taken) section. - You can unlink all users simultaneously, by removing all users from the SAML app while provisioning is turned on. -- It may be possible to use the [SCIM API](../../../api/scim.md#update-a-single-scim-provisioned-user) to manually correct the `externalId` stored for users to match the SAML `NameId`. +- Use the [SCIM API](../../../api/scim.md) to manually correct the `externalId` stored for users to match the SAML `NameId`. To look up a user, you need to know the desired value that matches the `NameId` as well as the current `externalId`. It is important not to update these to incorrect values, since this causes users to be unable to sign in. It is also important not to assign a value to the wrong user, as this causes users to get signed into the wrong account. @@ -71,11 +71,13 @@ Changing the SAML or SCIM configuration or provider can cause the following prob | Problem | Solution | | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | SAML and SCIM identity mismatch. | First [verify that the user's SAML NameId matches the SCIM externalId](#how-do-i-verify-users-saml-nameid-matches-the-scim-externalid) and then [update or fix the mismatched SCIM externalId and SAML NameId](#update-or-fix-mismatched-scim-externalid-and-saml-nameid). | -| SCIM identity mismatch between GitLab and the identity provider SCIM app. | You can confirm whether you're hitting the error because of your SCIM identity mismatch between your SCIM app and GitLab.com by using [SCIM API](../../../api/scim.md#update-a-single-scim-provisioned-user) which shows up in the `id` key and compares it with the user `externalId` in the SCIM app. You can use the same [SCIM API](../../../api/scim.md#update-a-single-scim-provisioned-user) to update the SCIM `id` for the user on GitLab.com. | +| SCIM identity mismatch between GitLab and the identity provider SCIM app. | You can confirm whether you're hitting the error because of your SCIM identity mismatch between your SCIM app and GitLab.com by using the [SCIM API](../../../api/scim.md) which shows up in the `id` key and compares it with the user `externalId` in the SCIM app. You can use the same [SCIM API](../../../api/scim.md) to update the SCIM `id` for the user on GitLab.com. | ## Search Rails logs for SCIM requests -GitLab.com administrators can search for SCIM requests in the `api_json.log` using the `pubsub-rails-inf-gprd-*` index in [Kibana](https://about.gitlab.com/handbook/support/workflows/kibana.html#using-kibana). Use the following filters based on the [SCIM API](../../../api/scim.md): +GitLab.com administrators can search for SCIM requests in the `api_json.log` using the `pubsub-rails-inf-gprd-*` index in +[Kibana](https://about.gitlab.com/handbook/support/workflows/kibana.html#using-kibana). Use the following filters based on the internal +[SCIM API](../../../development/internal_api/index.md#scim-api): - `json.path`: `/scim/v2/groups/<group-path>` - `json.params.value`: `<externalId>` diff --git a/doc/user/group/settings/group_access_tokens.md b/doc/user/group/settings/group_access_tokens.md index 7ed36f8ab1e..158e1654c6e 100644 --- a/doc/user/group/settings/group_access_tokens.md +++ b/doc/user/group/settings/group_access_tokens.md @@ -140,10 +140,10 @@ The scope determines the actions you can perform when you authenticate with a gr |:-------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `api` | Grants complete read and write access to the scoped group and related project API, including the [Package Registry](../../packages/package_registry/index.md). | | `read_api` | Grants read access to the scoped group and related project API, including the [Package Registry](../../packages/package_registry/index.md). | -| `read_registry` | Allows read access (pull) to the [Container Registry](../../packages/container_registry/index.md) images if any project within a group is private and authorization is required. | -| `write_registry` | Allows write access (push) to the [Container Registry](../../packages/container_registry/index.md). | -| `read_repository` | Allows read access (pull) to all repositories within a group. | -| `write_repository` | Allows read and write access (pull and push) to all repositories within a group. | +| `read_registry` | Grants read access (pull) to the [Container Registry](../../packages/container_registry/index.md) images if any project within a group is private and authorization is required. | +| `write_registry` | Grants write access (push) to the [Container Registry](../../packages/container_registry/index.md). | +| `read_repository` | Grants read access (pull) to all repositories within a group. | +| `write_repository` | Grants read and write access (pull and push) to all repositories within a group. | ## Enable or disable group access token creation diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index f38fb6dd30d..ade5f912c93 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -317,6 +317,9 @@ GitLab tracks user contribution activity. You can follow or unfollow other users - The small popover that appears when you hover over a user's name ([introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76050) in GitLab 15.0). +In [GitLab 15.5 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/360755), +the maximum number of users you can follow is 300. + To view a user's activity in a top-level Activity view: 1. From a user's profile, select **Follow**. diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index 29dce202a37..c7fe68c0609 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -105,14 +105,14 @@ A personal access token can perform actions based on the assigned scopes. | Scope | Access | |--------------------|--------| -| `api` | Read-write for the complete API, including all groups and projects, the Container Registry, and the Package Registry. | -| `read_user` | Read-only for endpoints under `/users`. Essentially, access to any of the `GET` requests in the [Users API](../../api/users.md). | -| `read_api` | Read-only for the complete API, including all groups and projects, the Container Registry, and the Package Registry. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28944) in GitLab 12.10.) | -| `read_repository` | Read-only (pull) for the repository through `git clone`. | -| `write_repository` | Read-write (pull, push) for the repository through `git clone`. | -| `read_registry` | Read-only (pull) for [Container Registry](../packages/container_registry/index.md) images if a project is private and authorization is required. Available only when the Container Registry is enabled. | -| `write_registry` | Read-write (push) for [Container Registry](../packages/container_registry/index.md) images if a project is private and authorization is required. Available only when the Container Registry is enabled. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28958) in GitLab 12.10.) | -| `sudo` | API actions as any user in the system (if the authenticated user is an administrator). | +| `api` | Grants complete read/write access to the API, including all groups and projects, the container registry, and the package registry. | +| `read_user` | Grants read-only access to the authenticated user's profile through the `/user` API endpoint, which includes username, public email, and full name. Also grants access to read-only API endpoints under [`/users`](../../api/users.md). | +| `read_api` | Grants read access to the API, including all groups and projects, the container registry, and the package registry. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28944) in GitLab 12.10.) | +| `read_repository` | Grants read-only access to repositories on private projects using Git-over-HTTP or the Repository Files API. | +| `write_repository` | Grants read-write access to repositories on private projects using Git-over-HTTP (not using the API). | +| `read_registry` | Grants read-only (pull) access to a [Container Registry](../packages/container_registry/index.md) images if a project is private and authorization is required. Available only when the Container Registry is enabled. | +| `write_registry` | Grants read-write (push) access to a [Container Registry](../packages/container_registry/index.md) images if a project is private and authorization is required. Available only when the Container Registry is enabled. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28958) in GitLab 12.10.) | +| `sudo` | Grants permission to perform API actions as any user in the system, when authenticated as an administrator. | ## When personal access tokens expire diff --git a/doc/user/project/deploy_tokens/index.md b/doc/user/project/deploy_tokens/index.md index 1ee0b8fbe73..0fd0d05daa2 100644 --- a/doc/user/project/deploy_tokens/index.md +++ b/doc/user/project/deploy_tokens/index.md @@ -68,11 +68,11 @@ following table along with GitLab version it was introduced in: | Scope | Description | Introduced in GitLab Version | |--------------------------|-------------|------------------------------| -| `read_repository` | Allows read-access to the repository through `git clone` | -- | -| `read_registry` | Allows read-access to [container registry](../../packages/container_registry/index.md) images if a project is private and authorization is required. | -- | -| `write_registry` | Allows write-access (push) to [container registry](../../packages/container_registry/index.md). | 12.10 | -| `read_package_registry` | Allows read access to the package registry. | 13.0 | -| `write_package_registry` | Allows write access to the package registry. | 13.0 | +| `read_repository` | Grants read-access to the repository through `git clone` | -- | +| `read_registry` | Grants read-access to [container registry](../../packages/container_registry/index.md) images if a project is private and authorization is required. | -- | +| `write_registry` | Grants write-access (push) to [container registry](../../packages/container_registry/index.md). | 12.10 | +| `read_package_registry` | Grants read access to the package registry. | 13.0 | +| `write_package_registry` | Grants write access to the package registry. | 13.0 | ## Deploy token custom username diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md index 0e556de515f..0fc49798a15 100644 --- a/doc/user/project/import/github.md +++ b/doc/user/project/import/github.md @@ -204,7 +204,7 @@ defaults to the default project visibility. Supported GitHub branch protection rules are mapped to GitLab branch protection rules or project-wide GitLab settings when they are imported: - GitHub rule **Require conversation resolution before merging** for the project's default branch is mapped to the [**All threads must be resolved** GitLab setting](../../discussions/index.md#prevent-merge-unless-all-threads-are-resolved). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/371110) in GitLab 15.5. -- Support for GitHub rule **Require a pull request before merging** is proposed in issue [370951](https://gitlab.com/gitlab-org/gitlab/-/issues/370951). +- GitHub rule **Require a pull request before merging** is mapped to the **No one** option in the **Allowed to push** list of the branch protection rule. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/370951) in GitLab 15.5. - GitHub rule **Require signed commits** for the project's default branch is mapped to the **Reject unsigned commits** GitLab setting. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/370949) in GitLab 15.5. - Support for GitHub rule **Require status checks to pass before merging** was proposed in issue [370948](https://gitlab.com/gitlab-org/gitlab/-/issues/370948). However, this rule cannot be translated during project import into GitLab due to technical difficulties. You can still create [status checks](../merge_requests/status_checks.md) in GitLab yourself. diff --git a/doc/user/project/integrations/asana.md b/doc/user/project/integrations/asana.md index 17fd87c9b4a..97fb4e7c463 100644 --- a/doc/user/project/integrations/asana.md +++ b/doc/user/project/integrations/asana.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/bamboo.md b/doc/user/project/integrations/bamboo.md index ec886a71b13..fceec006a1a 100644 --- a/doc/user/project/integrations/bamboo.md +++ b/doc/user/project/integrations/bamboo.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/bugzilla.md b/doc/user/project/integrations/bugzilla.md index d35e30f73b5..9221250e17f 100644 --- a/doc/user/project/integrations/bugzilla.md +++ b/doc/user/project/integrations/bugzilla.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/custom_issue_tracker.md b/doc/user/project/integrations/custom_issue_tracker.md index 3a4ce00982c..24a2e3d1ebc 100644 --- a/doc/user/project/integrations/custom_issue_tracker.md +++ b/doc/user/project/integrations/custom_issue_tracker.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/discord_notifications.md b/doc/user/project/integrations/discord_notifications.md index 2be5ba93267..9439e480484 100644 --- a/doc/user/project/integrations/discord_notifications.md +++ b/doc/user/project/integrations/discord_notifications.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/emails_on_push.md b/doc/user/project/integrations/emails_on_push.md index 1a399432bb6..ff255cbba51 100644 --- a/doc/user/project/integrations/emails_on_push.md +++ b/doc/user/project/integrations/emails_on_push.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/ewm.md b/doc/user/project/integrations/ewm.md index 9461caaba92..d972509c0f6 100644 --- a/doc/user/project/integrations/ewm.md +++ b/doc/user/project/integrations/ewm.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/github.md b/doc/user/project/integrations/github.md index 32e3239e8b4..603ed8b4c05 100644 --- a/doc/user/project/integrations/github.md +++ b/doc/user/project/integrations/github.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/gitlab_slack_application.md b/doc/user/project/integrations/gitlab_slack_application.md index 3693ee61d12..07c99653a0e 100644 --- a/doc/user/project/integrations/gitlab_slack_application.md +++ b/doc/user/project/integrations/gitlab_slack_application.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/hangouts_chat.md b/doc/user/project/integrations/hangouts_chat.md index 18d06672195..1be0db223ac 100644 --- a/doc/user/project/integrations/hangouts_chat.md +++ b/doc/user/project/integrations/hangouts_chat.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/harbor.md b/doc/user/project/integrations/harbor.md index a9729ceef6b..259b91fc1c7 100644 --- a/doc/user/project/integrations/harbor.md +++ b/doc/user/project/integrations/harbor.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/index.md b/doc/user/project/integrations/index.md index 42095a204d6..77444570499 100644 --- a/doc/user/project/integrations/index.md +++ b/doc/user/project/integrations/index.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/irker.md b/doc/user/project/integrations/irker.md index 66fff358eed..70f48e4647a 100644 --- a/doc/user/project/integrations/irker.md +++ b/doc/user/project/integrations/irker.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md index c9bf2721c57..39b89cd87a9 100644 --- a/doc/user/project/integrations/mattermost.md +++ b/doc/user/project/integrations/mattermost.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/mattermost_slash_commands.md b/doc/user/project/integrations/mattermost_slash_commands.md index fa36efdf654..192360f5440 100644 --- a/doc/user/project/integrations/mattermost_slash_commands.md +++ b/doc/user/project/integrations/mattermost_slash_commands.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/microsoft_teams.md b/doc/user/project/integrations/microsoft_teams.md index ab755d3d444..cedb5af144f 100644 --- a/doc/user/project/integrations/microsoft_teams.md +++ b/doc/user/project/integrations/microsoft_teams.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/mock_ci.md b/doc/user/project/integrations/mock_ci.md index 2fc8edac6a2..64ee4521ce4 100644 --- a/doc/user/project/integrations/mock_ci.md +++ b/doc/user/project/integrations/mock_ci.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/pipeline_status_emails.md b/doc/user/project/integrations/pipeline_status_emails.md index b542224b888..009bb6662ff 100644 --- a/doc/user/project/integrations/pipeline_status_emails.md +++ b/doc/user/project/integrations/pipeline_status_emails.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/pivotal_tracker.md b/doc/user/project/integrations/pivotal_tracker.md index b100f8cc687..79a00725470 100644 --- a/doc/user/project/integrations/pivotal_tracker.md +++ b/doc/user/project/integrations/pivotal_tracker.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/pumble.md b/doc/user/project/integrations/pumble.md index 80c8fed290b..f9c0c79be1b 100644 --- a/doc/user/project/integrations/pumble.md +++ b/doc/user/project/integrations/pumble.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/redmine.md b/doc/user/project/integrations/redmine.md index 15f59dc8a0a..e9752d7ce6c 100644 --- a/doc/user/project/integrations/redmine.md +++ b/doc/user/project/integrations/redmine.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/servicenow.md b/doc/user/project/integrations/servicenow.md index 96a9e4e0441..d528d1a5547 100644 --- a/doc/user/project/integrations/servicenow.md +++ b/doc/user/project/integrations/servicenow.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/shimo.md b/doc/user/project/integrations/shimo.md index 550a5c10059..28cb53f8bf6 100644 --- a/doc/user/project/integrations/shimo.md +++ b/doc/user/project/integrations/shimo.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md index 8df199ee58a..9fe0c76ec4f 100644 --- a/doc/user/project/integrations/slack.md +++ b/doc/user/project/integrations/slack.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/slack_slash_commands.md b/doc/user/project/integrations/slack_slash_commands.md index 5687d126531..cb698ac0ee0 100644 --- a/doc/user/project/integrations/slack_slash_commands.md +++ b/doc/user/project/integrations/slack_slash_commands.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/unify_circuit.md b/doc/user/project/integrations/unify_circuit.md index e3b3fc5966b..c13f642d9e9 100644 --- a/doc/user/project/integrations/unify_circuit.md +++ b/doc/user/project/integrations/unify_circuit.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/webex_teams.md b/doc/user/project/integrations/webex_teams.md index 161df09b33b..930ca8e99b8 100644 --- a/doc/user/project/integrations/webex_teams.md +++ b/doc/user/project/integrations/webex_teams.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/webhook_events.md b/doc/user/project/integrations/webhook_events.md index c4e1eafcbf6..bd5fcde71b3 100644 --- a/doc/user/project/integrations/webhook_events.md +++ b/doc/user/project/integrations/webhook_events.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index e703585b6d2..9fc9d6e2eda 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/youtrack.md b/doc/user/project/integrations/youtrack.md index 0d34756eee2..fb6807aeeb0 100644 --- a/doc/user/project/integrations/youtrack.md +++ b/doc/user/project/integrations/youtrack.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/zentao.md b/doc/user/project/integrations/zentao.md index 41400bd1516..17727ba22b1 100644 --- a/doc/user/project/integrations/zentao.md +++ b/doc/user/project/integrations/zentao.md @@ -1,5 +1,5 @@ --- -stage: Ecosystem +stage: Manage group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/pages/public_folder.md b/doc/user/project/pages/public_folder.md index 1ec467c2ac5..b5d498402d5 100644 --- a/doc/user/project/pages/public_folder.md +++ b/doc/user/project/pages/public_folder.md @@ -51,7 +51,9 @@ rename that folder to a collision-free alternative first: ```javascript // astro.config.mjs - export default { + import { defineConfig } from 'astro/config'; + + export default defineConfig({ // GitLab Pages requires exposed files to be located in a folder called "public". // So we're instructing Astro to put the static build output in a folder of that name. outDir: 'public', @@ -60,7 +62,7 @@ rename that folder to a collision-free alternative first: // for the build output. So in deviation from the defaults we're using a folder // called `static` instead. publicDir: 'static', - }; + }); ``` ### SvelteKit diff --git a/doc/user/project/settings/project_access_tokens.md b/doc/user/project/settings/project_access_tokens.md index c1652251b9f..f27672a1b07 100644 --- a/doc/user/project/settings/project_access_tokens.md +++ b/doc/user/project/settings/project_access_tokens.md @@ -85,10 +85,10 @@ The scope determines the actions you can perform when you authenticate with a pr |:-------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------| | `api` | Grants complete read and write access to the scoped project API, including the [Package Registry](../../packages/package_registry/index.md). | | `read_api` | Grants read access to the scoped project API, including the [Package Registry](../../packages/package_registry/index.md). | -| `read_registry` | Allows read access (pull) to the [Container Registry](../../packages/container_registry/index.md) images if a project is private and authorization is required. | -| `write_registry` | Allows write access (push) to the [Container Registry](../../packages/container_registry/index.md). | -| `read_repository` | Allows read access (pull) to the repository. | -| `write_repository` | Allows read and write access (pull and push) to the repository. | +| `read_registry` | Grants read access (pull) to the [Container Registry](../../packages/container_registry/index.md) images if a project is private and authorization is required. | +| `write_registry` | Grants write access (push) to the [Container Registry](../../packages/container_registry/index.md). | +| `read_repository` | Grants read access (pull) to the repository. | +| `write_repository` | Grants read and write access (pull and push) to the repository. | ## Enable or disable project access token creation diff --git a/lib/api/users.rb b/lib/api/users.rb index 3a2bbc3fb32..0654c6540f6 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -189,7 +189,10 @@ module API user = find_user(params[:id]) not_found!('User') unless user - if current_user.follow(user) + followee = current_user.follow(user) + if followee&.errors&.any? + render_api_error!(followee.errors.full_messages.join(', '), 400) + elsif followee&.persisted? present user, with: Entities::UserBasic else not_modified! diff --git a/lib/gitlab/ci/parsers/security/concerns/deprecated_syntax.rb b/lib/gitlab/ci/parsers/security/concerns/deprecated_syntax.rb deleted file mode 100644 index 24613a441be..00000000000 --- a/lib/gitlab/ci/parsers/security/concerns/deprecated_syntax.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - module Parsers - module Security - module Concerns - module DeprecatedSyntax - extend ActiveSupport::Concern - - included do - extend ::Gitlab::Utils::Override - - override :parse_report - end - - def report_data - @report_data ||= begin - data = super - - if data.is_a?(Array) - data = { - "version" => self.class::DEPRECATED_REPORT_VERSION, - "vulnerabilities" => data - } - end - - data - end - end - end - end - end - end - end -end diff --git a/lib/gitlab/ci/parsers/security/sast.rb b/lib/gitlab/ci/parsers/security/sast.rb index e3c62614cd8..3d999f20f1e 100644 --- a/lib/gitlab/ci/parsers/security/sast.rb +++ b/lib/gitlab/ci/parsers/security/sast.rb @@ -5,10 +5,6 @@ module Gitlab module Parsers module Security class Sast < Common - include Security::Concerns::DeprecatedSyntax - - DEPRECATED_REPORT_VERSION = "1.2" - private def create_location(location_data) diff --git a/lib/gitlab/ci/parsers/security/secret_detection.rb b/lib/gitlab/ci/parsers/security/secret_detection.rb index c6d95c1d391..175731b6b64 100644 --- a/lib/gitlab/ci/parsers/security/secret_detection.rb +++ b/lib/gitlab/ci/parsers/security/secret_detection.rb @@ -5,10 +5,6 @@ module Gitlab module Parsers module Security class SecretDetection < Common - include Security::Concerns::DeprecatedSyntax - - DEPRECATED_REPORT_VERSION = "1.2" - private def create_location(location_data) diff --git a/lib/gitlab/github_import/importer/note_attachments_importer.rb b/lib/gitlab/github_import/importer/note_attachments_importer.rb index c0ea7e109ef..ce785c3e776 100644 --- a/lib/gitlab/github_import/importer/note_attachments_importer.rb +++ b/lib/gitlab/github_import/importer/note_attachments_importer.rb @@ -15,12 +15,12 @@ module Gitlab end def execute - attachment_urls = MarkdownText.fetch_attachment_urls(note_text.text) - return if attachment_urls.blank? + attachments = MarkdownText.fetch_attachments(note_text.text) + return if attachments.blank? - new_text = attachment_urls.reduce(note_text.text) do |text, url| - new_url = download_attachment(url) - text.gsub(url, new_url) + new_text = attachments.reduce(note_text.text) do |text, attachment| + new_url = download_attachment(attachment) + text.gsub(attachment.url, new_url) end update_note_record(new_text) @@ -28,32 +28,17 @@ module Gitlab private - # in: github attachment markdown url + # in: an instance of Gitlab::GithubImport::Markdown::Attachment # out: gitlab attachment markdown url - def download_attachment(markdown_url) - url = extract_url_from_markdown(markdown_url) - name_prefix = extract_name_from_markdown(markdown_url) - - downloader = ::Gitlab::GithubImport::AttachmentsDownloader.new(url) + def download_attachment(attachment) + downloader = ::Gitlab::GithubImport::AttachmentsDownloader.new(attachment.url) file = downloader.perform uploader = UploadService.new(project, file, FileUploader).execute - "#{name_prefix}(#{uploader.to_h[:url]})" + uploader.to_h[:url] ensure downloader&.delete end - # in: "![image-icon](https://user-images.githubusercontent.com/..)" - # out: https://user-images.githubusercontent.com/.. - def extract_url_from_markdown(text) - text.match(%r{https://.*\)$}).to_a[0].chop - end - - # in: "![image-icon](https://user-images.githubusercontent.com/..)" - # out: ![image-icon] - def extract_name_from_markdown(text) - text.match(%r{^!?\[.*\]}).to_a[0] - end - def update_note_record(text) case note_text.record_type when ::Release.name diff --git a/lib/gitlab/github_import/importer/protected_branch_importer.rb b/lib/gitlab/github_import/importer/protected_branch_importer.rb index ac80f40c8bc..21075e21e1d 100644 --- a/lib/gitlab/github_import/importer/protected_branch_importer.rb +++ b/lib/gitlab/github_import/importer/protected_branch_importer.rb @@ -6,6 +6,10 @@ module Gitlab class ProtectedBranchImporter attr_reader :protected_branch, :project, :client + # By default on GitHub, both developers and maintainers can merge + # a PR into the protected branch + GITHUB_DEFAULT_MERGE_ACCESS_LEVEL = Gitlab::Access::DEVELOPER + # protected_branch - An instance of # `Gitlab::GithubImport::Representation::ProtectedBranch`. # project - An instance of `Project` @@ -31,8 +35,8 @@ module Gitlab def params { name: protected_branch.id, - push_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }], - merge_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }], + push_access_levels_attributes: [{ access_level: push_access_level }], + merge_access_levels_attributes: [{ access_level: merge_access_level }], allow_force_push: allow_force_push? } end @@ -68,6 +72,71 @@ module Gitlab push_rule.update!(reject_unsigned_commits: true) project.project_setting.update!(push_rule_id: push_rule.id) end + + def push_access_level + if protected_branch.required_pull_request_reviews + Gitlab::Access::NO_ACCESS + else + gitlab_access_level_for(:push) + end + end + + # Gets the strictest merge_access_level between GitHub and GitLab + def merge_access_level + gitlab_access = gitlab_access_level_for(:merge) + + return gitlab_access if gitlab_access == Gitlab::Access::NO_ACCESS + + [gitlab_access, GITHUB_DEFAULT_MERGE_ACCESS_LEVEL].max + end + + # action - :push/:merge + def gitlab_access_level_for(action) + if default_branch? + action == :push ? default_branch_push_access_level : default_branch_merge_access_level + elsif protected_on_gitlab? + non_default_branch_access_level_for(action) + else + gitlab_default_access_level_for(action) + end + end + + def default_branch_push_access_level + if default_branch_protection.developer_can_push? + Gitlab::Access::DEVELOPER + else + gitlab_default_access_level_for(:push) + end + end + + def default_branch_merge_access_level + if default_branch_protection.developer_can_merge? + Gitlab::Access::DEVELOPER + else + gitlab_default_access_level_for(:merge) + end + end + + def default_branch_protection + Gitlab::Access::BranchProtection.new(project.namespace.default_branch_protection) + end + + def protected_on_gitlab? + ProtectedBranch.protected?(project, protected_branch.id) + end + + def non_default_branch_access_level_for(action) + access_level = ProtectedBranch.access_levels_for_ref(protected_branch.id, action: action) + .find(&:role?)&.access_level + + access_level || gitlab_default_access_level_for(action) + end + + def gitlab_default_access_level_for(action) + return ProtectedBranch::PushAccessLevel::GITLAB_DEFAULT_ACCESS_LEVEL if action == :push + + ProtectedBranch::MergeAccessLevel::GITLAB_DEFAULT_ACCESS_LEVEL + end end end end diff --git a/lib/gitlab/github_import/markdown/attachment.rb b/lib/gitlab/github_import/markdown/attachment.rb new file mode 100644 index 00000000000..a5cf5ffa60e --- /dev/null +++ b/lib/gitlab/github_import/markdown/attachment.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +module Gitlab + module GithubImport + module Markdown + class Attachment + MEDIA_TYPES = %w[gif jpeg jpg mov mp4 png svg webm].freeze + DOC_TYPES = %w[ + csv docx fodg fodp fods fodt gz log md odf odg odp ods + odt pdf pptx tgz txt xls xlsx zip + ].freeze + + class << self + # markdown_node - CommonMarker::Node + def from_markdown(markdown_node) + case markdown_node.type + when :html, :inline_html + from_inline_html(markdown_node) + when :image + from_markdown_image(markdown_node) + when :link + from_markdown_link(markdown_node) + end + end + + private + + def from_markdown_image(markdown_node) + url = markdown_node.url + + return unless github_url?(url, media: true) + return unless whitelisted_type?(url, media: true) + + new(markdown_node.to_plaintext.strip, url) + end + + def from_markdown_link(markdown_node) + url = markdown_node.url + + return unless github_url?(url, docs: true) + return unless whitelisted_type?(url, docs: true) + + new(markdown_node.to_plaintext.strip, url) + end + + def from_inline_html(markdown_node) + img = Nokogiri::HTML.parse(markdown_node.string_content).xpath('//img')[0] + + return unless img + return unless github_url?(img[:src], media: true) + return unless whitelisted_type?(img[:src], media: true) + + new(img[:alt], img[:src]) + end + + def github_url?(url, docs: false, media: false) + if media + url.start_with?(::Gitlab::GithubImport::MarkdownText::GITHUB_MEDIA_CDN) + elsif docs + url.start_with?(::Gitlab::GithubImport::MarkdownText.github_url) + end + end + + def whitelisted_type?(url, docs: false, media: false) + if media + MEDIA_TYPES.any? { |type| url.end_with?(type) } + elsif docs + DOC_TYPES.any? { |type| url.end_with?(type) } + end + end + end + + attr_reader :name, :url + + def initialize(name, url) + @name = name + @url = url + end + + def inspect + "<#{self.class.name}: { name: #{name}, url: #{url} }>" + end + end + end + end +end diff --git a/lib/gitlab/github_import/markdown_text.rb b/lib/gitlab/github_import/markdown_text.rb index bf2856bc77f..2424b3e8c1f 100644 --- a/lib/gitlab/github_import/markdown_text.rb +++ b/lib/gitlab/github_import/markdown_text.rb @@ -8,23 +8,12 @@ module Gitlab class MarkdownText include Gitlab::EncodingHelper - ISSUE_REF_MATCHER = '%{github_url}/%{import_source}/issues' - PULL_REF_MATCHER = '%{github_url}/%{import_source}/pull' - - MEDIA_TYPES = %w[gif jpeg jpg mov mp4 png svg webm].freeze - DOC_TYPES = %w[ - csv docx fodg fodp fods fodt gz log md odf odg odp ods - odt pdf pptx tgz txt xls xlsx zip - ].freeze - ALL_TYPES = (MEDIA_TYPES + DOC_TYPES).freeze - # On github.com we have base url for docs and CDN url for media. # On github EE as far as we can know there is no CDN urls and media is placed on base url. - # To no escape the escaping symbol we use single quotes instead of double with interpolation. - # rubocop:disable Style/StringConcatenation - CDN_URL_MATCHER = '(!\[.+\]\(%{github_media_cdn}/\d+/(\w|-)+\.(' + MEDIA_TYPES.join('|') + ')\))' - BASE_URL_MATCHER = '(\[.+\]\(%{github_url}/.+/.+/files/\d+/.+\.(' + ALL_TYPES.join('|') + ')\))' - # rubocop:enable Style/StringConcatenation + GITHUB_MEDIA_CDN = 'https://user-images.githubusercontent.com' + + ISSUE_REF_MATCHER = '%{github_url}/%{import_source}/issues' + PULL_REF_MATCHER = '%{github_url}/%{import_source}/pull' class << self def format(*args) @@ -42,20 +31,6 @@ module Gitlab .gsub(pull_ref_matcher, url_helpers.project_merge_requests_url(project)) end - def fetch_attachment_urls(text) - cdn_url_matcher = CDN_URL_MATCHER % { github_media_cdn: Regexp.escape(github_media_cdn) } - doc_url_matcher = BASE_URL_MATCHER % { github_url: Regexp.escape(github_url) } - - text.scan(Regexp.new(cdn_url_matcher)).map(&:first) + - text.scan(Regexp.new(doc_url_matcher)).map(&:first) - end - - private - - def github_media_cdn - 'https://user-images.githubusercontent.com' - end - # Returns github domain without slash in the end def github_url oauth_config = Gitlab::Auth::OAuth::Provider.config_for('github') || {} @@ -63,6 +38,23 @@ module Gitlab url = url.chop if url.end_with?('/') url end + + def fetch_attachments(text) + attachments = [] + doc = CommonMarker.render_doc(text) + + doc.walk do |node| + attachment = extract_attachment(node) + attachments << attachment if attachment + end + attachments + end + + private + + def extract_attachment(node) + ::Gitlab::GithubImport::Markdown::Attachment.from_markdown(node) + end end # text - The Markdown text as a String. diff --git a/lib/gitlab/github_import/representation/protected_branch.rb b/lib/gitlab/github_import/representation/protected_branch.rb index 4795e67e011..07a607ae70d 100644 --- a/lib/gitlab/github_import/representation/protected_branch.rb +++ b/lib/gitlab/github_import/representation/protected_branch.rb @@ -9,12 +9,13 @@ module Gitlab attr_reader :attributes - expose_attribute :id, :allow_force_pushes, :required_conversation_resolution, :required_signatures + expose_attribute :id, :allow_force_pushes, :required_conversation_resolution, :required_signatures, + :required_pull_request_reviews # Builds a Branch Protection info from a GitHub API response. # Resource structure details: # https://docs.github.com/en/rest/branches/branch-protection#get-branch-protection - # branch_protection - An instance of `Sawyer::Resource` containing the protection details. + # branch_protection - An instance of `Hash` containing the protection details. def self.from_api_response(branch_protection, _additional_object_data = {}) branch_name = branch_protection[:url].match(%r{/branches/(\S{1,255})/protection$})[1] @@ -22,7 +23,8 @@ module Gitlab id: branch_name, allow_force_pushes: branch_protection.dig(:allow_force_pushes, :enabled), required_conversation_resolution: branch_protection.dig(:required_conversation_resolution, :enabled), - required_signatures: branch_protection.dig(:required_signatures, :enabled) + required_signatures: branch_protection.dig(:required_signatures, :enabled), + required_pull_request_reviews: branch_protection[:required_pull_request_reviews].present? } new(hash) diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb index 5b9216c0914..a2d06b7f5b3 100644 --- a/lib/gitlab/i18n.rb +++ b/lib/gitlab/i18n.rb @@ -44,28 +44,28 @@ module Gitlab TRANSLATION_LEVELS = { 'bg' => 0, 'cs_CZ' => 0, - 'da_DK' => 38, + 'da_DK' => 37, 'de' => 17, 'en' => 100, 'eo' => 0, - 'es' => 37, + 'es' => 36, 'fil_PH' => 0, - 'fr' => 11, + 'fr' => 72, 'gl_ES' => 0, 'id_ID' => 0, 'it' => 1, 'ja' => 31, - 'ko' => 17, - 'nb_NO' => 26, + 'ko' => 20, + 'nb_NO' => 25, 'nl_NL' => 0, - 'pl_PL' => 4, - 'pt_BR' => 56, + 'pl_PL' => 3, + 'pt_BR' => 57, 'ro_RO' => 99, - 'ru' => 27, - 'si_LK' => 10, + 'ru' => 26, + 'si_LK' => 11, 'tr_TR' => 11, - 'uk' => 50, - 'zh_CN' => 97, + 'uk' => 49, + 'zh_CN' => 98, 'zh_HK' => 1, 'zh_TW' => 99 }.freeze diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e9413406d7d..6b21e82df41 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -45635,6 +45635,9 @@ msgstr "" msgid "WorkItem|Something went wrong when deleting the task. Please try again." msgstr "" +msgid "WorkItem|Something went wrong when fetching labels. Please try again." +msgstr "" + msgid "WorkItem|Something went wrong when fetching tasks. Please refresh this page." msgstr "" @@ -46047,6 +46050,9 @@ msgstr "" msgid "You can't add any more, but you can manage your existing members, for example, by removing inactive members and replacing them with new members. To get more members an owner of the group can start a trial or upgrade to a paid tier." msgstr "" +msgid "You can't follow more than %{limit} users. To follow more users, unfollow some others." +msgstr "" + msgid "You cannot %{action} %{state} users." msgstr "" diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh index a50db8b6f5d..0fc245a409f 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -235,16 +235,21 @@ function create_application_secret() { } function download_chart() { - echoinfo "Downloading the GitLab chart..." true + # If the requirements.lock is present, it means we got everything we need from the cache. + if [[ -f "gitlab-${GITLAB_HELM_CHART_REF}/requirements.lock" ]]; then + echosuccess "Downloading/Building chart dependencies skipped. Using the chart ${gitlab-${GITLAB_HELM_CHART_REF}} local folder'..." + else + echoinfo "Downloading the GitLab chart..." true - curl --location -o gitlab.tar.bz2 "https://gitlab.com/gitlab-org/charts/gitlab/-/archive/${GITLAB_HELM_CHART_REF}/gitlab-${GITLAB_HELM_CHART_REF}.tar.bz2" - tar -xjf gitlab.tar.bz2 + curl --location -o gitlab.tar.bz2 "https://gitlab.com/gitlab-org/charts/gitlab/-/archive/${GITLAB_HELM_CHART_REF}/gitlab-${GITLAB_HELM_CHART_REF}.tar.bz2" + tar -xjf gitlab.tar.bz2 - echoinfo "Adding the gitlab repo to Helm..." - helm repo add gitlab https://charts.gitlab.io + echoinfo "Adding the gitlab repo to Helm..." + helm repo add gitlab https://charts.gitlab.io - echoinfo "Building the gitlab chart's dependencies..." - helm dependency build "gitlab-${GITLAB_HELM_CHART_REF}" + echoinfo "Building the gitlab chart's dependencies..." + helm dependency build "gitlab-${GITLAB_HELM_CHART_REF}" + fi } function base_config_changed() { diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb index 20306fd8bdc..304d77e8521 100644 --- a/spec/factories/ci/job_artifacts.rb +++ b/spec/factories/ci/job_artifacts.rb @@ -393,16 +393,6 @@ FactoryBot.define do end end - trait :sast_deprecated do - file_type { :sast } - file_format { :raw } - - after(:build) do |artifact, _| - artifact.file = fixture_file_upload( - Rails.root.join('spec/fixtures/security_reports/deprecated/gl-sast-report.json'), 'application/json') - end - end - trait :sast_with_corrupted_data do file_type { :sast } file_format { :raw } diff --git a/spec/fixtures/security_reports/deprecated/gl-sast-report.json b/spec/fixtures/security_reports/deprecated/gl-sast-report.json deleted file mode 100644 index c5b0148fe3e..00000000000 --- a/spec/fixtures/security_reports/deprecated/gl-sast-report.json +++ /dev/null @@ -1,964 +0,0 @@ -[ - { - "category": "sast", - "message": "Probable insecure usage of temp file/directory.", - "cve": "python/hardcoded/hardcoded-tmp.py:52865813c884a507be1f152d654245af34aba8a391626d01f1ab6d3f52ec8779:B108", - "severity": "Medium", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-tmp.py", - "start_line": 1, - "end_line": 1 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B108", - "value": "B108", - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" - } - ], - "priority": "Medium", - "file": "python/hardcoded/hardcoded-tmp.py", - "line": 1, - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html", - "tool": "bandit" - }, - { - "category": "sast", - "name": "Predictable pseudorandom number generator", - "message": "Predictable pseudorandom number generator", - "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:47:PREDICTABLE_RANDOM", - "severity": "Medium", - "confidence": "Medium", - "scanner": { - "id": "find_sec_bugs", - "name": "Find Security Bugs" - }, - "location": { - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "start_line": 47, - "end_line": 47, - "class": "com.gitlab.security_products.tests.App", - "method": "generateSecretToken2" - }, - "identifiers": [ - { - "type": "find_sec_bugs_type", - "name": "Find Security Bugs-PREDICTABLE_RANDOM", - "value": "PREDICTABLE_RANDOM", - "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM" - } - ], - "priority": "Medium", - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "line": 47, - "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM", - "tool": "find_sec_bugs" - }, - { - "category": "sast", - "name": "Predictable pseudorandom number generator", - "message": "Predictable pseudorandom number generator", - "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:41:PREDICTABLE_RANDOM", - "severity": "Medium", - "confidence": "Medium", - "scanner": { - "id": "find_sec_bugs", - "name": "Find Security Bugs" - }, - "location": { - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "start_line": 41, - "end_line": 41, - "class": "com.gitlab.security_products.tests.App", - "method": "generateSecretToken1" - }, - "identifiers": [ - { - "type": "find_sec_bugs_type", - "name": "Find Security Bugs-PREDICTABLE_RANDOM", - "value": "PREDICTABLE_RANDOM", - "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM" - } - ], - "priority": "Medium", - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "line": 41, - "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM", - "tool": "find_sec_bugs" - }, - { - "category": "sast", - "message": "Use of insecure MD2, MD4, or MD5 hash function.", - "cve": "python/imports/imports-aliases.py:cb203b465dffb0cb3a8e8bd8910b84b93b0a5995a938e4b903dbb0cd6ffa1254:B303", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 11, - "end_line": 11 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B303", - "value": "B303" - } - ], - "priority": "Medium", - "file": "python/imports/imports-aliases.py", - "line": 11, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Use of insecure MD2, MD4, or MD5 hash function.", - "cve": "python/imports/imports-aliases.py:a7173c43ae66bd07466632d819d450e0071e02dbf782763640d1092981f9631b:B303", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 12, - "end_line": 12 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B303", - "value": "B303" - } - ], - "priority": "Medium", - "file": "python/imports/imports-aliases.py", - "line": 12, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Use of insecure MD2, MD4, or MD5 hash function.", - "cve": "python/imports/imports-aliases.py:017017b77deb0b8369b6065947833eeea752a92ec8a700db590fece3e934cf0d:B303", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 13, - "end_line": 13 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B303", - "value": "B303" - } - ], - "priority": "Medium", - "file": "python/imports/imports-aliases.py", - "line": 13, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Use of insecure MD2, MD4, or MD5 hash function.", - "cve": "python/imports/imports-aliases.py:45fc8c53aea7b84f06bc4e590cc667678d6073c4c8a1d471177ca2146fb22db2:B303", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 14, - "end_line": 14 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B303", - "value": "B303" - } - ], - "priority": "Medium", - "file": "python/imports/imports-aliases.py", - "line": 14, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Pickle library appears to be in use, possible security issue.", - "cve": "python/imports/imports-aliases.py:5f200d47291e7bbd8352db23019b85453ca048dd98ea0c291260fa7d009963a4:B301", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 15, - "end_line": 15 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B301", - "value": "B301" - } - ], - "priority": "Medium", - "file": "python/imports/imports-aliases.py", - "line": 15, - "tool": "bandit" - }, - { - "category": "sast", - "name": "ECB mode is insecure", - "message": "ECB mode is insecure", - "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:29:ECB_MODE", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "find_sec_bugs", - "name": "Find Security Bugs" - }, - "location": { - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "start_line": 29, - "end_line": 29, - "class": "com.gitlab.security_products.tests.App", - "method": "insecureCypher" - }, - "identifiers": [ - { - "type": "find_sec_bugs_type", - "name": "Find Security Bugs-ECB_MODE", - "value": "ECB_MODE", - "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE" - } - ], - "priority": "Medium", - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "line": 29, - "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE", - "tool": "find_sec_bugs" - }, - { - "category": "sast", - "name": "Cipher with no integrity", - "message": "Cipher with no integrity", - "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:29:CIPHER_INTEGRITY", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "find_sec_bugs", - "name": "Find Security Bugs" - }, - "location": { - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "start_line": 29, - "end_line": 29, - "class": "com.gitlab.security_products.tests.App", - "method": "insecureCypher" - }, - "identifiers": [ - { - "type": "find_sec_bugs_type", - "name": "Find Security Bugs-CIPHER_INTEGRITY", - "value": "CIPHER_INTEGRITY", - "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY" - } - ], - "priority": "Medium", - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "line": 29, - "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY", - "tool": "find_sec_bugs" - }, - { - "category": "sast", - "message": "Probable insecure usage of temp file/directory.", - "cve": "python/hardcoded/hardcoded-tmp.py:63dd4d626855555b816985d82c4614a790462a0a3ada89dc58eb97f9c50f3077:B108", - "severity": "Medium", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-tmp.py", - "start_line": 14, - "end_line": 14 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B108", - "value": "B108", - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" - } - ], - "priority": "Medium", - "file": "python/hardcoded/hardcoded-tmp.py", - "line": 14, - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Probable insecure usage of temp file/directory.", - "cve": "python/hardcoded/hardcoded-tmp.py:4ad6d4c40a8c263fc265f3384724014e0a4f8dd6200af83e51ff120420038031:B108", - "severity": "Medium", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-tmp.py", - "start_line": 10, - "end_line": 10 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B108", - "value": "B108", - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" - } - ], - "priority": "Medium", - "file": "python/hardcoded/hardcoded-tmp.py", - "line": 10, - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with Popen module.", - "cve": "python/imports/imports-aliases.py:2c3e1fa1e54c3c6646e8bcfaee2518153c6799b77587ff8d9a7b0631f6d34785:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 1, - "end_line": 1 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports-aliases.py", - "line": 1, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with pickle module.", - "cve": "python/imports/imports.py:af58d07f6ad519ef5287fcae65bf1a6999448a1a3a8bc1ac2a11daa80d0b96bf:B403", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports.py", - "start_line": 2, - "end_line": 2 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B403", - "value": "B403" - } - ], - "priority": "Low", - "file": "python/imports/imports.py", - "line": 2, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with subprocess module.", - "cve": "python/imports/imports.py:8de9bc98029d212db530785a5f6780cfa663548746ff228ab8fa96c5bb82f089:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports.py", - "start_line": 4, - "end_line": 4 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports.py", - "line": 4, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: 'blerg'", - "cve": "python/hardcoded/hardcoded-passwords.py:97c30f1d76d2a88913e3ce9ae74087874d740f87de8af697a9c455f01119f633:B106", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 22, - "end_line": 22 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B106", - "value": "B106", - "url": "https://docs.openstack.org/bandit/latest/plugins/b106_hardcoded_password_funcarg.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 22, - "url": "https://docs.openstack.org/bandit/latest/plugins/b106_hardcoded_password_funcarg.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: 'root'", - "cve": "python/hardcoded/hardcoded-passwords.py:7431c73a0bc16d94ece2a2e75ef38f302574d42c37ac0c3c38ad0b3bf8a59f10:B105", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 5, - "end_line": 5 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B105", - "value": "B105", - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 5, - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: ''", - "cve": "python/hardcoded/hardcoded-passwords.py:d2d1857c27caedd49c57bfbcdc23afcc92bd66a22701fcdc632869aab4ca73ee:B105", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 9, - "end_line": 9 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B105", - "value": "B105", - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 9, - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: 'ajklawejrkl42348swfgkg'", - "cve": "python/hardcoded/hardcoded-passwords.py:fb3866215a61393a5c9c32a3b60e2058171a23219c353f722cbd3567acab21d2:B105", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 13, - "end_line": 13 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B105", - "value": "B105", - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 13, - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: 'blerg'", - "cve": "python/hardcoded/hardcoded-passwords.py:63c62a8b7e1e5224439bd26b28030585ac48741e28ca64561a6071080c560a5f:B105", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 23, - "end_line": 23 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B105", - "value": "B105", - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 23, - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: 'blerg'", - "cve": "python/hardcoded/hardcoded-passwords.py:4311b06d08df8fa58229b341c531da8e1a31ec4520597bdff920cd5c098d86f9:B105", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 24, - "end_line": 24 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B105", - "value": "B105", - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 24, - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with subprocess module.", - "cve": "python/imports/imports-function.py:5858400c2f39047787702de44d03361ef8d954c9d14bd54ee1c2bef9e6a7df93:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-function.py", - "start_line": 4, - "end_line": 4 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports-function.py", - "line": 4, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with pickle module.", - "cve": "python/imports/imports-function.py:dbda3cf4190279d30e0aad7dd137eca11272b0b225e8af4e8bf39682da67d956:B403", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-function.py", - "start_line": 2, - "end_line": 2 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B403", - "value": "B403" - } - ], - "priority": "Low", - "file": "python/imports/imports-function.py", - "line": 2, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with Popen module.", - "cve": "python/imports/imports-from.py:eb8a0db9cd1a8c1ab39a77e6025021b1261cc2a0b026b2f4a11fca4e0636d8dd:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-from.py", - "start_line": 7, - "end_line": 7 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports-from.py", - "line": 7, - "tool": "bandit" - }, - { - "category": "sast", - "message": "subprocess call with shell=True seems safe, but may be changed in the future, consider rewriting without shell", - "cve": "python/imports/imports-aliases.py:f99f9721e27537fbcb6699a4cf39c6740d6234d2c6f06cfc2d9ea977313c483d:B602", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 9, - "end_line": 9 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B602", - "value": "B602", - "url": "https://docs.openstack.org/bandit/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html" - } - ], - "priority": "Low", - "file": "python/imports/imports-aliases.py", - "line": 9, - "url": "https://docs.openstack.org/bandit/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with subprocess module.", - "cve": "python/imports/imports-from.py:332a12ab1146698f614a905ce6a6a5401497a12281aef200e80522711c69dcf4:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-from.py", - "start_line": 6, - "end_line": 6 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports-from.py", - "line": 6, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with Popen module.", - "cve": "python/imports/imports-from.py:0a48de4a3d5348853a03666cb574697e3982998355e7a095a798bd02a5947276:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-from.py", - "start_line": 1, - "end_line": 2 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports-from.py", - "line": 1, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with pickle module.", - "cve": "python/imports/imports-aliases.py:51b71661dff994bde3529639a727a678c8f5c4c96f00d300913f6d5be1bbdf26:B403", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 7, - "end_line": 8 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B403", - "value": "B403" - } - ], - "priority": "Low", - "file": "python/imports/imports-aliases.py", - "line": 7, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with loads module.", - "cve": "python/imports/imports-aliases.py:6ff02aeb3149c01ab68484d794a94f58d5d3e3bb0d58557ef4153644ea68ea54:B403", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 6, - "end_line": 6 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B403", - "value": "B403" - } - ], - "priority": "Low", - "file": "python/imports/imports-aliases.py", - "line": 6, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)", - "cve": "c/subdir/utils.c:b466873101951fe96e1332f6728eb7010acbbd5dfc3b65d7d53571d091a06d9e:CWE-119!/CWE-120", - "confidence": "Low", - "solution": "Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length", - "scanner": { - "id": "flawfinder", - "name": "Flawfinder" - }, - "location": { - "file": "c/subdir/utils.c", - "start_line": 4 - }, - "identifiers": [ - { - "type": "flawfinder_func_name", - "name": "Flawfinder - char", - "value": "char" - }, - { - "type": "cwe", - "name": "CWE-119", - "value": "119", - "url": "https://cwe.mitre.org/data/definitions/119.html" - }, - { - "type": "cwe", - "name": "CWE-120", - "value": "120", - "url": "https://cwe.mitre.org/data/definitions/120.html" - } - ], - "file": "c/subdir/utils.c", - "line": 4, - "url": "https://cwe.mitre.org/data/definitions/119.html", - "tool": "flawfinder" - }, - { - "category": "sast", - "message": "Check when opening files - can an attacker redirect it (via symlinks), force the opening of special file type (e.g., device files), move things around to create a race condition, control its ancestors, or change its contents? (CWE-362)", - "cve": "c/subdir/utils.c:bab681140fcc8fc3085b6bba74081b44ea145c1c98b5e70cf19ace2417d30770:CWE-362", - "confidence": "Low", - "scanner": { - "id": "flawfinder", - "name": "Flawfinder" - }, - "location": { - "file": "c/subdir/utils.c", - "start_line": 8 - }, - "identifiers": [ - { - "type": "flawfinder_func_name", - "name": "Flawfinder - fopen", - "value": "fopen" - }, - { - "type": "cwe", - "name": "CWE-362", - "value": "362", - "url": "https://cwe.mitre.org/data/definitions/362.html" - } - ], - "file": "c/subdir/utils.c", - "line": 8, - "url": "https://cwe.mitre.org/data/definitions/362.html", - "tool": "flawfinder" - }, - { - "category": "sast", - "message": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)", - "cve": "cplusplus/src/hello.cpp:c8c6dd0afdae6814194cf0930b719f757ab7b379cf8f261e7f4f9f2f323a818a:CWE-119!/CWE-120", - "confidence": "Low", - "solution": "Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length", - "scanner": { - "id": "flawfinder", - "name": "Flawfinder" - }, - "location": { - "file": "cplusplus/src/hello.cpp", - "start_line": 6 - }, - "identifiers": [ - { - "type": "flawfinder_func_name", - "name": "Flawfinder - char", - "value": "char" - }, - { - "type": "cwe", - "name": "CWE-119", - "value": "119", - "url": "https://cwe.mitre.org/data/definitions/119.html" - }, - { - "type": "cwe", - "name": "CWE-120", - "value": "120", - "url": "https://cwe.mitre.org/data/definitions/120.html" - } - ], - "file": "cplusplus/src/hello.cpp", - "line": 6, - "url": "https://cwe.mitre.org/data/definitions/119.html", - "tool": "flawfinder" - }, - { - "category": "sast", - "message": "Does not check for buffer overflows when copying to destination [MS-banned] (CWE-120)", - "cve": "cplusplus/src/hello.cpp:331c04062c4fe0c7c486f66f59e82ad146ab33cdd76ae757ca41f392d568cbd0:CWE-120", - "confidence": "Low", - "solution": "Consider using snprintf, strcpy_s, or strlcpy (warning: strncpy easily misused)", - "scanner": { - "id": "flawfinder", - "name": "Flawfinder" - }, - "location": { - "file": "cplusplus/src/hello.cpp", - "start_line": 7 - }, - "identifiers": [ - { - "type": "flawfinder_func_name", - "name": "Flawfinder - strcpy", - "value": "strcpy" - }, - { - "type": "cwe", - "name": "CWE-120", - "value": "120", - "url": "https://cwe.mitre.org/data/definitions/120.html" - } - ], - "file": "cplusplus/src/hello.cpp", - "line": 7, - "url": "https://cwe.mitre.org/data/definitions/120.html", - "tool": "flawfinder" - } -]
\ No newline at end of file diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js index e47fc518b23..f6316af6ad8 100644 --- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js +++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js @@ -8,7 +8,9 @@ import { I18N_USER_BLOCKED, I18N_USER_LEARN, I18N_USER_FOLLOW, + I18N_ERROR_FOLLOW, I18N_USER_UNFOLLOW, + I18N_ERROR_UNFOLLOW, } from '~/vue_shared/components/user_popover/constants'; import axios from '~/lib/utils/axios_utils'; import { createAlert } from '~/flash'; @@ -379,27 +381,49 @@ describe('User Popover Component', () => { itTracksToggleFollowButtonClick('follow_from_user_popover'); describe('when an error occurs', () => { - beforeEach(() => { - followUser.mockRejectedValue({}); + describe('api send error message', () => { + const mockedMessage = sprintf(I18N_ERROR_UNFOLLOW, { limit: 300 }); + const apiResponse = { response: { data: { message: mockedMessage } } }; - findToggleFollowButton().trigger('click'); - }); + beforeEach(() => { + followUser.mockRejectedValue(apiResponse); + findToggleFollowButton().trigger('click'); + }); - it('shows an error message', async () => { - await axios.waitForAll(); + it('show an error message from api response', async () => { + await axios.waitForAll(); - expect(createAlert).toHaveBeenCalledWith({ - message: 'An error occurred while trying to follow this user, please try again.', - error: {}, - captureError: true, + expect(createAlert).toHaveBeenCalledWith({ + message: mockedMessage, + error: apiResponse, + captureError: true, + }); }); }); - it('emits no events', async () => { - await axios.waitForAll(); + describe('api did not send error message', () => { + beforeEach(() => { + followUser.mockRejectedValue({}); - expect(wrapper.emitted().follow).toBeUndefined(); - expect(wrapper.emitted().unfollow).toBeUndefined(); + findToggleFollowButton().trigger('click'); + }); + + it('shows an error message', async () => { + await axios.waitForAll(); + + expect(createAlert).toHaveBeenCalledWith({ + message: I18N_ERROR_FOLLOW, + error: {}, + captureError: true, + }); + }); + + it('emits no events', async () => { + await axios.waitForAll(); + + expect(wrapper.emitted().follow).toBeUndefined(); + expect(wrapper.emitted().unfollow).toBeUndefined(); + }); }); }); }); @@ -438,7 +462,7 @@ describe('User Popover Component', () => { it('shows an error message', () => { expect(createAlert).toHaveBeenCalledWith({ - message: 'An error occurred while trying to unfollow this user, please try again.', + message: I18N_ERROR_UNFOLLOW, error: {}, captureError: true, }); diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index 38e82d6a4ed..3660d00cf0b 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -29,7 +29,6 @@ import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subs import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql'; -import { temporaryConfig } from '~/graphql_shared/issuable_client'; import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { mockParent, @@ -86,7 +85,6 @@ describe('WorkItemDetail component', () => { subscriptionHandler = titleSubscriptionHandler, confidentialityMock = [updateWorkItemMutation, jest.fn()], workItemsMvc2Enabled = false, - includeWidgets = false, error = undefined, } = {}) => { const handlers = [ @@ -102,13 +100,7 @@ describe('WorkItemDetail component', () => { } wrapper = shallowMount(WorkItemDetail, { - apolloProvider: createMockApollo( - handlers, - {}, - { - typePolicies: includeWidgets ? temporaryConfig.cacheConfig.typePolicies : {}, - }, - ), + apolloProvider: createMockApollo(handlers), propsData: { isModal, workItemId }, data() { return { @@ -502,11 +494,13 @@ describe('WorkItemDetail component', () => { describe('labels widget', () => { it.each` - description | includeWidgets | exists - ${'renders when widget is returned from API'} | ${true} | ${true} - ${'does not render when widget is not returned from API'} | ${false} | ${false} - `('$description', async ({ includeWidgets, exists }) => { - createComponent({ includeWidgets, workItemsMvc2Enabled: true }); + description | labelsWidgetPresent | exists + ${'renders when widget is returned from API'} | ${true} | ${true} + ${'does not render when widget is not returned from API'} | ${false} | ${false} + `('$description', async ({ labelsWidgetPresent, exists }) => { + const response = workItemResponseFactory({ labelsWidgetPresent }); + const handler = jest.fn().mockResolvedValue(response); + createComponent({ handler, workItemsMvc2Enabled: true }); await waitForPromises(); expect(findWorkItemLabels().exists()).toBe(exists); diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js index e8173997ac1..3f5eb4f0def 100644 --- a/spec/frontend/work_items/components/work_item_labels_spec.js +++ b/spec/frontend/work_items/components/work_item_labels_spec.js @@ -7,10 +7,16 @@ import { mountExtended } from 'helpers/vue_test_utils_helper'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import labelSearchQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; +import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import WorkItemLabels from '~/work_items/components/work_item_labels.vue'; -import { i18n } from '~/work_items/constants'; -import { temporaryConfig, resolvers } from '~/graphql_shared/issuable_client'; -import { projectLabelsResponse, mockLabels, workItemQueryResponse } from '../mock_data'; +import { i18n, I18N_WORK_ITEM_ERROR_FETCHING_LABELS } from '~/work_items/constants'; +import { + projectLabelsResponse, + mockLabels, + workItemQueryResponse, + workItemResponseFactory, + updateWorkItemMutationResponse, +} from '../mock_data'; Vue.use(VueApollo); @@ -24,29 +30,27 @@ describe('WorkItemLabels component', () => { const findEmptyState = () => wrapper.findByTestId('empty-state'); const findLabelsTitle = () => wrapper.findByTestId('labels-title'); + const workItemQuerySuccess = jest.fn().mockResolvedValue(workItemQueryResponse); const successSearchQueryHandler = jest.fn().mockResolvedValue(projectLabelsResponse); + const successUpdateWorkItemMutationHandler = jest + .fn() + .mockResolvedValue(updateWorkItemMutationResponse); const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem'); const createComponent = ({ - labels = mockLabels, canUpdate = true, + workItemQueryHandler = workItemQuerySuccess, searchQueryHandler = successSearchQueryHandler, + updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler, } = {}) => { - const apolloProvider = createMockApollo([[labelSearchQuery, searchQueryHandler]], resolvers, { - typePolicies: temporaryConfig.cacheConfig.typePolicies, - }); - - apolloProvider.clients.defaultClient.writeQuery({ - query: workItemQuery, - variables: { - id: workItemId, - }, - data: workItemQueryResponse.data, - }); + const apolloProvider = createMockApollo([ + [workItemQuery, workItemQueryHandler], + [labelSearchQuery, searchQueryHandler], + [updateWorkItemMutation, updateWorkItemMutationHandler], + ]); wrapper = mountExtended(WorkItemLabels, { propsData: { - labels, workItemId, canUpdate, fullPath: 'test-project-path', @@ -157,7 +161,7 @@ describe('WorkItemLabels component', () => { findTokenSelector().vm.$emit('focus'); await waitForPromises(); - expect(wrapper.emitted('error')).toEqual([[i18n.fetchError]]); + expect(wrapper.emitted('error')).toEqual([[I18N_WORK_ITEM_ERROR_FETCHING_LABELS]]); }); it('should search for with correct key after text input', async () => { @@ -169,7 +173,43 @@ describe('WorkItemLabels component', () => { await waitForPromises(); expect(successSearchQueryHandler).toHaveBeenCalledWith( - expect.objectContaining({ search: searchKey }), + expect.objectContaining({ searchTerm: searchKey }), ); }); + + describe('when clicking outside the token selector', () => { + it('calls a mutation with correct variables', () => { + createComponent(); + + findTokenSelector().vm.$emit('input', [mockLabels[0]]); + findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null })); + + expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith({ + input: { + labelsWidget: { addLabelIds: [mockLabels[0].id], removeLabelIds: [] }, + id: 'gid://gitlab/WorkItem/1', + }, + }); + }); + + it('emits an error and resets labels if mutation was rejected', async () => { + const workItemQueryHandler = jest.fn().mockResolvedValue(workItemResponseFactory()); + + createComponent({ updateWorkItemMutationHandler: errorHandler, workItemQueryHandler }); + + await waitForPromises(); + + const initialLabels = findTokenSelector().props('selectedTokens'); + + findTokenSelector().vm.$emit('input', [mockLabels[0]]); + findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null })); + + await waitForPromises(); + + const updatedLabels = findTokenSelector().props('selectedTokens'); + + expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]); + expect(updatedLabels).toEqual(initialLabels); + }); + }); }); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index d1108a57e23..47906a6e160 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -17,6 +17,25 @@ export const mockAssignees = [ }, ]; +export const mockLabels = [ + { + __typename: 'Label', + id: 'gid://gitlab/Label/1', + title: 'Label 1', + description: '', + color: '#f00', + textColor: '#00f', + }, + { + __typename: 'Label', + id: 'gid://gitlab/Label/2', + title: 'Label::2', + description: '', + color: '#b00', + textColor: '#00b', + }, +]; + export const workItemQueryResponse = { data: { workItem: { @@ -163,9 +182,11 @@ export const workItemResponseFactory = ({ allowsMultipleAssignees = true, assigneesWidgetPresent = true, datesWidgetPresent = true, + labelsWidgetPresent = true, weightWidgetPresent = true, confidential = false, canInviteMembers = false, + allowsScopedLabels = false, parent = mockParent.parent, } = {}) => ({ data: { @@ -212,6 +233,16 @@ export const workItemResponseFactory = ({ }, } : { type: 'MOCK TYPE' }, + labelsWidgetPresent + ? { + __typename: 'WorkItemWidgetLabels', + type: 'LABELS', + allowsScopedLabels, + labels: { + nodes: mockLabels, + }, + } + : { type: 'MOCK TYPE' }, datesWidgetPresent ? { __typename: 'WorkItemWidgetStartAndDueDate', @@ -873,25 +904,6 @@ export const currentUserNullResponse = { }, }; -export const mockLabels = [ - { - __typename: 'Label', - id: 'gid://gitlab/Label/1', - title: 'Label 1', - description: '', - color: '#f00', - textColor: '#00f', - }, - { - __typename: 'Label', - id: 'gid://gitlab/Label/2', - title: 'Label 2', - description: '', - color: '#b00', - textColor: '#00b', - }, -]; - export const projectLabelsResponse = { data: { workspace: { diff --git a/spec/lib/gitlab/ci/parsers/security/sast_spec.rb b/spec/lib/gitlab/ci/parsers/security/sast_spec.rb index 8a30d70c28c..f6113308201 100644 --- a/spec/lib/gitlab/ci/parsers/security/sast_spec.rb +++ b/spec/lib/gitlab/ci/parsers/security/sast_spec.rb @@ -62,59 +62,6 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Sast do end end - # This spec is KIND OF MAGIC - # sast_deprecated trait uses spec/fixtures/security_reports/deprecated/gl-sast-report.json - # if you take a look at it then it does not conform to the schema at all, yet it somehow passes. - # The reason for this is because lib/gitlab/ci/parsers/security/sast.rb - # includes lib/gitlab/ci/parsers/security/concerns/deprecated_syntax.rb - # which modifies the report data in place and puts the version there - # - # TODO: Do not allow modifying input to the parser https://gitlab.com/gitlab-org/gitlab/-/issues/373177 - context "when parsing unsupported report" do - where(:report_format, :report_version) do - :sast_deprecated | '1.2' - end - - with_them do - let(:report) do - Gitlab::Ci::Reports::Security::Report.new( - artifact.file_type, - pipeline, - created_at - ) - end - - let(:artifact) { create(:ci_job_artifact, report_format) } - - before do - artifact.each_blob { |blob| described_class.parse!(blob, report, validate: validate) } - end - - context 'when validation is disabled' do - let(:validate) { false } - - it "generates no errors" do - expect(report.errors.size).to eq(0) - end - end - - context 'when validation is enabled' do - let(:validate) { true } - - it "generates errors" do - messages = report.errors.map { |e| e[:message] }.sort - first_error = /^Version #{report_version} for report type sast is unsupported.*/ - second_error = %r{^property '/version' does not match.*} - - # One for missing version, one for missing required keys - expect(report.errors.size).to eq(2) - expect(messages.first).to match(first_error) - expect(messages.last).to match(second_error) - end - end - end - end - context "when parsing an empty report" do let(:report) { Gitlab::Ci::Reports::Security::Report.new('sast', pipeline, created_at) } let(:blob) { Gitlab::Json.generate({}) } diff --git a/spec/lib/gitlab/github_import/importer/note_attachments_importer_spec.rb b/spec/lib/gitlab/github_import/importer/note_attachments_importer_spec.rb index e4d5ea77dcf..d4c5612e906 100644 --- a/spec/lib/gitlab/github_import/importer/note_attachments_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/note_attachments_importer_spec.rb @@ -11,12 +11,14 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteAttachmentsImporter do let(:doc_url) { 'https://github.com/nickname/public-test-repo/files/9020437/git-cheat-sheet.txt' } let(:image_url) { 'https://user-images.githubusercontent.com/6833842/0cf366b61ef2.jpeg' } + let(:image_tag_url) { 'https://user-images.githubusercontent.com/6833842/0cf366b61ea5.jpeg' } let(:text) do - <<-TEXT.strip + <<-TEXT.split("\n").map(&:strip).join("\n") Some text... [special-doc](#{doc_url}) ![image.jpeg](#{image_url}) + <img width=\"248\" alt=\"tag-image\" src="#{image_tag_url}"> TEXT end @@ -24,31 +26,37 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteAttachmentsImporter do let(:downloader_stub) { instance_double(Gitlab::GithubImport::AttachmentsDownloader) } let(:tmp_stub_doc) { Tempfile.create('attachment_download_test.txt') } let(:tmp_stub_image) { Tempfile.create('image.jpeg') } + let(:tmp_stub_image_tag) { Tempfile.create('image-tag.jpeg') } before do allow(Gitlab::GithubImport::AttachmentsDownloader).to receive(:new).with(doc_url) .and_return(downloader_stub) allow(Gitlab::GithubImport::AttachmentsDownloader).to receive(:new).with(image_url) .and_return(downloader_stub) - allow(downloader_stub).to receive(:perform).and_return(tmp_stub_doc, tmp_stub_image) - allow(downloader_stub).to receive(:delete).twice - - allow(UploadService).to receive(:new) - .with(project, tmp_stub_doc, FileUploader).and_call_original - allow(UploadService).to receive(:new) - .with(project, tmp_stub_image, FileUploader).and_call_original + allow(Gitlab::GithubImport::AttachmentsDownloader).to receive(:new).with(image_tag_url) + .and_return(downloader_stub) + allow(downloader_stub).to receive(:perform).and_return(tmp_stub_doc, tmp_stub_image, tmp_stub_image_tag) + allow(downloader_stub).to receive(:delete).exactly(3).times end context 'when importing release attachments' do let(:release) { create(:release, project: project, description: text) } let(:note_text) { Gitlab::GithubImport::Representation::NoteText.from_db_record(release) } - it 'updates release description with new attachment urls' do + it 'updates release description with new attachment url' do + expect(UploadService).to receive(:new) + .with(project, tmp_stub_doc, FileUploader).and_call_original + expect(UploadService).to receive(:new) + .with(project, tmp_stub_image, FileUploader).and_call_original + expect(UploadService).to receive(:new) + .with(project, tmp_stub_image_tag, FileUploader).and_call_original + importer.execute release.reload - expect(release.description).to start_with("Some text...\n\n [special-doc](/uploads/") + expect(release.description).to start_with("Some text...\n\n[special-doc](/uploads/") expect(release.description).to include('![image.jpeg](/uploads/') + expect(release.description).to include('<img width="248" alt="tag-image" src="/uploads') end end @@ -60,8 +68,9 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteAttachmentsImporter do importer.execute note.reload - expect(note.note).to start_with("Some text...\n\n [special-doc](/uploads/") + expect(note.note).to start_with("Some text...\n\n[special-doc](/uploads/") expect(note.note).to include('![image.jpeg](/uploads/') + expect(note.note).to include('<img width="248" alt="tag-image" src="/uploads') end end end diff --git a/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb b/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb index 4529c2675ff..027b2ac422e 100644 --- a/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb @@ -9,12 +9,17 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchImporter do let(:allow_force_pushes_on_github) { true } let(:required_conversation_resolution) { false } let(:required_signatures) { false } + let(:required_pull_request_reviews) { false } + let(:expected_push_access_level) { Gitlab::Access::MAINTAINER } + let(:expected_merge_access_level) { Gitlab::Access::MAINTAINER } + let(:expected_allow_force_push) { true } let(:github_protected_branch) do Gitlab::GithubImport::Representation::ProtectedBranch.new( id: branch_name, allow_force_pushes: allow_force_pushes_on_github, required_conversation_resolution: required_conversation_resolution, - required_signatures: required_signatures + required_signatures: required_signatures, + required_pull_request_reviews: required_pull_request_reviews ) end @@ -28,8 +33,8 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchImporter do let(:expected_ruleset) do { name: 'protection', - push_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }], - merge_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }], + push_access_levels_attributes: [{ access_level: expected_push_access_level }], + merge_access_levels_attributes: [{ access_level: expected_merge_access_level }], allow_force_push: expected_allow_force_push } end @@ -165,7 +170,7 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchImporter do end end - context "when branch is not default" do + context 'when branch is not default' do context 'when required_conversation_resolution rule is enabled' do let(:required_conversation_resolution) { true } @@ -190,5 +195,107 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchImporter do it_behaves_like 'does not change project attributes' end end + + context 'when required_pull_request_reviews rule is enabled on GitHub' do + let(:required_pull_request_reviews) { true } + let(:expected_push_access_level) { Gitlab::Access::NO_ACCESS } + let(:expected_merge_access_level) { Gitlab::Access::MAINTAINER } + + it_behaves_like 'create branch protection by the strictest ruleset' + end + + context 'when required_pull_request_reviews rule is disabled on GitHub' do + let(:required_pull_request_reviews) { false } + + context 'when branch is default' do + before do + allow(project).to receive(:default_branch).and_return(branch_name) + end + + context 'when default branch protection = Gitlab::Access::PROTECTION_DEV_CAN_PUSH' do + before do + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH) + end + + let(:expected_push_access_level) { Gitlab::Access::DEVELOPER } + let(:expected_merge_access_level) { Gitlab::Access::MAINTAINER } + + it_behaves_like 'create branch protection by the strictest ruleset' + end + + context 'when default branch protection = Gitlab::Access::PROTECTION_DEV_CAN_MERGE' do + before do + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE) + end + + let(:expected_push_access_level) { Gitlab::Access::MAINTAINER } + let(:expected_merge_access_level) { Gitlab::Access::DEVELOPER } + + it_behaves_like 'create branch protection by the strictest ruleset' + end + end + + context 'when branch is protected on GitLab' do + let(:protected_branch) do + create( + :protected_branch, + project: project, + name: 'protect*', + allow_force_push: true + ) + end + + let(:push_access_level) { protected_branch.push_access_levels.first } + let(:merge_access_level) { protected_branch.merge_access_levels.first } + + context 'when there is branch protection rule for the role' do + context 'when No one can merge' do + before do + merge_access_level.update_column(:access_level, Gitlab::Access::NO_ACCESS) + end + + let(:expected_push_access_level) { push_access_level.access_level } + let(:expected_merge_access_level) { Gitlab::Access::NO_ACCESS } + + it_behaves_like 'create branch protection by the strictest ruleset' + end + + context 'when Maintainers and Developers can merge' do + before do + merge_access_level.update_column(:access_level, Gitlab::Access::DEVELOPER) + end + + let(:gitlab_push_access_level) { push_access_level.access_level } + let(:gitlab_merge_access_level) { merge_access_level.access_level } + let(:expected_push_access_level) { gitlab_push_access_level } + let(:expected_merge_access_level) { [gitlab_merge_access_level, github_default_merge_access_level].max } + let(:github_default_merge_access_level) do + Gitlab::GithubImport::Importer::ProtectedBranchImporter::GITHUB_DEFAULT_MERGE_ACCESS_LEVEL + end + + it_behaves_like 'create branch protection by the strictest ruleset' + end + end + + context 'when there is no branch protection rule for the role' do + before do + push_access_level.update_column(:user_id, project.owner.id) + merge_access_level.update_column(:user_id, project.owner.id) + end + + let(:expected_push_access_level) { ProtectedBranch::PushAccessLevel::GITLAB_DEFAULT_ACCESS_LEVEL } + let(:expected_merge_access_level) { Gitlab::Access::MAINTAINER } + + it_behaves_like 'create branch protection by the strictest ruleset' + end + end + + context 'when branch is neither default nor protected on GitLab' do + let(:expected_push_access_level) { ProtectedBranch::PushAccessLevel::GITLAB_DEFAULT_ACCESS_LEVEL } + let(:expected_merge_access_level) { ProtectedBranch::MergeAccessLevel::GITLAB_DEFAULT_ACCESS_LEVEL } + + it_behaves_like 'create branch protection by the strictest ruleset' + end + end end end diff --git a/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb b/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb index 4e9208be985..a0ced456391 100644 --- a/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb @@ -23,11 +23,13 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchesImporter do let(:github_protection_rule) do response = Struct.new(:name, :url, :required_signatures, :enforce_admins, :required_linear_history, :allow_force_pushes, :allow_deletion, :block_creations, :required_conversation_resolution, + :required_pull_request_reviews, keyword_init: true ) required_signatures = Struct.new(:url, :enabled, keyword_init: true) enforce_admins = Struct.new(:url, :enabled, keyword_init: true) allow_option = Struct.new(:enabled, keyword_init: true) + required_pull_request_reviews = Struct.new(:url, :dismissal_restrictions, keyword_init: true) response.new( name: 'main', url: 'https://example.com/branches/main/protection', @@ -53,6 +55,10 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchesImporter do ), required_conversation_resolution: allow_option.new( enabled: false + ), + required_pull_request_reviews: required_pull_request_reviews.new( + url: 'https://example.com/branches/main/protection/required_pull_request_reviews', + dismissal_restrictions: {} ) ) end diff --git a/spec/lib/gitlab/github_import/markdown/attachment_spec.rb b/spec/lib/gitlab/github_import/markdown/attachment_spec.rb new file mode 100644 index 00000000000..5d29de34141 --- /dev/null +++ b/spec/lib/gitlab/github_import/markdown/attachment_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Markdown::Attachment do + let(:name) { FFaker::Lorem.word } + let(:url) { FFaker::Internet.uri('https') } + + describe '.from_markdown' do + context "when it's a doc attachment" do + let(:doc_extension) { Gitlab::GithubImport::Markdown::Attachment::DOC_TYPES.sample } + let(:url) { "https://github.com/nickname/public-test-repo/files/3/git-cheat-sheet.#{doc_extension}" } + let(:name) { FFaker::Lorem.word } + let(:markdown_node) do + instance_double('CommonMarker::Node', url: url, to_plaintext: name, type: :link) + end + + it 'returns instance with attachment info' do + attachment = described_class.from_markdown(markdown_node) + + expect(attachment.name).to eq name + expect(attachment.url).to eq url + end + + context "when type is not in whitelist" do + let(:doc_extension) { 'exe' } + + it { expect(described_class.from_markdown(markdown_node)).to eq nil } + end + + context 'when domain name is unknown' do + let(:url) do + "https://bitbucket.com/nickname/public-test-repo/files/3/git-cheat-sheet.#{doc_extension}" + end + + it { expect(described_class.from_markdown(markdown_node)).to eq nil } + end + end + + context "when it's an image attachment" do + let(:image_extension) { Gitlab::GithubImport::Markdown::Attachment::MEDIA_TYPES.sample } + let(:url) { "https://user-images.githubusercontent.com/1/uuid-1.#{image_extension}" } + let(:name) { FFaker::Lorem.word } + let(:markdown_node) do + instance_double('CommonMarker::Node', url: url, to_plaintext: name, type: :image) + end + + it 'returns instance with attachment info' do + attachment = described_class.from_markdown(markdown_node) + + expect(attachment.name).to eq name + expect(attachment.url).to eq url + end + + context "when type is not in whitelist" do + let(:image_extension) { 'mkv' } + + it { expect(described_class.from_markdown(markdown_node)).to eq nil } + end + + context 'when domain name is unknown' do + let(:url) { "https://user-images.github.com/1/uuid-1.#{image_extension}" } + + it { expect(described_class.from_markdown(markdown_node)).to eq nil } + end + end + + context "when it's an inline html node" do + let(:name) { FFaker::Lorem.word } + let(:image_extension) { Gitlab::GithubImport::Markdown::Attachment::MEDIA_TYPES.sample } + let(:url) { "https://user-images.githubusercontent.com/1/uuid-1.#{image_extension}" } + let(:img) { "<img width=\"248\" alt=\"#{name}\" src=\"#{url}\">" } + let(:markdown_node) do + instance_double('CommonMarker::Node', string_content: img, type: :inline_html) + end + + it 'returns instance with attachment info' do + attachment = described_class.from_markdown(markdown_node) + + expect(attachment.name).to eq name + expect(attachment.url).to eq url + end + end + end + + describe '#inspect' do + it 'returns attachment basic info' do + attachment = described_class.new(name, url) + + expect(attachment.inspect).to eq "<Gitlab::GithubImport::Markdown::Attachment: { name: #{name}, url: #{url} }>" + end + end +end diff --git a/spec/lib/gitlab/github_import/markdown_text_spec.rb b/spec/lib/gitlab/github_import/markdown_text_spec.rb index 5bca431a110..3f771970588 100644 --- a/spec/lib/gitlab/github_import/markdown_text_spec.rb +++ b/spec/lib/gitlab/github_import/markdown_text_spec.rb @@ -60,31 +60,48 @@ RSpec.describe Gitlab::GithubImport::MarkdownText do end end - describe '.fetch_attachment_urls' do - let(:image_extension) { described_class::MEDIA_TYPES.sample } + describe '.fetch_attachments' do + let(:image_extension) { Gitlab::GithubImport::Markdown::Attachment::MEDIA_TYPES.sample } let(:image_attachment) do - "![special-image](https://user-images.githubusercontent.com/6833862/"\ - "176685788-e7a93168-7ded-406a-82b5-eb1c56685a93.#{image_extension})" + "![special-image](https://user-images.githubusercontent.com/1/uuid-1.#{image_extension})" end - let(:doc_extension) { described_class::DOC_TYPES.sample } + let(:img_tag_attachment) do + "<img width=\"248\" alt=\"tag-image\" src=\"https://user-images.githubusercontent.com/2/"\ + "uuid-2.#{image_extension}\">" + end + + let(:damaged_img_tag) do + "<img width=\"248\" alt=\"tag-image\" src=\"https://user-images.githubusercontent.com" + end + + let(:doc_extension) { Gitlab::GithubImport::Markdown::Attachment::DOC_TYPES.sample } let(:doc_attachment) do "[some-doc](https://github.com/nickname/public-test-repo/"\ - "files/9020437/sample.#{doc_extension})" + "files/3/git-cheat-sheet.#{doc_extension})" end let(:text) do - <<-TEXT + <<-TEXT.split("\n").map(&:strip).join("\n") Comment with an attachment #{image_attachment} #{FFaker::Lorem.sentence} #{doc_attachment} + #{damaged_img_tag} + #{FFaker::Lorem.paragraph} + #{img_tag_attachment} TEXT end - it 'fetches attachment urls' do - expect(described_class.fetch_attachment_urls(text)) - .to contain_exactly(image_attachment, doc_attachment) + it 'fetches attachments' do + attachments = described_class.fetch_attachments(text) + + expect(attachments.map(&:name)).to contain_exactly('special-image', 'tag-image', 'some-doc') + expect(attachments.map(&:url)).to contain_exactly( + "https://user-images.githubusercontent.com/1/uuid-1.#{image_extension}", + "https://user-images.githubusercontent.com/2/uuid-2.#{image_extension}", + "https://github.com/nickname/public-test-repo/files/3/git-cheat-sheet.#{doc_extension}" + ) end end diff --git a/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb b/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb index 21fd3b2e44a..30b29659eee 100644 --- a/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb +++ b/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb @@ -20,6 +20,10 @@ RSpec.describe Gitlab::GithubImport::Representation::ProtectedBranch do it 'includes the protected branch required_conversation_resolution attribute' do expect(protected_branch.required_conversation_resolution).to eq true end + + it 'includes the protected branch required_pull_request_reviews' do + expect(protected_branch.required_pull_request_reviews).to eq true + end end end @@ -27,9 +31,11 @@ RSpec.describe Gitlab::GithubImport::Representation::ProtectedBranch do let(:response) do response = Struct.new( :url, :allow_force_pushes, :required_conversation_resolution, :required_signatures, + :required_pull_request_reviews, keyword_init: true ) enabled_setting = Struct.new(:enabled, keyword_init: true) + required_pull_request_reviews = Struct.new(:url, :dismissal_restrictions, keyword_init: true) response.new( url: 'https://example.com/branches/main/protection', allow_force_pushes: enabled_setting.new( @@ -40,6 +46,10 @@ RSpec.describe Gitlab::GithubImport::Representation::ProtectedBranch do ), required_signatures: enabled_setting.new( enabled: true + ), + required_pull_request_reviews: required_pull_request_reviews.new( + url: 'https://example.com/branches/main/protection/required_pull_request_reviews', + dismissal_restrictions: {} ) ) end @@ -56,7 +66,8 @@ RSpec.describe Gitlab::GithubImport::Representation::ProtectedBranch do 'id' => 'main', 'allow_force_pushes' => true, 'required_conversation_resolution' => true, - 'required_signatures' => true + 'required_signatures' => true, + 'required_pull_request_reviews' => true } end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a32eea7dfc9..a163441590f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -3723,6 +3723,22 @@ RSpec.describe User do expect(user.followees).to be_empty end + + it 'does not follow if max followee limit is reached' do + stub_const('Users::UserFollowUser::MAX_FOLLOWEE_LIMIT', 2) + + user = create(:user) + Users::UserFollowUser::MAX_FOLLOWEE_LIMIT.times { user.follow(create(:user)) } + + followee = create(:user) + user_follow_user = user.follow(followee) + + expect(user_follow_user).not_to be_persisted + expected_message = format(_("You can't follow more than %{limit} users. To follow more users, unfollow some others."), limit: Users::UserFollowUser::MAX_FOLLOWEE_LIMIT) + expect(user_follow_user.errors.messages[:base].first).to eq(expected_message) + + expect(user.following?(followee)).to be_falsey + end end describe '#unfollow' do @@ -3751,6 +3767,18 @@ RSpec.describe User do expect(user.followees).to be_empty end + + it 'unfollows when over followee limit' do + user = create(:user) + + followees = create_list(:user, 4) + followees.each { |f| expect(user.follow(f)).to be_truthy } + + stub_const('Users::UserFollowUser::MAX_FOLLOWEE_LIMIT', followees.length - 2) + + expect(user.unfollow(followees.first)).to be_truthy + expect(user.following?(followees.first)).to be_falsey + end end describe '#notification_email_or_default' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 96e23337411..7acbd12cae9 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -940,6 +940,17 @@ RSpec.describe API::Users do expect(user.followees).to contain_exactly(followee) expect(response).to have_gitlab_http_status(:created) end + + it 'alerts and not follow when over followee limit' do + stub_const('Users::UserFollowUser::MAX_FOLLOWEE_LIMIT', 2) + Users::UserFollowUser::MAX_FOLLOWEE_LIMIT.times { user.follow(create(:user)) } + + post api("/users/#{followee.id}/follow", user) + expect(response).to have_gitlab_http_status(:bad_request) + expected_message = format(_("You can't follow more than %{limit} users. To follow more users, unfollow some others."), limit: Users::UserFollowUser::MAX_FOLLOWEE_LIMIT) + expect(json_response['message']).to eq(expected_message) + expect(user.following?(followee)).to be_falsey + end end context 'on a followed user' do diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index 42f14392117..e78d4cc326e 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -828,6 +828,26 @@ RSpec.describe UsersController do end end + describe 'POST #follow' do + context 'when over followee limit' do + before do + stub_const('Users::UserFollowUser::MAX_FOLLOWEE_LIMIT', 2) + sign_in(user) + end + + it 'alerts and not follow' do + Users::UserFollowUser::MAX_FOLLOWEE_LIMIT.times { user.follow(create(:user)) } + + post user_follow_url(username: public_user.username) + expect(response).to be_redirect + + expected_message = format(_("You can't follow more than %{limit} users. To follow more users, unfollow some others."), limit: Users::UserFollowUser::MAX_FOLLOWEE_LIMIT) + expect(flash[:alert]).to eq(expected_message) + expect(user).not_to be_following(public_user) + end + end + end + context 'token authentication' do it_behaves_like 'authenticates sessionless user for the request spec', 'show atom', public_resource: true do let(:url) { user_url(user, format: :atom) } diff --git a/spec/services/clusters/applications/destroy_service_spec.rb b/spec/services/clusters/applications/destroy_service_spec.rb deleted file mode 100644 index 7306256e68e..00000000000 --- a/spec/services/clusters/applications/destroy_service_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Clusters::Applications::DestroyService, '#execute' do - let(:cluster) { create(:cluster, :project, :provided_by_gcp) } - let(:user) { create(:user) } - let(:params) { { application: 'prometheus' } } - let(:service) { described_class.new(cluster, user, params) } - let(:test_request) { double } - let(:worker_class) { Clusters::Applications::UninstallWorker } - - subject { service.execute(test_request) } - - before do - allow(worker_class).to receive(:perform_async) - end - - context 'application is not installed' do - it 'raises Clusters::Applications::BaseService::InvalidApplicationError' do - expect(worker_class).not_to receive(:perform_async) - - expect { subject } - .to raise_exception { Clusters::Applications::BaseService::InvalidApplicationError } - .and not_change { Clusters::Applications::Prometheus.count } - .and not_change { Clusters::Applications::Prometheus.with_status(:scheduled).count } - end - end - - context 'application is installed' do - context 'application is schedulable' do - let!(:application) do - create(:clusters_applications_prometheus, :installed, cluster: cluster) - end - - it 'makes application scheduled!' do - subject - - expect(application.reload).to be_scheduled - end - - it 'schedules UninstallWorker' do - expect(worker_class).to receive(:perform_async).with(application.name, application.id) - - subject - end - end - - context 'application is not schedulable' do - let!(:application) do - create(:clusters_applications_prometheus, :updating, cluster: cluster) - end - - it 'raises StateMachines::InvalidTransition' do - expect(worker_class).not_to receive(:perform_async) - - expect { subject } - .to raise_exception { StateMachines::InvalidTransition } - .and not_change { Clusters::Applications::Prometheus.with_status(:scheduled).count } - end - end - end -end diff --git a/spec/services/clusters/applications/uninstall_service_spec.rb b/spec/services/clusters/applications/uninstall_service_spec.rb deleted file mode 100644 index bfe38ba670d..00000000000 --- a/spec/services/clusters/applications/uninstall_service_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Clusters::Applications::UninstallService, '#execute' do - let(:application) { create(:clusters_applications_prometheus, :scheduled) } - let(:service) { described_class.new(application) } - let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::API) } - let(:worker_class) { Clusters::Applications::WaitForUninstallAppWorker } - - before do - allow(service).to receive(:helm_api).and_return(helm_client) - end - - context 'when there are no errors' do - before do - expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::V3::DeleteCommand)) - allow(worker_class).to receive(:perform_in).and_return(nil) - end - - it 'make the application to be uninstalling' do - expect(application.cluster).not_to be_nil - service.execute - - expect(application).to be_uninstalling - end - - it 'schedule async installation status check' do - expect(worker_class).to receive(:perform_in).once - - service.execute - end - end - - context 'when k8s cluster communication fails' do - let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) } - - before do - expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::V3::DeleteCommand)).and_raise(error) - end - - include_examples 'logs kubernetes errors' do - let(:error_name) { 'Kubeclient::HttpError' } - let(:error_message) { 'system failure' } - let(:error_code) { 500 } - end - - it 'make the application errored' do - service.execute - - expect(application).to be_uninstall_errored - expect(application.status_reason).to match('Kubernetes error: 500') - end - end - - context 'a non kubernetes error happens' do - let(:application) { create(:clusters_applications_prometheus, :scheduled) } - let(:error) { StandardError.new('something bad happened') } - - before do - expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::V3::DeleteCommand)).and_raise(error) - end - - include_examples 'logs kubernetes errors' do - let(:error_name) { 'StandardError' } - let(:error_message) { 'something bad happened' } - let(:error_code) { nil } - end - - it 'make the application errored' do - service.execute - - expect(application).to be_uninstall_errored - expect(application.status_reason).to eq('Failed to uninstall.') - end - end -end |