diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-10-19 15:11:29 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-10-19 15:11:29 +0300 |
commit | 881435f2a3eeca1b5b544ad7c7510481b1773d1b (patch) | |
tree | 34d47e49a899efa730d92d2ea25a31e28be32895 | |
parent | 91a9a020dafedd084aaa72022f0aa72d14e4f20b (diff) |
Add latest changes from gitlab-org/gitlab@master
53 files changed, 1220 insertions, 358 deletions
diff --git a/Gemfile.lock b/Gemfile.lock index 052a59d6b7f..c5d2d2b1634 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -103,7 +103,7 @@ PATH specs: devise-pbkdf2-encryptable (0.0.0) devise (~> 4.0) - devise-two-factor (~> 4.0) + devise-two-factor (~> 4.1.1) PATH remote: vendor/gems/mail-smtp_pool diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue index 509efd31dcd..505612c59da 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -1,12 +1,12 @@ <script> -import { GlAlert, GlButton, GlCollapse, GlIcon } from '@gitlab/ui'; +import { GlAlert, GlButton, GlCollapse, GlLink, GlIcon, GlSprintf } from '@gitlab/ui'; import { partition, isString, uniqueId, isEmpty } from 'lodash'; import SafeHtml from '~/vue_shared/directives/safe_html'; import InviteModalBase from 'ee_else_ce/invite_members/components/invite_modal_base.vue'; import Api from '~/api'; import Tracking from '~/tracking'; import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants'; -import { n__, sprintf } from '~/locale'; +import { n__, s__, sprintf } from '~/locale'; import { memberName, triggerExternalAlert } from 'ee_else_ce/invite_members/utils/member_utils'; import { captureException } from '~/ci/runner/sentry_utils'; import { @@ -31,7 +31,9 @@ export default { GlAlert, GlButton, GlCollapse, + GlLink, GlIcon, + GlSprintf, InviteModalBase, MembersTokenSelect, ModalConfetti, @@ -43,6 +45,17 @@ export default { SafeHtml, }, mixins: [Tracking.mixin({ category: INVITE_MEMBER_MODAL_TRACKING_CATEGORY })], + inject: { + isCurrentUserAdmin: { + default: false, + }, + isEmailSignupEnabled: { + default: true, + }, + newUsersUrl: { + default: '', + }, + }, props: { id: { type: String, @@ -122,6 +135,9 @@ export default { isCelebration() { return this.mode === 'celebrate'; }, + isTextForAdmin() { + return this.isCurrentUserAdmin && Boolean(this.newUsersUrl); + }, modalTitle() { return this.$options.labels.modal[this.mode].title; }, @@ -131,6 +147,11 @@ export default { labelIntroText() { return this.$options.labels[this.inviteTo][this.mode].introText; }, + labelSearchField() { + return this.isEmailSignupEnabled + ? this.$options.labels.searchField + : s__('InviteMembersModal|Username'); + }, isEmptyInvites() { return Boolean(this.newUsersToInvite.length); }, @@ -144,6 +165,14 @@ export default { this.errorList.length, ); }, + signupDisabledText() { + return s__( + "InviteMembersModal|Administrators can %{linkStart}add new users by email manually%{linkEnd}. After they've been added, you can invite them to this group with their username.", + ); + }, + signupDisabledTitle() { + return s__('InviteMembersModal|Inviting users by email is disabled'); + }, showUserLimitNotification() { return !isEmpty(this.usersLimitDataset.alertVariant); }, @@ -173,8 +202,13 @@ export default { count: this.errorsExpanded.length, }); }, + formGroupDescriptionText() { + return this.isEmailSignupEnabled + ? this.$options.labels.placeHolder + : s__('InviteMembersModal|Select members'); + }, formGroupDescription() { - return this.invalidFeedbackMessage ? null : this.$options.labels.placeHolder; + return this.invalidFeedbackMessage ? null : this.formGroupDescriptionText; }, }, watch: { @@ -224,7 +258,7 @@ export default { this.$root.$emit(BV_HIDE_MODAL, this.modalId); }, showEmptyInvitesAlert() { - this.invalidFeedbackMessage = this.$options.labels.placeHolder; + this.invalidFeedbackMessage = this.formGroupDescriptionText; this.shouldShowEmptyInvitesAlert = true; this.$refs.alerts.focus(); }, @@ -345,7 +379,7 @@ export default { :default-access-level="defaultAccessLevel" :help-link="helpLink" :label-intro-text="labelIntroText" - :label-search-field="$options.labels.searchField" + :label-search-field="labelSearchField" :form-group-description="formGroupDescription" :invalid-feedback-message="invalidFeedbackMessage" :is-loading="isLoading" @@ -429,6 +463,24 @@ export default { </gl-button> </template> </gl-alert> + <gl-alert + v-if="!isEmailSignupEnabled" + id="signup-disabled-alert" + :dismissible="false" + :title="signupDisabledTitle" + class="gl-mb-4" + variant="warning" + data-testid="email-signup-disabled-alert" + > + <gl-sprintf :message="signupDisabledText"> + <template #link="{ content }"> + <gl-link v-if="isTextForAdmin" :href="newUsersUrl" target="_blank">{{ + content + }}</gl-link> + <span v-else>{{ content }}</span> + </template> + </gl-sprintf> + </gl-alert> <user-limit-notification v-else-if="showUserLimitNotification" class="gl-mb-5" @@ -447,6 +499,7 @@ export default { v-model="newUsersToInvite" class="gl-mb-2" aria-labelledby="empty-invites-alert" + :can-use-email-token="isEmailSignupEnabled" :input-id="inputId" :exception-state="exceptionState" :users-filter="usersFilter" diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue index 8493787f075..0be04b7af35 100644 --- a/app/assets/javascripts/invite_members/components/members_token_select.vue +++ b/app/assets/javascripts/invite_members/components/members_token_select.vue @@ -21,6 +21,11 @@ export default { GlSprintf, }, props: { + canUseEmailToken: { + type: Boolean, + required: false, + default: true, + }, placeholder: { type: String, required: false, @@ -68,6 +73,10 @@ export default { }, computed: { emailIsValid() { + if (!this.canUseEmailToken) { + return false; + } + const regex = /^\S+@\S+$/; return this.originalInput.match(regex) !== null; @@ -137,9 +146,8 @@ export default { username: token.username, avatar_url: token.avatar_url, })); - this.loading = false; }) - .catch(() => { + .finally(() => { this.loading = false; }); }, SEARCH_DELAY), diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js index 41ed0179364..8dfe697e2cb 100644 --- a/app/assets/javascripts/invite_members/init_invite_members_modal.js +++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js @@ -25,6 +25,9 @@ export default (function initInviteMembersModal() { name: 'InviteMembersModalRoot', provide: { name: el.dataset.name, + newUsersUrl: el.dataset.newUsersUrl, + isCurrentUserAdmin: parseBoolean(el.dataset.isCurrentUserAdmin), + isEmailSignupEnabled: parseBoolean(el.dataset.isSignupEnabled), }, render: (createElement) => createElement(InviteMembersModal, { diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue index 5416f86abeb..ff48b8d92cc 100644 --- a/app/assets/javascripts/super_sidebar/components/nav_item.vue +++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue @@ -70,14 +70,16 @@ export default { return { isMouseIn: false, canClickPinButton: false, - pillCount: this.item.pill_count, }; }, computed: { + pillData() { + return this.item.pill_count; + }, hasPill() { return ( - Number.isFinite(this.pillCount) || - (typeof this.pillCount === 'string' && this.pillCount !== '') + Number.isFinite(this.pillData) || + (typeof this.pillData === 'string' && this.pillData !== '') ); }, isPinnable() { @@ -193,7 +195,11 @@ export default { }, updatePillValue({ value, itemId }) { if (this.item.id === itemId) { - this.pillCount = value; + // https://gitlab.com/gitlab-org/gitlab/-/issues/428246 + // fixing this linting issue is causing the pills not to async update + // + // eslint-disable-next-line vue/no-mutating-props + this.item.pill_count = value; } }, }, @@ -258,7 +264,7 @@ export default { 'hide-on-focus-or-hover--target transition-opacity-on-hover--target': isPinnable, }" > - {{ pillCount }} + {{ pillData }} </gl-badge> </span> </component> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js new file mode 100644 index 00000000000..7458a2503e8 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js @@ -0,0 +1,86 @@ +import createMockApollo from 'helpers/mock_apollo_helper'; +import rebaseStateQuery from '../../queries/states/rebase.query.graphql'; +import Rebase from './rebase.vue'; + +const service = { + rebase: () => new Promise(() => {}), +}; + +const defaultRender = ({ apolloProvider, check, mr, canCreatePipelineInTargetProject }) => ({ + components: { Rebase }, + apolloProvider, + provide: { + canCreatePipelineInTargetProject, + }, + data() { + return { service, mr: { ...mr, targetProjectFullPath: 'gitlab-org/gitlab' }, check }; + }, + template: '<rebase :mr="mr" :service="service" :check="check" />', +}); + +const Template = ({ + failed, + pushToSourceBranch, + rebaseInProgress, + onlyAllowMergeIfPipelineSucceeds, + canCreatePipelineInTargetProject, +}) => { + const requestHandlers = [ + [ + rebaseStateQuery, + () => + Promise.resolve({ + data: { + project: { + id: '1', + mergeRequest: { + id: '2', + rebaseInProgress, + targetBranch: 'main', + userPermissions: { + pushToSourceBranch, + }, + pipelines: { + nodes: [ + { + id: '1', + project: { + id: '2', + fullPath: 'gitlab/gitlab', + }, + }, + ], + }, + }, + }, + }, + }), + ], + ]; + const apolloProvider = createMockApollo(requestHandlers); + + return defaultRender({ + apolloProvider, + check: { + failureReason: 'Needs rebasing', + identifier: 'rebase', + result: failed ? 'failed' : 'passed', + }, + mr: { onlyAllowMergeIfPipelineSucceeds }, + canCreatePipelineInTargetProject, + }); +}; + +export const Default = Template.bind({}); +Default.args = { + failed: true, + pushToSourceBranch: true, + rebaseInProgress: false, + onlyAllowMergeIfPipelineSucceeds: false, + canCreatePipelineInTargetProject: false, +}; + +export default { + title: 'vue_merge_request_widget/merge_checks/rebase', + component: Rebase, +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue new file mode 100644 index 00000000000..823a30c7063 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue @@ -0,0 +1,218 @@ +<script> +import { GlModal, GlLink } from '@gitlab/ui'; +import { s__, __ } from '~/locale'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import { createAlert } from '~/alert'; +import toast from '~/vue_shared/plugins/global_toast'; +import simplePoll from '~/lib/utils/simple_poll'; +import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; +import rebaseQuery from '../../queries/states/rebase.query.graphql'; +import eventHub from '../../event_hub'; +import ActionButtons from '../action_buttons.vue'; +import MergeChecksMessage from './message.vue'; + +export default { + name: 'MergeChecksRebase', + components: { + GlModal, + GlLink, + MergeChecksMessage, + ActionButtons, + }, + mixins: [mergeRequestQueryVariablesMixin], + apollo: { + state: { + query: rebaseQuery, + variables() { + return this.mergeRequestQueryVariables; + }, + update: (data) => data.project.mergeRequest, + }, + }, + inject: { + canCreatePipelineInTargetProject: { + default: false, + }, + }, + props: { + check: { + type: Object, + required: true, + }, + mr: { + type: Object, + required: false, + default: () => ({}), + }, + service: { + type: Object, + required: false, + default: () => ({}), + }, + }, + data() { + return { + state: {}, + isMakingRequest: false, + }; + }, + computed: { + isLoading() { + return this.$apollo.queries.state.loading; + }, + rebaseInProgress() { + return this.state.rebaseInProgress; + }, + showRebaseWithoutPipeline() { + return ( + !this.mr.onlyAllowMergeIfPipelineSucceeds || + (this.mr.onlyAllowMergeIfPipelineSucceeds && this.mr.allowMergeOnSkippedPipeline) + ); + }, + isForkMergeRequest() { + return this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath; + }, + isLatestPipelineCreatedInTargetProject() { + const latestPipeline = this.state.pipelines.nodes[0]; + + return latestPipeline?.project?.fullPath === this.mr.targetProjectFullPath; + }, + shouldShowSecurityWarning() { + return ( + this.canCreatePipelineInTargetProject && + this.isForkMergeRequest && + !this.isLatestPipelineCreatedInTargetProject + ); + }, + tertiaryActionsButtons() { + if (this.check.result === 'success') return []; + + return [ + { + text: s__('mrWidget|Rebase'), + loading: this.isMakingRequest || this.rebaseInProgress, + testId: 'standard-rebase-button', + onClick: () => this.tryRebase(), + }, + this.showRebaseWithoutPipeline && { + text: s__('mrWidget|Rebase without pipeline'), + loading: this.isMakingRequest || this.rebaseInProgress, + testId: 'rebase-without-ci-button', + onClick: () => this.rebaseWithoutCi(), + }, + ].filter((b) => b); + }, + }, + methods: { + rebase({ skipCi = false } = {}) { + this.isMakingRequest = true; + + this.service + .rebase({ skipCi }) + .then(() => simplePoll(this.checkRebaseStatus)) + .catch((error) => { + this.isMakingRequest = false; + + if (!error.response?.data?.merge_error) { + createAlert({ + message: __('Something went wrong. Please try again.'), + }); + } + }); + }, + rebaseWithoutCi() { + return this.rebase({ skipCi: true }); + }, + tryRebase() { + if (this.shouldShowSecurityWarning) { + this.$refs.modal.show(); + } else { + this.rebase(); + } + }, + checkRebaseStatus(continuePolling, stopPolling) { + this.service + .poll() + .then((res) => res.data) + .then((res) => { + if (res.rebase_in_progress || res.should_be_rebased) { + continuePolling(); + } else { + this.isMakingRequest = false; + + if (!res.merge_error?.length) { + toast(__('Rebase completed')); + } + + eventHub.$emit('MRWidgetRebaseSuccess'); + stopPolling(); + } + }) + .catch(() => { + this.isMakingRequest = false; + createAlert({ + message: __('Something went wrong. Please try again.'), + }); + stopPolling(); + }); + }, + }, + modal: { + id: 'rebase-security-risk-modal', + title: s__('mrWidget|Are you sure you want to rebase?'), + actionPrimary: { + text: s__('mrWidget|Rebase'), + attributes: { + variant: 'danger', + }, + }, + actionCancel: { + text: __('Cancel'), + attributes: { + variant: 'default', + }, + }, + }, + runPipelinesInTheParentProjectHelpPath: helpPagePath( + '/ci/pipelines/merge_request_pipelines.html', + { + anchor: 'run-pipelines-in-the-parent-project', + }, + ), +}; +</script> + +<template> + <merge-checks-message :check="check"> + <action-buttons v-if="!isLoading" :tertiary-buttons="tertiaryActionsButtons" /> + <gl-modal + ref="modal" + :modal-id="$options.modal.id" + :title="$options.modal.title" + :action-primary="$options.modal.actionPrimary" + :action-cancel="$options.modal.actionCancel" + @primary="rebase" + > + <p> + {{ + s__( + 'Pipelines|Rebasing creates a pipeline that runs code originating from a forked project merge request. Consequently there are potential security implications, such as the exposure of CI variables.', + ) + }} + </p> + <p> + {{ + s__( + "Pipelines|You should review the code thoroughly before running this pipeline with the parent project's CI/CD resources.", + ) + }} + </p> + <p> + {{ s__('Pipelines|If you are unsure, ask a project maintainer to review it for you.') }} + </p> + <gl-link :href="$options.runPipelinesInTheParentProjectHelpPath" target="_blank"> + {{ s__('Pipelines|More Information') }} + </gl-link> + </gl-modal> + </merge-checks-message> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js index 1c57226f887..a9745f3214c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js @@ -15,7 +15,7 @@ const defaultRender = (apolloProvider) => ({ components: { MergeChecks }, apolloProvider, data() { - return { mr: { conflictResolutionPath: 'https://gitlab.com' } }; + return { service: {}, mr: { conflictResolutionPath: 'https://gitlab.com' } }; }, template: '<merge-checks :mr="mr" />', }); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue index fa84c0a4a6f..5652b81386f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue @@ -8,6 +8,7 @@ import BoldText from './bold_text.vue'; const COMPONENTS = { conflicts: () => import('./checks/conflicts.vue'), + rebase: () => import('./checks/rebase.vue'), default: () => import('./checks/message.vue'), }; @@ -35,6 +36,10 @@ export default { type: Object, required: true, }, + service: { + type: Object, + required: true, + }, }, data() { return { @@ -122,6 +127,7 @@ export default { }" :check="check" :mr="mr" + :service="service" /> </div> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index ac434c5be4e..ac7e44364d8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -730,6 +730,9 @@ export default { class="mr-ready-merge-related-links gl-display-inline" /> </li> + <li v-if="state.autoMergeEnabled" class="gl-line-height-normal"> + {{ s__('mrWidget|Auto-merge enabled') }} + </li> </ul> </div> </div> diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index d2cf9058976..a4724fd7c02 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -729,7 +729,7 @@ module Ci end def artifacts_expired? - artifacts_expire_at && artifacts_expire_at < Time.current + artifacts_expire_at&.past? end def artifacts_expire_in @@ -745,7 +745,7 @@ module Ci def has_expired_locked_archive_artifacts? locked_artifacts? && - artifacts_expire_at.present? && artifacts_expire_at < Time.current + artifacts_expire_at&.past? end def has_expiring_archive_artifacts? diff --git a/app/models/ci/build_trace_metadata.rb b/app/models/ci/build_trace_metadata.rb index c5ad3d19425..525cb08f2ca 100644 --- a/app/models/ci/build_trace_metadata.rb +++ b/app/models/ci/build_trace_metadata.rb @@ -33,7 +33,7 @@ module Ci return false unless archival_attempts_available? return true unless last_archival_attempt_at - last_archival_attempt_at + backoff < Time.current + (last_archival_attempt_at + backoff).past? end def archival_attempts_available? diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 2a346f97958..fe4437a4ad6 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -306,7 +306,7 @@ module Ci end def expired? - expire_at.present? && expire_at < Time.current + expire_at.present? && expire_at.past? end def expiring? diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb index d0085b60d98..b25ee434484 100644 --- a/app/models/concerns/token_authenticatable_strategies/base.rb +++ b/app/models/concerns/token_authenticatable_strategies/base.rb @@ -65,7 +65,7 @@ module TokenAuthenticatableStrategies return false unless expirable? && token_expiration_enforced? exp = expires_at(instance) - !!exp && Time.current > exp + !!exp && exp.past? end def expirable? diff --git a/app/models/group.rb b/app/models/group.rb index c83dd24e98e..919b80ccffb 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -671,15 +671,6 @@ class Group < Namespace members.count end - # Returns all users that are members of projects - # belonging to the current group or sub-groups - def project_users_with_descendants - User - .joins(projects: :group) - .where(namespaces: { id: self_and_descendants.select(:id) }) - .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455") - end - # Return the highest access level for a user # # A special case is handled here when the user is a GitLab admin diff --git a/app/models/system/broadcast_message.rb b/app/models/system/broadcast_message.rb index 06f0115ade6..d959a6339a4 100644 --- a/app/models/system/broadcast_message.rb +++ b/app/models/system/broadcast_message.rb @@ -117,7 +117,7 @@ module System end def ended? - ends_at < Time.current + ends_at.past? end def now? diff --git a/app/models/user.rb b/app/models/user.rb index 4034677509f..5b6d9f3b6e8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1081,7 +1081,7 @@ class User < MainClusterwide::ApplicationRecord def otp_secret_expired? return true unless otp_secret_expires_at - otp_secret_expires_at < Time.current + otp_secret_expires_at.past? end def update_otp_secret! @@ -1446,7 +1446,7 @@ class User < MainClusterwide::ApplicationRecord if !Gitlab.config.ldap.enabled false elsif ldap_user? - !last_credential_check_at || (last_credential_check_at + ldap_sync_time) < Time.current + !last_credential_check_at || (last_credential_check_at + ldap_sync_time).past? else false end @@ -2087,7 +2087,7 @@ class User < MainClusterwide::ApplicationRecord end def password_expired? - !!(password_expires_at && password_expires_at < Time.current) + !!(password_expires_at && password_expires_at.past?) end def password_expired_if_applicable? diff --git a/app/services/boards/lists/move_service.rb b/app/services/boards/lists/move_service.rb index 4bb7b4dbc6d..4715f1276e3 100644 --- a/app/services/boards/lists/move_service.rb +++ b/app/services/boards/lists/move_service.rb @@ -22,8 +22,11 @@ module Boards attr_reader :board, :old_position, :new_position def valid_move? - new_position.present? && new_position != old_position && - new_position >= 0 && new_position <= board.lists.movable.last.position + new_position.present? && new_position != old_position && new_position.between?(0, max_position) + end + + def max_position + board.lists.movable.maximum(:position) end def reorder_intermediate_lists diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb index d599cecc8da..0f0dc297e9a 100644 --- a/app/services/packages/npm/create_package_service.rb +++ b/app/services/packages/npm/create_package_service.rb @@ -12,6 +12,7 @@ module Packages return error('Version is empty.', 400) if version.blank? return error('Attachment data is empty.', 400) if attachment['data'].blank? return error('Package already exists.', 403) if current_package_exists? + return error('Package protected.', 403) if current_package_protected? return error('File is too large.', 400) if file_size_exceeded? package = try_obtain_lease do @@ -56,6 +57,13 @@ module Packages .exists? end + def current_package_protected? + return false if Feature.disabled?(:packages_protected_packages, project) + + user_project_authorization_access_level = current_user.max_member_access_for_project(project.id) + project.package_protection_rules.push_protected_from?(access_level: user_project_authorization_access_level, package_name: name, package_type: :npm) + end + def name params[:name] end diff --git a/app/services/verify_pages_domain_service.rb b/app/services/verify_pages_domain_service.rb index 59c73aa929c..f5dfe13539b 100644 --- a/app/services/verify_pages_domain_service.rb +++ b/app/services/verify_pages_domain_service.rb @@ -79,7 +79,7 @@ class VerifyPagesDomainService < BaseService # A domain is only expired until `disable!` has been called def expired? - domain.enabled_until && domain.enabled_until < Time.current + domain.enabled_until&.past? end def dns_record_present? diff --git a/app/views/clusters/clusters/_provider_details_form.html.haml b/app/views/clusters/clusters/_provider_details_form.html.haml index 4b7164f9845..dfb97263c54 100644 --- a/app/views/clusters/clusters/_provider_details_form.html.haml +++ b/app/views/clusters/clusters/_provider_details_form.html.haml @@ -1,35 +1,35 @@ = gitlab_ui_form_for cluster, url: update_cluster_url_path, html: { class: 'js-provider-details gl-show-field-errors', role: 'form' }, as: :cluster do |field| .form-group - - copy_name_btn = deprecated_clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), - class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields? = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold required' .input-group.gl-field-error-anchor = field.text_field :name, class: 'form-control js-select-on-focus cluster-name', required: true, title: s_('ClusterIntegration|Cluster name is required.'), - readonly: cluster.read_only_kubernetes_platform_fields?, - append: copy_name_btn + readonly: cluster.read_only_kubernetes_platform_fields? + - if cluster.read_only_kubernetes_platform_fields? + .input-group-append + = clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), variant: :default, category: :primary, size: :medium) = field.fields_for :platform_kubernetes, platform do |platform_field| .form-group - - copy_api_url = deprecated_clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'), - class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields? = platform_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-bold required' .input-group.gl-field-error-anchor = platform_field.text_field :api_url, class: 'form-control js-select-on-focus', required: true, title: s_('ClusterIntegration|API URL should be a valid http/https url.'), - readonly: cluster.read_only_kubernetes_platform_fields?, - append: copy_api_url + readonly: cluster.read_only_kubernetes_platform_fields? + - if cluster.read_only_kubernetes_platform_fields? + .input-group-append + = clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'), variant: :default, category: :primary, size: :medium) .form-group - - copy_ca_cert_btn = deprecated_clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), - class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields? = platform_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-bold' - .input-group.gl-field-error-anchor - = platform_field.text_area :ca_cert, class: 'form-control js-select-on-focus', rows: '10', + .input-group.gl-field-error-anchor.markdown-code-block + = platform_field.text_area :ca_cert, class: 'gl-rounded-top-right-base! gl-rounded-bottom-right-base! form-control js-select-on-focus', rows: '10', readonly: cluster.read_only_kubernetes_platform_fields?, - placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), - append: copy_ca_cert_btn + placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)') + - if cluster.read_only_kubernetes_platform_fields? + %copy-code + = clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), variant: :default, category: :primary, size: :medium, class: 'copy-code') .form-group = platform_field.label :token, s_('ClusterIntegration|Enter new Service Token'), class: 'label-bold required' diff --git a/app/views/groups/_invite_members_modal.html.haml b/app/views/groups/_invite_members_modal.html.haml index cd3327ba9ec..d53190948fd 100644 --- a/app/views/groups/_invite_members_modal.html.haml +++ b/app/views/groups/_invite_members_modal.html.haml @@ -3,4 +3,8 @@ .js-invite-members-modal{ data: { is_project: 'false', access_levels: group.access_level_roles.to_json, reload_page_on_submit: current_path?('group_members#index').to_s, - help_link: help_page_url('user/permissions') }.merge(common_invite_modal_dataset(group)).merge(users_filter_data(group)) } + help_link: help_page_url('user/permissions'), + is_signup_enabled: signup_enabled?.to_s, + new_users_url: new_admin_user_url, + is_current_user_admin: current_user&.admin?.to_s, + }.merge(common_invite_modal_dataset(group)).merge(users_filter_data(group)) } diff --git a/app/views/projects/_invite_members_modal.html.haml b/app/views/projects/_invite_members_modal.html.haml index a1b0bdd6c56..8713cb4990a 100644 --- a/app/views/projects/_invite_members_modal.html.haml +++ b/app/views/projects/_invite_members_modal.html.haml @@ -3,4 +3,8 @@ .js-invite-members-modal{ data: { is_project: 'true', access_levels: ProjectMember.permissible_access_level_roles(current_user, project).to_json, reload_page_on_submit: current_path?('project_members#index').to_s, - help_link: help_page_url('user/permissions') }.merge(common_invite_modal_dataset(project)).merge(users_filter_data(project.group)) } + help_link: help_page_url('user/permissions'), + is_signup_enabled: signup_enabled?.to_s, + new_users_url: new_admin_user_url, + is_current_user_admin: current_user&.admin?.to_s, + }.merge(common_invite_modal_dataset(project)).merge(users_filter_data(project.group)) } diff --git a/app/workers/bulk_imports/finish_batched_pipeline_worker.rb b/app/workers/bulk_imports/finish_batched_pipeline_worker.rb index b1f3757e058..b953f8ab786 100644 --- a/app/workers/bulk_imports/finish_batched_pipeline_worker.rb +++ b/app/workers/bulk_imports/finish_batched_pipeline_worker.rb @@ -27,11 +27,6 @@ module BulkImports else tracker.finish! end - - ensure - # This is needed for in-flight migrations. - # It will be remove in https://gitlab.com/gitlab-org/gitlab/-/issues/426299 - ::BulkImports::EntityWorker.perform_async(tracker.entity.id) if job_version.nil? end private diff --git a/app/workers/bulk_imports/pipeline_worker.rb b/app/workers/bulk_imports/pipeline_worker.rb index 24185f43795..2f57c4579cc 100644 --- a/app/workers/bulk_imports/pipeline_worker.rb +++ b/app/workers/bulk_imports/pipeline_worker.rb @@ -34,10 +34,6 @@ module BulkImports fail_tracker(StandardError.new(message)) unless pipeline_tracker.finished? || pipeline_tracker.skipped? end end - ensure - # This is needed for in-flight migrations. - # It will be remove in https://gitlab.com/gitlab-org/gitlab/-/issues/426299 - ::BulkImports::EntityWorker.perform_async(entity_id) if job_version.nil? end private diff --git a/doc/ci/components/index.md b/doc/ci/components/index.md index a3d6d7224e4..8246e1c4073 100644 --- a/doc/ci/components/index.md +++ b/doc/ci/components/index.md @@ -395,7 +395,7 @@ For example: ```yaml include: # include the component located in the current project from the current SHA - - component: gitlab.com/$CI_PROJECT_PATH@$CI_COMMIT_SHA + - component: gitlab.com/$CI_PROJECT_PATH/my-component@$CI_COMMIT_SHA inputs: stage: build diff --git a/doc/development/database/iterating_tables_in_batches.md b/doc/development/database/iterating_tables_in_batches.md index 84b82b16255..44a8c72ea2c 100644 --- a/doc/development/database/iterating_tables_in_batches.md +++ b/doc/development/database/iterating_tables_in_batches.md @@ -523,14 +523,14 @@ and resumed at any point. This capability is demonstrated in the following code stop_at = Time.current + 3.minutes count, last_value = Issue.each_batch_count do - Time.current > stop_at # condition for stopping the counting + stop_at.past? # condition for stopping the counting end # Continue the counting later stop_at = Time.current + 3.minutes count, last_value = Issue.each_batch_count(last_count: count, last_value: last_value) do - Time.current > stop_at + stop_at.past? end ``` diff --git a/doc/development/internal_analytics/index.md b/doc/development/internal_analytics/index.md index 64b9c7af037..d02e366252a 100644 --- a/doc/development/internal_analytics/index.md +++ b/doc/development/internal_analytics/index.md @@ -50,9 +50,53 @@ such as the value of a setting or the count of rows in a database table. - To instrument an event-based metric, see the [internal event tracking quick start guide](internal_event_instrumentation/quick_start.md). - To instrument a metric that observes the GitLab instances state, see [the metrics instrumentation](metrics/metrics_instrumentation.md). -## Data flow +## Data availability For GitLab there is an essential difference in analytics setup between SaaS and self-managed or GitLab Dedicated instances. +On our SaaS instance both individual events and pre-computed metrics are available for analysis. +Additionally for SaaS page views are automatically instrumented. +For self-managed only the metrics instrumenented on the version installed on the instance are available. + +## Data discovery + +The data visualization tools [Sisense](https://about.gitlab.com/handbook/business-technology/data-team/platform/sisensecdt/) and [Tableau](https://about.gitlab.com/handbook/business-technology/data-team/platform/tableau/), +which have access to our Data Warehouse, can be used to query the internal analytics data. + +### Querying metrics + +The following example query returns all values reported for `count_distinct_user_id_from_feature_used_7d` within the last six months and the according `instance_id`: + +```sql +SELECT + date_trunc('week', ping_created_at), + dim_instance_id, + metric_value +FROM common.fct_ping_instance_metric_rolling_6_months --model limited to last 6 months for performance +WHERE metrics_path = 'counts.users_visiting_dashboard_weekly' --set to metric of interest +ORDER BY ping_created_at DESC +``` + +For a list of other metrics tables refer to the [Data Models Cheat Sheet](https://about.gitlab.com/handbook/product/product-analysis/data-model-cheat-sheet/#commonly-used-data-models). + +### Querying events + +The following example query returns the number of daily event occurences for the `feature_used` event. + +```sql +SELECT + behavior_date, + COUNT(*) as event_occurences +FROM common_mart.mart_behavior_structured_event +WHERE event_action = 'feature_used' +AND event_category = 'InternalEventTracking' +AND behavior_date > '2023-08-01' --restricted minimum date for performance +GROUP BY 1 ORDER BY 1 desc +``` + +For a list of other event tables refer to the [Data Models Cheat Sheet](https://about.gitlab.com/handbook/product/product-analysis/data-model-cheat-sheet/#commonly-used-data-models-2). + +## Data flow + On SaaS event records are directly sent to a collection system, called Snowplow, and imported into our data warehouse. Self-managed and GitLab Dedicated instances record event counts locally. Every week, a process called Service Ping sends the current values for all pre-defined and active metrics to our data warehouse. For GitLab.com, metrics are calculated directly in the data warehouse. diff --git a/doc/development/internal_analytics/service_ping/index.md b/doc/development/internal_analytics/service_ping/index.md index bae4e35149d..08e669ab413 100644 --- a/doc/development/internal_analytics/service_ping/index.md +++ b/doc/development/internal_analytics/service_ping/index.md @@ -38,13 +38,8 @@ We use the following terminology to describe the Service Ping components: ### Limitations -- Service Ping does not track frontend events things like page views, link clicks, or user sessions. -- Service Ping focuses only on aggregated backend events. - -Because of these limitations we recommend you: - -- Instrument your products with Snowplow for more detailed analytics on GitLab.com. -- Use Service Ping to track aggregated backend events on self-managed instances. +- Service Ping delivers only [metrics](../index.md#metric), not individual events. +- A metric has to be present and instrumented in the codebase for a GitLab version to be delivered in Service Pings for that version. ## Service Ping request flow @@ -358,14 +353,6 @@ The following is example content of the Service Ping payload. } ``` -## Notable changes - -In GitLab 14.6, [`flavor`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75587) was added to try to detect the underlying managed database variant. -Possible values are "Amazon Aurora PostgreSQL", "PostgreSQL on Amazon RDS", "Cloud SQL for PostgreSQL", -"Azure Database for PostgreSQL - Flexible Server", or "null". - -In GitLab 13.5, `pg_system_id` was added to send the [PostgreSQL system identifier](https://www.2ndquadrant.com/en/blog/support-for-postgresqls-system-identifier-in-barman/). - ## Export Service Ping data Rake tasks exist to export Service Ping data in different formats. @@ -390,105 +377,7 @@ bin/rake gitlab:usage_data:dump_non_sql_in_json bin/rake gitlab:usage_data:dump_sql_in_yaml > ~/Desktop/usage-metrics-2020-09-02.yaml ``` -## Generate Service Ping - -To generate Service Ping, use [Teleport](https://goteleport.com/docs/) or a detached screen session on a remote server. - -### Triggering - -#### Trigger Service Ping with Teleport - -1. Request temporary [access](https://gitlab.com/gitlab-com/runbooks/-/blob/master/docs/teleport/Connect_to_Rails_Console_via_Teleport.md#how-to-use-teleport-to-connect-to-rails-console) to the required environment. -1. After your approval is issued, [access the Rails console](https://gitlab.com/gitlab-com/runbooks/-/blob/master/docs/teleport/Connect_to_Rails_Console_via_Teleport.md#access-approval). -1. Run `GitlabServicePingWorker.new.perform('triggered_from_cron' => false)`. - -#### Trigger Service Ping with a detached screen session - -1. Connect to bastion with agent forwarding: - - ```shell - ssh -A lb-bastion.gprd.gitlab.com - ``` - -1. Create named screen: - - ```shell - screen -S <username>_usage_ping_<date> - ``` - -1. Connect to console host: - - ```shell - ssh $USER-rails@console-01-sv-gprd.c.gitlab-production.internal - ``` - -1. Run: - - ```shell - GitlabServicePingWorker.new.perform('triggered_from_cron' => false) - ``` - -1. To detach from screen, press `ctrl + A`, `ctrl + D`. -1. Exit from bastion: - - ```shell - exit - ``` - -1. Get the metrics duration from logs: - -Search in Google Console logs for `time_elapsed`. [Query example](https://cloudlogging.app.goo.gl/nWheZvD8D3nWazNe6). - -### Verification (After approx 30 hours) - -#### Verify with Teleport - -1. Follow [the steps](https://gitlab.com/gitlab-com/runbooks/-/blob/master/docs/teleport/Connect_to_Rails_Console_via_Teleport.md#how-to-use-teleport-to-connect-to-rails-console) to request a new access to the required environment and connect to the Rails console -1. Check the last payload in `raw_usage_data` table: `RawUsageData.last.payload` -1. Check the when the payload was sent: `RawUsageData.last.sent_at` - -#### Verify using detached screen session - -1. Reconnect to bastion: - - ```shell - ssh -A lb-bastion.gprd.gitlab.com - ``` - -1. Find your screen session: - - ```shell - screen -ls - ``` - -1. Attach to your screen session: - - ```shell - screen -x 14226.mwawrzyniak_usage_ping_2021_01_22 - ``` - -1. Check the last payload in `raw_usage_data` table: - - ```shell - RawUsageData.last.payload - ``` - -1. Check the when the payload was sent: - - ```shell - RawUsageData.last.sent_at - ``` - -### Skip database write operations - -To skip database write operations, DevOps report creation, and storage of usage data payload, pass an optional argument: - -```shell -skip_db_write: -GitlabServicePingWorker.new.perform('triggered_from_cron' => false, 'skip_db_write' => true) -``` - -### Fallback values for Service Ping +## Fallback values for Service Ping We return fallback values in these cases: diff --git a/doc/development/rubocop_development_guide.md b/doc/development/rubocop_development_guide.md index 6568d025ca5..807544b71d4 100644 --- a/doc/development/rubocop_development_guide.md +++ b/doc/development/rubocop_development_guide.md @@ -28,15 +28,51 @@ discussions, nitpicking, or back-and-forth in reviews. The [GitLab Ruby style guide](backend/ruby_style_guide.md) includes a non-exhaustive list of styles that commonly come up in reviews and are not enforced. -By default, we should not -[disable a RuboCop rule inline](https://docs.rubocop.org/rubocop/configuration.html#disabling-cops-within-source-code), because it negates agreed-upon code standards that the rule is attempting to apply to the codebase. - -If you must use inline disable, provide the reason on the MR and ensure the reviewers agree -before merging. - Additionally, we have dedicated [test-specific style guides and best practices](testing_guide/index.md). +## Disabling rules inline + +By default, RuboCop rules should not be +[disabled inline](https://docs.rubocop.org/rubocop/configuration.html#disabling-cops-within-source-code), +because it negates agreed-upon code standards that the rule is attempting to +apply to the codebase. + +If you must use inline disable provide the reason as a code comment in +the same line where the rule is disabled. + +More context can go into code comments above this inline disable comment. To +reduce verbose code comments link a resource (issue, epic, ...) to provide +detailed context. + +For example: + +```ruby +# bad +module Types + module Domain + # rubocop:disable Graphql/AuthorizeTypes + class SomeType < BaseObject + object.public_send(action) # rubocop:disable GitlabSecurity/PublicSend + end + # rubocop:enable Graphql/AuthorizeTypes + end +end + +# good +module Types + module Domain + # rubocop:disable Graphql/AuthorizeTypes -- already authroized in parent entity + class SomeType < BaseObject + # At this point `action` is safe to be used in `public_send`. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/123457890. + object.public_send(action) # rubocop:disable GitlabSecurity/PublicSend -- User input verified + end + # rubocop:enable Graphql/AuthorizeTypes + end +end +``` + ## Creating new RuboCop cops Typically it is better for the linting rules to be enforced programmatically as it diff --git a/doc/user/application_security/policies/scan-result-policies.md b/doc/user/application_security/policies/scan-result-policies.md index d892012c365..bcc0a27d8b1 100644 --- a/doc/user/application_security/policies/scan-result-policies.md +++ b/doc/user/application_security/policies/scan-result-policies.md @@ -257,25 +257,26 @@ You can use this example in the YAML mode of the [Scan Result Policy editor](#sc It corresponds to a single object from the previous example: ```yaml -- name: critical vulnerability CS approvals - description: critical severity level only for container scanning - enabled: true - rules: - - type: scan_finding - branches: - - main - scanners: - - container_scanning - vulnerabilities_allowed: 1 - severity_levels: - - critical - vulnerability_states: - - newly_detected - actions: - - type: require_approval - approvals_required: 1 - user_approvers: - - adalberto.dare +type: scan_result_policy +name: critical vulnerability CS approvals +description: critical severity level only for container scanning +enabled: true +rules: +- type: scan_finding + branches: + - main + scanners: + - container_scanning + vulnerabilities_allowed: 1 + severity_levels: + - critical + vulnerability_states: + - newly_detected +actions: +- type: require_approval + approvals_required: 1 + user_approvers: + - adalberto.dare ``` ## Example situations where scan result policies require additional approval diff --git a/gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb b/gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb index 2b3841b8f09..8f1ddfbd578 100644 --- a/gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb +++ b/gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb @@ -44,7 +44,7 @@ module Gitlab if instance_variable_defined?(expiration_key) expire_at = instance_variable_get(expiration_key) - clear_memoization(name) if Time.current > expire_at + clear_memoization(name) if expire_at.past? end if instance_variable_defined?(key) diff --git a/lib/gitlab/auth/two_factor_auth_verifier.rb b/lib/gitlab/auth/two_factor_auth_verifier.rb index fbdfd105ee3..4b66aaf0e6a 100644 --- a/lib/gitlab/auth/two_factor_auth_verifier.rb +++ b/lib/gitlab/auth/two_factor_auth_verifier.rb @@ -36,7 +36,7 @@ module Gitlab return false unless time - two_factor_grace_period.hours.since(time) < Time.current + two_factor_grace_period.hours.since(time).past? end def allow_2fa_bypass_for_provider diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index 5a0ae680ab8..33e74c90115 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -182,12 +182,12 @@ module Gitlab request_count_counter.increment - raise_or_wait_for_rate_limit unless requests_remaining? + raise_or_wait_for_rate_limit('Internal threshold reached') unless requests_remaining? begin with_retry { yield } - rescue ::Octokit::TooManyRequests - raise_or_wait_for_rate_limit + rescue ::Octokit::TooManyRequests => e + raise_or_wait_for_rate_limit(e.response_body) # This retry will only happen when running in sequential mode as we'll # raise an error in parallel mode. @@ -213,11 +213,11 @@ module Gitlab octokit.rate_limit.limit end - def raise_or_wait_for_rate_limit + def raise_or_wait_for_rate_limit(message) rate_limit_counter.increment if parallel? - raise RateLimitError + raise RateLimitError, message else sleep(rate_limit_resets_in) end diff --git a/lib/gitlab/group_search_results.rb b/lib/gitlab/group_search_results.rb index 8ca88859b22..6fe7a0030f0 100644 --- a/lib/gitlab/group_search_results.rb +++ b/lib/gitlab/group_search_results.rb @@ -13,7 +13,7 @@ module Gitlab # rubocop:disable CodeReuse/ActiveRecord def users groups = group.self_and_hierarchy_intersecting_with_user_groups(current_user) - groups = groups.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455") + groups = groups.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/427108") members = GroupMember.where(group: groups).non_invite users = super diff --git a/lib/gitlab/import_export/project/sample/date_calculator.rb b/lib/gitlab/import_export/project/sample/date_calculator.rb index 543fd25d883..0cb0eb32a23 100644 --- a/lib/gitlab/import_export/project/sample/date_calculator.rb +++ b/lib/gitlab/import_export/project/sample/date_calculator.rb @@ -25,7 +25,7 @@ module Gitlab end def calculate_by_closest_date_to_average(date) - return date unless closest_date_to_average && closest_date_to_average < Time.current + return date unless closest_date_to_average && closest_date_to_average.past? date + (Time.current - closest_date_to_average).seconds end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f4e362f05d1..f595aa585ea 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -25736,6 +25736,9 @@ msgstr "" msgid "InviteMembersModal|Add unlimited members with your trial" msgstr "" +msgid "InviteMembersModal|Administrators can %{linkStart}add new users by email manually%{linkEnd}. After they've been added, you can invite them to this group with their username." +msgstr "" + msgid "InviteMembersModal|Cancel" msgstr "" @@ -25769,6 +25772,9 @@ msgstr "" msgid "InviteMembersModal|Inviting a group %{linkStart}adds its members to your project%{linkEnd}, including members who join after the invite. This might put your group over the free %{count} user limit." msgstr "" +msgid "InviteMembersModal|Inviting users by email is disabled" +msgstr "" + msgid "InviteMembersModal|Manage members" msgstr "" @@ -25790,6 +25796,9 @@ msgstr "" msgid "InviteMembersModal|Select a role" msgstr "" +msgid "InviteMembersModal|Select members" +msgstr "" + msgid "InviteMembersModal|Select members or type email addresses" msgstr "" @@ -25816,6 +25825,9 @@ msgstr "" msgid "InviteMembersModal|To invite new users to this top-level group, you must remove existing users. You can still add existing users from the top-level group, including any subgroups and projects." msgstr "" +msgid "InviteMembersModal|Username" +msgstr "" + msgid "InviteMembersModal|Username or email address" msgstr "" @@ -56913,6 +56925,9 @@ msgstr "" msgid "mrWidget|Assign yourself to this issue" msgstr "" +msgid "mrWidget|Auto-merge enabled" +msgstr "" + msgid "mrWidget|Cancel auto-merge" msgstr "" diff --git a/package.json b/package.json index c915abb792a..33826889ae4 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "block-dependencies": "node scripts/frontend/block_dependencies.js", "check:startup_css": "scripts/frontend/startup_css/startup_css_changed.sh", "clean": "rm -rf public/assets tmp/cache/*-loader", - "dev-server": "NODE_OPTIONS=\"--max-old-space-size=5120\" node scripts/frontend/webpack_dev_server.js", + "dev-server": "NODE_OPTIONS=\"${NODE_OPTIONS:=--max-old-space-size=5120}\" node scripts/frontend/webpack_dev_server.js", "file-coverage": "scripts/frontend/file_test_coverage.js", "lint-docs": "scripts/lint-doc.sh", "internal:eslint": "eslint --cache --max-warnings 0 --report-unused-disable-directives --ext .js,.vue,.graphql", @@ -44,9 +44,9 @@ "storybook:build": "yarn --cwd ./storybook build --quiet", "storybook:start": "./scripts/frontend/start_storybook.sh", "swagger:validate": "swagger-cli validate", - "webpack": "NODE_OPTIONS=\"--max-old-space-size=5120\" webpack --config config/webpack.config.js", - "webpack-vendor": "NODE_OPTIONS=\"--max-old-space-size=5120\" webpack --config config/webpack.vendor.config.js", - "webpack-prod": "NODE_OPTIONS=\"--max-old-space-size=5120\" NODE_ENV=production webpack --config config/webpack.config.js" + "webpack": "NODE_OPTIONS=\"${NODE_OPTIONS:=--max-old-space-size=5120}\" webpack --config config/webpack.config.js", + "webpack-vendor": "NODE_OPTIONS=\"${NODE_OPTIONS:=--max-old-space-size=5120}\" webpack --config config/webpack.vendor.config.js", + "webpack-prod": "NODE_OPTIONS=\"${NODE_OPTIONS:=--max-old-space-size=5120}\" NODE_ENV=production webpack --config config/webpack.config.js" }, "dependencies": { "@apollo/client": "^3.5.10", diff --git a/spec/features/projects/issues/email_participants_spec.rb b/spec/features/projects/issues/email_participants_spec.rb index a902c8294d7..215c45351c1 100644 --- a/spec/features/projects/issues/email_participants_spec.rb +++ b/spec/features/projects/issues/email_participants_spec.rb @@ -35,10 +35,13 @@ RSpec.describe 'viewing an issue', :js, feature_category: :service_desk do end context 'when issue is confidential' do + let!(:confidential_issue) { create(:issue, project: project, confidential: true) } + let!(:confidential_note) { create(:note_on_issue, project: project, noteable: confidential_issue) } + let!(:confidential_participants) { create_list(:issue_email_participant, 4, issue: confidential_issue) } + before do - issue.update!(confidential: true) sign_in(user) - visit project_issue_path(project, issue) + visit project_issue_path(project, confidential_issue) end it_behaves_like 'email participants warning in all editors' diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js index cfc2fd65cc1..19b7fad5fc8 100644 --- a/spec/frontend/invite_members/components/invite_members_modal_spec.js +++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js @@ -1,4 +1,4 @@ -import { GlModal, GlSprintf, GlFormGroup, GlCollapse, GlIcon } from '@gitlab/ui'; +import { GlLink, GlModal, GlSprintf, GlFormGroup, GlCollapse, GlIcon } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; import { stubComponent } from 'helpers/stub_component'; @@ -60,6 +60,7 @@ describe('InviteMembersModal', () => { let mock; let trackingSpy; const showToast = jest.fn(); + const newUsersUrl = '/new/users/url'; const expectTracking = (action, label = undefined, property = undefined) => expect(trackingSpy).toHaveBeenCalledWith(INVITE_MEMBER_MODAL_TRACKING_CATEGORY, action, { @@ -68,11 +69,13 @@ describe('InviteMembersModal', () => { property, }); - const createComponent = (props = {}, stubs = {}) => { + const createComponent = (props = {}, stubs = {}, provide = {}) => { wrapper = shallowMountExtended(InviteMembersModal, { provide: { newProjectPath, name: propsData.name, + newUsersUrl, + ...provide, }, propsData: { usersLimitDataset: {}, @@ -129,6 +132,7 @@ describe('InviteMembersModal', () => { const findEmptyInvitesAlert = () => wrapper.findByTestId('empty-invites-alert'); const findMemberErrorAlert = () => wrapper.findByTestId('alert-member-error'); const findMoreInviteErrorsButton = () => wrapper.findByTestId('accordion-button'); + const findEmailSignupDisabledAlert = () => wrapper.findByTestId('email-signup-disabled-alert'); const findUserLimitAlert = () => wrapper.findComponent(UserLimitNotification); const findAccordion = () => wrapper.findComponent(GlCollapse); const findErrorsIcon = () => wrapper.findComponent(GlIcon); @@ -759,6 +763,58 @@ describe('InviteMembersModal', () => { expect(findMemberErrorAlert().exists()).toBe(false); }); }); + + describe('when email signup is not allowed', () => { + beforeEach(() => { + createComponent({}, {}, { isEmailSignupEnabled: false }); + }); + + it('shows the correct form description', () => { + expect(membersFormGroupDescription()).toBe('Select members'); + }); + + it('shows an alert', () => { + expect(findEmailSignupDisabledAlert().text()).toBe( + "Administrators can add new users by email manually. After they've been added, you can invite them to this group with their username.", + ); + }); + + it('does not render a link', () => { + expect(findEmailSignupDisabledAlert().findComponent(GlLink).exists()).toBe(false); + }); + + describe('when the current user is an admin', () => { + beforeEach(() => { + createComponent({}, {}, { isCurrentUserAdmin: true, isEmailSignupEnabled: false }); + }); + + it('shows an alert', () => { + expect(findEmailSignupDisabledAlert().text()).toBe( + "Administrators can add new users by email manually. After they've been added, you can invite them to this group with their username.", + ); + }); + + it('renders a link', () => { + expect(findEmailSignupDisabledAlert().findComponent(GlLink).attributes('href')).toBe( + newUsersUrl, + ); + }); + + describe('when no new users url is provided', () => { + beforeEach(() => { + createComponent( + {}, + {}, + { isCurrentUserAdmin: true, isEmailSignupEnabled: false, newUsersUrl: '' }, + ); + }); + + it('does not render a link', () => { + expect(findEmailSignupDisabledAlert().findComponent(GlLink).exists()).toBe(false); + }); + }); + }); + }); }); describe('when inviting members and non-members in same click', () => { diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js index 925534edd7c..a4b8a8b0197 100644 --- a/spec/frontend/invite_members/components/members_token_select_spec.js +++ b/spec/frontend/invite_members/components/members_token_select_spec.js @@ -157,6 +157,21 @@ describe('MembersTokenSelect', () => { expect(tokenSelector.props('allowUserDefinedTokens')).toBe(result); }); + + describe('when cannot use email token', () => { + beforeEach(() => { + wrapper = createComponent({ canUseEmailToken: false }); + tokenSelector = findTokenSelector(); + + tokenSelector.vm.$emit('text-input', 'foo@bar.com'); + + return nextTick(); + }); + + it('does not allow user defined tokens', () => { + expect(tokenSelector.props('allowUserDefinedTokens')).toBe(false); + }); + }); }); }); diff --git a/spec/frontend/super_sidebar/components/nav_item_spec.js b/spec/frontend/super_sidebar/components/nav_item_spec.js index e6de9b1de22..94eb47887c3 100644 --- a/spec/frontend/super_sidebar/components/nav_item_spec.js +++ b/spec/frontend/super_sidebar/components/nav_item_spec.js @@ -90,6 +90,19 @@ describe('NavItem component', () => { expect(findPill().text()).toBe(initialPillValue); }); }); + + describe('async updating pill prop', () => { + it('re-renders item with when prop pill_count changes', async () => { + createWrapper({ item: { title: 'Foo', pill_count: 0 } }); + + expect(findPill().text()).toBe('0'); + + // https://gitlab.com/gitlab-org/gitlab/-/issues/428246 + // This is testing specific async behaviour that was before missed + await wrapper.setProps({ item: { title: 'Foo', pill_count: 10 } }); + expect(findPill().text()).toBe('10'); + }); + }); }); describe('destroyed', () => { diff --git a/spec/frontend/vue_merge_request_widget/components/checks/rebase_spec.js b/spec/frontend/vue_merge_request_widget/components/checks/rebase_spec.js new file mode 100644 index 00000000000..6b77b869a34 --- /dev/null +++ b/spec/frontend/vue_merge_request_widget/components/checks/rebase_spec.js @@ -0,0 +1,323 @@ +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlModal } from '@gitlab/ui'; +import MergeChecksRebase from '~/vue_merge_request_widget/components/checks/rebase.vue'; +import rebaseQuery from '~/vue_merge_request_widget/queries/states/rebase.query.graphql'; +import eventHub from '~/vue_merge_request_widget/event_hub'; +import toast from '~/vue_shared/plugins/global_toast'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { stubComponent } from 'helpers/stub_component'; + +jest.mock('~/vue_shared/plugins/global_toast'); + +let wrapper; +const showMock = jest.fn(); + +const mockPipelineNodes = [ + { + id: '1', + project: { + id: '2', + fullPath: 'user/forked', + }, + }, +]; + +const mockQueryHandler = ({ + rebaseInProgress = false, + targetBranch = '', + pushToSourceBranch = false, + nodes = mockPipelineNodes, +} = {}) => + jest.fn().mockResolvedValue({ + data: { + project: { + id: '1', + mergeRequest: { + id: '2', + rebaseInProgress, + targetBranch, + userPermissions: { + pushToSourceBranch, + }, + pipelines: { + nodes, + }, + }, + }, + }, + }); + +const createMockApolloProvider = (handler) => { + Vue.use(VueApollo); + + return createMockApollo([[rebaseQuery, handler]]); +}; + +function createWrapper({ propsData = {}, provideData = {}, handler = mockQueryHandler() } = {}) { + wrapper = mountExtended(MergeChecksRebase, { + apolloProvider: createMockApolloProvider(handler), + provide: { + ...provideData, + }, + propsData: { + mr: {}, + service: {}, + check: { + failureReason: '', + result: 'failed', + }, + ...propsData, + }, + stubs: { + GlModal: stubComponent(GlModal, { + methods: { + show: showMock, + }, + }), + }, + }); +} + +describe('Merge request merge checks rebase component', () => { + const findStandardRebaseButton = () => wrapper.findByTestId('standard-rebase-button'); + const findRebaseWithoutCiButton = () => wrapper.findByTestId('rebase-without-ci-button'); + const findModal = () => wrapper.findComponent(GlModal); + + describe('with permissions', () => { + const rebaseMock = jest.fn().mockResolvedValue(); + const pollMock = jest.fn().mockResolvedValue({}); + + describe('Rebase buttons', () => { + it('renders both buttons', async () => { + createWrapper({ + handler: mockQueryHandler({ pushToSourceBranch: true }), + }); + + await waitForPromises(); + + expect(findRebaseWithoutCiButton().exists()).toBe(true); + expect(findStandardRebaseButton().exists()).toBe(true); + }); + + it('starts the rebase when clicking', async () => { + createWrapper({ + propsData: { + service: { + rebase: rebaseMock, + poll: pollMock, + }, + }, + handler: mockQueryHandler({ pushToSourceBranch: true }), + }); + + await waitForPromises(); + + findStandardRebaseButton().vm.$emit('click'); + + expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false }); + }); + + it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => { + createWrapper({ + propsData: { + service: { + rebase: rebaseMock, + poll: pollMock, + }, + }, + handler: mockQueryHandler({ pushToSourceBranch: true }), + }); + + await waitForPromises(); + + findRebaseWithoutCiButton().vm.$emit('click'); + + expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true }); + }); + }); + + describe('Rebase when pipelines must succeed is enabled', () => { + beforeEach(async () => { + createWrapper({ + propsData: { + mr: { + onlyAllowMergeIfPipelineSucceeds: true, + }, + service: { + rebase: rebaseMock, + poll: pollMock, + }, + }, + handler: mockQueryHandler({ pushToSourceBranch: true }), + }); + + await waitForPromises(); + }); + + it('renders only the rebase button', () => { + expect(findRebaseWithoutCiButton().exists()).toBe(false); + expect(findStandardRebaseButton().exists()).toBe(true); + }); + + it('starts the rebase when clicking', async () => { + findStandardRebaseButton().vm.$emit('click'); + + await nextTick(); + + expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false }); + }); + }); + + describe('Rebase when pipelines must succeed and skipped pipelines are considered successful are enabled', () => { + beforeEach(async () => { + createWrapper({ + propsData: { + mr: { + onlyAllowMergeIfPipelineSucceeds: true, + allowMergeOnSkippedPipeline: true, + }, + service: { + rebase: rebaseMock, + poll: pollMock, + }, + }, + handler: mockQueryHandler({ pushToSourceBranch: true }), + }); + + await waitForPromises(); + }); + + it('renders both rebase buttons', () => { + expect(findRebaseWithoutCiButton().exists()).toBe(true); + expect(findStandardRebaseButton().exists()).toBe(true); + }); + + it('starts the rebase when clicking', async () => { + findStandardRebaseButton().vm.$emit('click'); + + await nextTick(); + + expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false }); + }); + + it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => { + findRebaseWithoutCiButton().vm.$emit('click'); + + await nextTick(); + + expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true }); + }); + }); + + describe('security modal', () => { + it('displays modal and rebases after confirming', async () => { + createWrapper({ + propsData: { + mr: { + sourceProjectFullPath: 'user/forked', + targetProjectFullPath: 'root/original', + }, + service: { + rebase: rebaseMock, + poll: pollMock, + }, + }, + provideData: { canCreatePipelineInTargetProject: true }, + handler: mockQueryHandler({ pushToSourceBranch: true }), + }); + + await waitForPromises(); + + findStandardRebaseButton().vm.$emit('click'); + expect(showMock).toHaveBeenCalled(); + + findModal().vm.$emit('primary'); + + expect(rebaseMock).toHaveBeenCalled(); + }); + + it('does not display modal', async () => { + createWrapper({ + propsData: { + mr: { + sourceProjectFullPath: 'user/forked', + targetProjectFullPath: 'root/original', + }, + service: { + rebase: rebaseMock, + poll: pollMock, + }, + }, + provideData: { canCreatePipelineInTargetProject: false }, + handler: mockQueryHandler({ pushToSourceBranch: true }), + }); + + await waitForPromises(); + + findStandardRebaseButton().vm.$emit('click'); + + expect(showMock).not.toHaveBeenCalled(); + expect(rebaseMock).toHaveBeenCalled(); + }); + }); + }); + + describe('without permissions', () => { + const exampleTargetBranch = 'fake-branch-to-test-with'; + + it('does render the "Rebase without pipeline" button', async () => { + createWrapper({ + handler: mockQueryHandler({ + rebaseInProgress: false, + pushToSourceBranch: false, + targetBranch: exampleTargetBranch, + }), + }); + + await waitForPromises(); + + expect(findRebaseWithoutCiButton().exists()).toBe(true); + }); + }); + + describe('methods', () => { + it('checkRebaseStatus', async () => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + createWrapper({ + propsData: { + service: { + rebase() { + return Promise.resolve(); + }, + poll() { + return Promise.resolve({ + data: { + rebase_in_progress: false, + should_be_rebased: false, + merge_error: null, + }, + }); + }, + }, + }, + }); + + await waitForPromises(); + + findRebaseWithoutCiButton().vm.$emit('click'); + + // Wait for the rebase request + await nextTick(); + // Wait for the polling request + await nextTick(); + // Wait for the eventHub to be called + await nextTick(); + + expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetRebaseSuccess'); + expect(toast).toHaveBeenCalledWith('Rebase completed'); + }); + }); +}); diff --git a/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js b/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js index c86fe6d0a10..6224d6e42ee 100644 --- a/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js @@ -31,6 +31,7 @@ function factory({ canMerge = true, mergeChecks = [] } = {}) { apolloProvider, propsData: { mr: {}, + service: {}, }, }); } diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js index 48b86d879ad..9239807ae71 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -831,4 +831,16 @@ describe('ReadyToMerge', () => { }); }); }); + + describe('merge details', () => { + it('shows auto-merge hint when auto merge is set and some checks have failed', () => { + createComponent({ mr: { state: 'mergeChecksFailed', autoMergeEnabled: true } }); + expect(wrapper.text()).toContain('Auto-merge enabled'); + }); + + it("doesn't show auto-merge hint when auto merge is not set", () => { + createComponent({ mr: { autoMergeEnabled: false } }); + expect(wrapper.text()).not.toContain('Auto-merge enabled'); + }); + }); }); diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb index 5f321a15de9..c409ec6983f 100644 --- a/spec/lib/gitlab/github_import/client_spec.rb +++ b/spec/lib/gitlab/github_import/client_spec.rb @@ -278,7 +278,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do client.with_rate_limit do if retries == 0 retries += 1 - raise(Octokit::TooManyRequests) + raise(Octokit::TooManyRequests.new(body: 'primary')) end end @@ -306,6 +306,37 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do expect(client.with_rate_limit { 10 }).to eq(10) end + context 'when threshold is hit' do + it 'raises a RateLimitError with the appropriate message' do + expect(client).to receive(:requests_remaining?).and_return(false) + + expect { client.with_rate_limit } + .to raise_error(Gitlab::GithubImport::RateLimitError, 'Internal threshold reached') + end + end + + context 'when primary rate limit hit' do + let(:limited_block) { -> { raise(Octokit::TooManyRequests.new(body: 'primary')) } } + + it 're-raises a RateLimitError with the appropriate message' do + expect(client).to receive(:requests_remaining?).and_return(true) + + expect { client.with_rate_limit(&limited_block) } + .to raise_error(Gitlab::GithubImport::RateLimitError, 'primary') + end + end + + context 'when secondary rate limit hit' do + let(:limited_block) { -> { raise(Octokit::TooManyRequests.new(body: 'secondary')) } } + + it 're-raises a RateLimitError with the appropriate message' do + expect(client).to receive(:requests_remaining?).and_return(true) + + expect { client.with_rate_limit(&limited_block) } + .to raise_error(Gitlab::GithubImport::RateLimitError, 'secondary') + end + end + context 'when Faraday error received from octokit', :aggregate_failures do let(:error_class) { described_class::CLIENT_CONNECTION_ERROR } let(:info_params) { { 'error.class': error_class } } @@ -392,7 +423,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do describe '#raise_or_wait_for_rate_limit' do context 'when running in parallel mode' do it 'raises RateLimitError' do - expect { client.raise_or_wait_for_rate_limit } + expect { client.raise_or_wait_for_rate_limit('primary') } .to raise_error(Gitlab::GithubImport::RateLimitError) end end @@ -404,7 +435,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do expect(client).to receive(:rate_limit_resets_in).and_return(1) expect(client).to receive(:sleep).with(1) - client.raise_or_wait_for_rate_limit + client.raise_or_wait_for_rate_limit('primary') end it 'increments the rate limit counter' do @@ -420,7 +451,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do .to receive(:increment) .and_call_original - client.raise_or_wait_for_rate_limit + client.raise_or_wait_for_rate_limit('primary') end end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 2a5d781edc7..e2e13ea0e17 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2230,7 +2230,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def end context 'when artifacts do not expire' do - it { is_expected.to eq(false) } + it { is_expected.to be_falsey } end context 'when artifacts expire in the future' do diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 96ef36a5b75..0ae4b35af7a 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -2053,29 +2053,6 @@ RSpec.describe Group, feature_category: :groups_and_projects do end end - describe '#project_users_with_descendants' do - let(:user_a) { create(:user) } - let(:user_b) { create(:user) } - let(:user_c) { create(:user) } - - let(:group) { create(:group) } - let(:nested_group) { create(:group, parent: group) } - let(:deep_nested_group) { create(:group, parent: nested_group) } - let(:project_a) { create(:project, namespace: group) } - let(:project_b) { create(:project, namespace: nested_group) } - let(:project_c) { create(:project, namespace: deep_nested_group) } - - it 'returns members of all projects in group and subgroups' do - project_a.add_developer(user_a) - project_b.add_developer(user_b) - project_c.add_developer(user_c) - - expect(group.project_users_with_descendants).to contain_exactly(user_a, user_b, user_c) - expect(nested_group.project_users_with_descendants).to contain_exactly(user_b, user_c) - expect(deep_nested_group.project_users_with_descendants).to contain_exactly(user_c) - end - end - describe '#refresh_members_authorized_projects' do let_it_be(:group) { create(:group, :nested) } let_it_be(:parent_group_user) { create(:user) } diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb index 1c935c27d7f..7336867f5ea 100644 --- a/spec/services/packages/npm/create_package_service_spec.rb +++ b/spec/services/packages/npm/create_package_service_spec.rb @@ -332,6 +332,84 @@ RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_r end end + context 'when feature flag :packages_package_protection is disabled' do + before do + stub_feature_flags(packages_protected_packages: false) + end + + context 'with matching package protection rule for all roles' do + using RSpec::Parameterized::TableSyntax + + let(:package_name_pattern_no_match) { "#{package_name}_no_match" } + + where(:package_name_pattern, :push_protected_up_to_access_level) do + ref(:package_name) | :developer + ref(:package_name) | :owner + ref(:package_name_pattern_no_match) | :developer + ref(:package_name_pattern_no_match) | :owner + end + + with_them do + let!(:package_protection_rule) do + create( + :package_protection_rule, + package_name_pattern: package_name_pattern, + package_type: :npm, + project: project, + push_protected_up_to_access_level: push_protected_up_to_access_level + ) + end + + it_behaves_like 'valid package' + end + end + end + + context 'with package protection rule for different roles and package_name_patterns' do + using RSpec::Parameterized::TableSyntax + + let(:project_developer) { create(:user).tap { |u| project.add_developer(u) } } + let(:project_maintainer) { create(:user).tap { |u| project.add_maintainer(u) } } + let(:project_owner) { project.owner } + + let(:package_name_pattern_no_match) { "#{package_name}_no_match" } + + shared_examples 'protected package' do + it { is_expected.to include http_status: 403, message: 'Package protected.' } + + it 'does not create any npm-related package records' do + expect { subject } + .to not_change { Packages::Package.count } + .and not_change { Packages::Package.npm.count } + .and not_change { Packages::Tag.count } + .and not_change { Packages::Npm::Metadatum.count } + end + end + + where(:package_name_pattern, :push_protected_up_to_access_level, :user, :shared_examples_name) do + ref(:package_name) | :developer | ref(:project_developer) | 'protected package' + ref(:package_name) | :developer | ref(:project_owner) | 'valid package' + ref(:package_name) | :maintainer | ref(:project_maintainer) | 'protected package' + ref(:package_name) | :owner | ref(:project_owner) | 'protected package' + ref(:package_name_pattern_no_match) | :developer | ref(:project_owner) | 'valid package' + ref(:package_name_pattern_no_match) | :owner | ref(:project_owner) | 'valid package' + end + + with_them do + let!(:package_protection_rule) do + create( + :package_protection_rule, + package_name_pattern: package_name_pattern, + package_type: :npm, + project: project, + push_protected_up_to_access_level: push_protected_up_to_access_level + ) + end + + it_behaves_like params[:shared_examples_name] + end + end + def create_packages(project, user, params) with_threads do described_class.new(project, user, params).execute diff --git a/spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb b/spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb index 5beb11c64aa..8c67583f6b5 100644 --- a/spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb +++ b/spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb @@ -13,32 +13,13 @@ RSpec.describe BulkImports::FinishBatchedPipelineWorker, feature_category: :impo subject(:worker) { described_class.new } describe '#perform' do - context 'when job version is nil' do - before do - allow(subject).to receive(:job_version).and_return(nil) - end - - it 'finishes pipeline and enqueues entity worker' do - expect(BulkImports::EntityWorker).to receive(:perform_async) - .with(entity.id) - - subject.perform(pipeline_tracker.id) - - expect(pipeline_tracker.reload.finished?).to eq(true) - end - end - - context 'when job version is present' do - it 'finishes pipeline and does not enqueues entity worker' do - expect(BulkImports::EntityWorker).not_to receive(:perform_async) - - subject.perform(pipeline_tracker.id) - - expect(pipeline_tracker.reload.finished?).to eq(true) + context 'when import is in progress' do + it 'marks the pipeline as finished' do + expect { subject.perform(pipeline_tracker.id) } + .to change { pipeline_tracker.reload.finished? } + .from(false).to(true) end - end - context 'when import is in progress' do it 're-enqueues for any started batches' do create(:bulk_import_batch_tracker, :started, tracker: pipeline_tracker) diff --git a/spec/workers/bulk_imports/pipeline_worker_spec.rb b/spec/workers/bulk_imports/pipeline_worker_spec.rb index e1259d5666d..2042300edf0 100644 --- a/spec/workers/bulk_imports/pipeline_worker_spec.rb +++ b/spec/workers/bulk_imports/pipeline_worker_spec.rb @@ -63,36 +63,6 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do expect(pipeline_tracker.jid).to eq('jid') end - context 'when job version is nil' do - before do - allow(subject).to receive(:job_version).and_return(nil) - end - - it 'runs the given pipeline successfully and enqueues entity worker' do - expect(BulkImports::EntityWorker).to receive(:perform_async).with(entity.id) - - subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id) - - pipeline_tracker.reload - - expect(pipeline_tracker.status_name).to eq(:finished) - end - - context 'when an error occurs' do - it 'enqueues entity worker' do - expect_next_instance_of(pipeline_class) do |pipeline| - expect(pipeline) - .to receive(:run) - .and_raise(StandardError, 'Error!') - end - - expect(BulkImports::EntityWorker).to receive(:perform_async).with(entity.id) - - subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id) - end - end - end - context 'when exclusive lease cannot be obtained' do it 'does not run the pipeline' do expect(subject).to receive(:try_obtain_lease).and_return(false) diff --git a/vendor/gems/devise-pbkdf2-encryptable/Gemfile.lock b/vendor/gems/devise-pbkdf2-encryptable/Gemfile.lock index 617ee7d91f5..47191da2a01 100644 --- a/vendor/gems/devise-pbkdf2-encryptable/Gemfile.lock +++ b/vendor/gems/devise-pbkdf2-encryptable/Gemfile.lock @@ -3,85 +3,115 @@ PATH specs: devise-pbkdf2-encryptable (0.0.0) devise (~> 4.0) - devise-two-factor (~> 4.0) + devise-two-factor (~> 4.1.1) GEM remote: https://rubygems.org/ specs: - actionpack (6.1.6) - actionview (= 6.1.6) - activesupport (= 6.1.6) - rack (~> 2.0, >= 2.0.9) + actionpack (7.1.1) + actionview (= 7.1.1) + activesupport (= 7.1.1) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actionview (6.1.6) - activesupport (= 6.1.6) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actionview (7.1.1) + activesupport (= 7.1.1) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activemodel (6.1.6) - activesupport (= 6.1.6) - activesupport (6.1.6) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activemodel (7.1.1) + activesupport (= 7.1.1) + activesupport (7.1.1) + base64 + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) minitest (>= 5.1) + mutex_m tzinfo (~> 2.0) - zeitwerk (~> 2.3) - attr_encrypted (3.1.0) + attr_encrypted (4.0.0) encryptor (~> 3.0.0) - bcrypt (3.1.18) + base64 (0.1.1) + bcrypt (3.1.19) + bigdecimal (3.1.4) builder (3.2.4) - concurrent-ruby (1.1.10) + concurrent-ruby (1.2.2) + connection_pool (2.4.1) crass (1.0.6) - devise (4.8.1) + devise (4.9.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-two-factor (4.0.2) - activesupport (< 7.1) - attr_encrypted (>= 1.3, < 4, != 2) + devise-two-factor (4.1.1) + activesupport (~> 7.0) + attr_encrypted (>= 1.3, < 5, != 2) devise (~> 4.0) - railties (< 7.1) + railties (~> 7.0) rotp (~> 6.0) diff-lcs (1.5.0) + drb (2.1.1) + ruby2_keywords encryptor (3.0.0) - erubi (1.10.0) - i18n (1.10.0) + erubi (1.12.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) - loofah (2.18.0) + io-console (0.6.0) + irb (1.8.3) + rdoc + reline (>= 0.3.8) + loofah (2.21.4) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - method_source (1.0.0) - mini_portile2 (2.8.0) - minitest (5.16.0) - nokogiri (1.13.6) - mini_portile2 (~> 2.8.0) + nokogiri (>= 1.12.0) + mini_portile2 (2.8.4) + minitest (5.20.0) + mutex_m (0.1.2) + nokogiri (1.15.4) + mini_portile2 (~> 2.8.2) racc (~> 1.4) orm_adapter (0.5.0) - racc (1.6.0) - rack (2.2.3.1) - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + psych (5.1.1.1) + stringio + racc (1.7.1) + rack (3.0.8) + rack-session (2.0.0) + rack (>= 3.0.0) + rack-test (2.1.0) + rack (>= 1.3) + rackup (2.1.0) + rack (>= 3) + webrick (~> 1.8) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.4.3) - loofah (~> 2.3) - railties (6.1.6) - actionpack (= 6.1.6) - activesupport (= 6.1.6) - method_source + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.1.1) + actionpack (= 7.1.1) + activesupport (= 7.1.1) + irb + rackup (>= 1.0.0) rake (>= 12.2) - thor (~> 1.0) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) rake (13.0.6) - responders (3.0.1) - actionpack (>= 5.0) - railties (>= 5.0) - rotp (6.2.0) + rdoc (6.5.0) + psych (>= 4.0.0) + reline (0.3.9) + io-console (~> 0.5) + responders (3.1.1) + actionpack (>= 5.2) + railties (>= 5.2) + rotp (6.3.0) rspec (3.10.0) rspec-core (~> 3.10.0) rspec-expectations (~> 3.10.0) @@ -95,18 +125,21 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.10.0) rspec-support (3.10.3) - thor (1.2.1) - tzinfo (2.0.4) + ruby2_keywords (0.0.5) + stringio (3.0.8) + thor (1.2.2) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) warden (1.2.9) rack (>= 2.0.9) - zeitwerk (2.6.0) + webrick (1.8.1) + zeitwerk (2.6.12) PLATFORMS ruby DEPENDENCIES - activemodel (~> 6.1, < 8) + activemodel (~> 7.0, < 8) devise-pbkdf2-encryptable! rspec (~> 3.10.0) diff --git a/vendor/gems/devise-pbkdf2-encryptable/devise-pbkdf2-encryptable.gemspec b/vendor/gems/devise-pbkdf2-encryptable/devise-pbkdf2-encryptable.gemspec index 9c7e3dd5af5..cd2c62b457d 100644 --- a/vendor/gems/devise-pbkdf2-encryptable/devise-pbkdf2-encryptable.gemspec +++ b/vendor/gems/devise-pbkdf2-encryptable/devise-pbkdf2-encryptable.gemspec @@ -19,8 +19,8 @@ Gem::Specification.new do |spec| spec.version = '0.0.0' spec.add_runtime_dependency 'devise', '~> 4.0' - spec.add_runtime_dependency 'devise-two-factor', '~> 4.0' + spec.add_runtime_dependency 'devise-two-factor', '~> 4.1.1' - spec.add_development_dependency 'activemodel', '~> 6.1', '< 8' + spec.add_development_dependency 'activemodel', '~> 7.0', '< 8' spec.add_development_dependency 'rspec', '~> 3.10.0' end |