diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-01 09:09:59 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-01 09:09:59 +0300 |
commit | a7e81add72ecfbbf888ec4f73debdc2647b059de (patch) | |
tree | 3139b36451667bdd07181ef35044bd9903424fb1 | |
parent | 4ed4dc08a806773e5dc326fc0c18bda6f6ea7af7 (diff) |
Add latest changes from gitlab-org/gitlab@master
62 files changed, 818 insertions, 490 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index ac663669a67..5b06b4f3f5b 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -87acab96b9eb16381a49f2c08a2eaa9664a2fa75 +3f5e218def93024f3aafe590c22cd1b29f744105 diff --git a/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue b/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue index 5e8e1a76182..ec252878e93 100644 --- a/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue +++ b/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue @@ -5,7 +5,7 @@ import { GlSprintf, GlLink, GlToggle, - GlDeprecatedButton, + GlButton, GlDeprecatedDropdown, GlDeprecatedDropdownItem, GlIcon, @@ -25,7 +25,7 @@ export default { GlSprintf, GlLink, GlToggle, - GlDeprecatedButton, + GlButton, GlDeprecatedDropdown, GlDeprecatedDropdownItem, GlIcon, @@ -232,18 +232,24 @@ export default { </gl-deprecated-dropdown> </div> </div> - <div v-if="showButtons" class="mt-3"> - <gl-deprecated-button - class="btn-success inline mr-1" + <div v-if="showButtons" class="gl-mt-5 gl-display-flex"> + <gl-button + variant="success" + category="primary" + data-qa-selector="save_ingress_modsecurity_settings" :loading="saving" :disabled="saveButtonDisabled" @click="updateApplication" > {{ saveButtonLabel }} - </gl-deprecated-button> - <gl-deprecated-button :disabled="saveButtonDisabled" @click="resetStatus"> + </gl-button> + <gl-button + data-qa-selector="cancel_ingress_modsecurity_settings" + :disabled="saveButtonDisabled" + @click="resetStatus" + > {{ __('Cancel') }} - </gl-deprecated-button> + </gl-button> </div> </div> </div> diff --git a/app/assets/javascripts/registry/settings/registry_settings_bundle.js b/app/assets/javascripts/registry/settings/registry_settings_bundle.js index 418483fdb41..5f25d508e2f 100644 --- a/app/assets/javascripts/registry/settings/registry_settings_bundle.js +++ b/app/assets/javascripts/registry/settings/registry_settings_bundle.js @@ -1,7 +1,6 @@ import Vue from 'vue'; import { GlToast } from '@gitlab/ui'; import Translate from '~/vue_shared/translate'; -import store from './store'; import RegistrySettingsApp from './components/registry_settings_app.vue'; import { apolloProvider } from './graphql/index'; @@ -13,11 +12,9 @@ export default () => { if (!el) { return null; } - store.dispatch('setInitialState', el.dataset); const { projectPath, isAdmin, adminSettingsPath, enableHistoricEntries } = el.dataset; return new Vue({ el, - store, apolloProvider, components: { RegistrySettingsApp, diff --git a/app/assets/javascripts/registry/settings/store/actions.js b/app/assets/javascripts/registry/settings/store/actions.js deleted file mode 100644 index 0530a870ecc..00000000000 --- a/app/assets/javascripts/registry/settings/store/actions.js +++ /dev/null @@ -1,30 +0,0 @@ -import Api from '~/api'; -import * as types from './mutation_types'; - -export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data); -export const updateSettings = ({ commit }, data) => commit(types.UPDATE_SETTINGS, data); -export const toggleLoading = ({ commit }) => commit(types.TOGGLE_LOADING); -export const receiveSettingsSuccess = ({ commit }, data) => { - commit(types.SET_SETTINGS, data); -}; -export const resetSettings = ({ commit }) => commit(types.RESET_SETTINGS); - -export const fetchSettings = ({ dispatch, state }) => { - dispatch('toggleLoading'); - return Api.project(state.projectId) - .then(({ data: { container_expiration_policy } }) => - dispatch('receiveSettingsSuccess', container_expiration_policy), - ) - .finally(() => dispatch('toggleLoading')); -}; - -export const saveSettings = ({ dispatch, state }) => { - dispatch('toggleLoading'); - return Api.updateProject(state.projectId, { - container_expiration_policy_attributes: state.settings, - }) - .then(({ data: { container_expiration_policy } }) => - dispatch('receiveSettingsSuccess', container_expiration_policy), - ) - .finally(() => dispatch('toggleLoading')); -}; diff --git a/app/assets/javascripts/registry/settings/store/getters.js b/app/assets/javascripts/registry/settings/store/getters.js deleted file mode 100644 index ac1a931d8e0..00000000000 --- a/app/assets/javascripts/registry/settings/store/getters.js +++ /dev/null @@ -1,26 +0,0 @@ -import { isEqual } from 'lodash'; -import { findDefaultOption } from '../../shared/utils'; - -export const getCadence = state => - state.settings.cadence || findDefaultOption(state.formOptions.cadence); - -export const getKeepN = state => - state.settings.keep_n || findDefaultOption(state.formOptions.keepN); - -export const getOlderThan = state => - state.settings.older_than || findDefaultOption(state.formOptions.olderThan); - -export const getSettings = (state, getters) => ({ - enabled: state.settings.enabled, - cadence: getters.getCadence, - older_than: getters.getOlderThan, - keep_n: getters.getKeepN, - name_regex: state.settings.name_regex, - name_regex_keep: state.settings.name_regex_keep, -}); - -export const getIsEdited = state => !isEqual(state.original, state.settings); - -export const getIsDisabled = state => { - return !(state.original || state.enableHistoricEntries); -}; diff --git a/app/assets/javascripts/registry/settings/store/index.js b/app/assets/javascripts/registry/settings/store/index.js deleted file mode 100644 index c2500454d8e..00000000000 --- a/app/assets/javascripts/registry/settings/store/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import Vue from 'vue'; -import Vuex from 'vuex'; -import * as actions from './actions'; -import mutations from './mutations'; -import * as getters from './getters'; -import state from './state'; - -Vue.use(Vuex); - -export const createStore = () => - new Vuex.Store({ - state, - actions, - mutations, - getters, - }); - -export default createStore(); diff --git a/app/assets/javascripts/registry/settings/store/mutation_types.js b/app/assets/javascripts/registry/settings/store/mutation_types.js deleted file mode 100644 index db499ffa761..00000000000 --- a/app/assets/javascripts/registry/settings/store/mutation_types.js +++ /dev/null @@ -1,5 +0,0 @@ -export const SET_INITIAL_STATE = 'SET_INITIAL_STATE'; -export const UPDATE_SETTINGS = 'UPDATE_SETTINGS'; -export const TOGGLE_LOADING = 'TOGGLE_LOADING'; -export const SET_SETTINGS = 'SET_SETTINGS'; -export const RESET_SETTINGS = 'RESET_SETTINGS'; diff --git a/app/assets/javascripts/registry/settings/store/mutations.js b/app/assets/javascripts/registry/settings/store/mutations.js deleted file mode 100644 index 3ba13419b98..00000000000 --- a/app/assets/javascripts/registry/settings/store/mutations.js +++ /dev/null @@ -1,29 +0,0 @@ -import { parseBoolean } from '~/lib/utils/common_utils'; -import * as types from './mutation_types'; - -export default { - [types.SET_INITIAL_STATE](state, initialState) { - state.projectId = initialState.projectId; - state.formOptions = { - cadence: JSON.parse(initialState.cadenceOptions), - keepN: JSON.parse(initialState.keepNOptions), - olderThan: JSON.parse(initialState.olderThanOptions), - }; - state.enableHistoricEntries = parseBoolean(initialState.enableHistoricEntries); - state.isAdmin = parseBoolean(initialState.isAdmin); - state.adminSettingsPath = initialState.adminSettingsPath; - }, - [types.UPDATE_SETTINGS](state, data) { - state.settings = { ...state.settings, ...data.settings }; - }, - [types.SET_SETTINGS](state, settings) { - state.settings = settings ?? state.settings; - state.original = Object.freeze(settings); - }, - [types.RESET_SETTINGS](state) { - state.settings = { ...state.original }; - }, - [types.TOGGLE_LOADING](state) { - state.isLoading = !state.isLoading; - }, -}; diff --git a/app/assets/javascripts/registry/settings/store/state.js b/app/assets/javascripts/registry/settings/store/state.js deleted file mode 100644 index fccc0991c1c..00000000000 --- a/app/assets/javascripts/registry/settings/store/state.js +++ /dev/null @@ -1,42 +0,0 @@ -export default () => ({ - /* - * Project Id used to build the API call - */ - projectId: '', - /* - * Boolean to determine if the UI is loading data from the API - */ - isLoading: false, - /* - * Boolean to determine if the user is an admin - */ - isAdmin: false, - /* - * String containing the full path to the admin config page for CI/CD - */ - adminSettingsPath: '', - /* - * Boolean to determine if project created before 12.8 can use this feature - */ - enableHistoricEntries: false, - /* - * This contains the data shown and manipulated in the UI - * Has the following structure: - * { - * enabled: Boolean - * cadence: String, - * older_than: String, - * keep_n: String, - * name_regex: String - * } - */ - settings: {}, - /* - * Same structure as settings, above but Frozen object and used only in case the user clicks 'cancel', initialized to null - */ - original: null, - /* - * Contains the options used to populate the form selects - */ - formOptions: {}, -}); diff --git a/app/assets/javascripts/vue_shared/components/members/avatars/user_avatar.vue b/app/assets/javascripts/vue_shared/components/members/avatars/user_avatar.vue index 3d61cdff747..4cd74305450 100644 --- a/app/assets/javascripts/vue_shared/components/members/avatars/user_avatar.vue +++ b/app/assets/javascripts/vue_shared/components/members/avatars/user_avatar.vue @@ -1,5 +1,11 @@ <script> -import { GlAvatarLink, GlAvatarLabeled, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; +import { + GlAvatarLink, + GlAvatarLabeled, + GlBadge, + GlSafeHtmlDirective as SafeHtml, +} from '@gitlab/ui'; +import { generateBadges } from 'ee_else_ce/vue_shared/components/members/utils'; import { __ } from '~/locale'; import { AVATAR_SIZE } from '../constants'; @@ -7,7 +13,11 @@ export default { name: 'UserAvatar', avatarSize: AVATAR_SIZE, orphanedUserLabel: __('Orphaned member'), - components: { GlAvatarLink, GlAvatarLabeled }, + components: { + GlAvatarLink, + GlAvatarLabeled, + GlBadge, + }, directives: { SafeHtml, }, @@ -16,11 +26,18 @@ export default { type: Object, required: true, }, + isCurrentUser: { + type: Boolean, + required: true, + }, }, computed: { user() { return this.member.user; }, + badges() { + return generateBadges(this.member, this.isCurrentUser).filter(badge => badge.show); + }, }, }; </script> @@ -41,7 +58,15 @@ export default { :size="$options.avatarSize" :entity-name="user.name" :entity-id="user.id" - /> + > + <template #meta> + <div v-for="badge in badges" :key="badge.text" class="gl-p-1"> + <gl-badge size="sm" :variant="badge.variant"> + {{ badge.text }} + </gl-badge> + </div> + </template> + </gl-avatar-labeled> </gl-avatar-link> <gl-avatar-labeled diff --git a/app/assets/javascripts/vue_shared/components/members/table/member_avatar.vue b/app/assets/javascripts/vue_shared/components/members/table/member_avatar.vue index 3abd86fa37f..a1f98d4008a 100644 --- a/app/assets/javascripts/vue_shared/components/members/table/member_avatar.vue +++ b/app/assets/javascripts/vue_shared/components/members/table/member_avatar.vue @@ -12,6 +12,10 @@ export default { type: String, required: true, }, + isCurrentUser: { + type: Boolean, + required: true, + }, member: { type: Object, required: true, @@ -27,5 +31,5 @@ export default { </script> <template> - <component :is="avatarComponent" :member="member" /> + <component :is="avatarComponent" :member="member" :is-current-user="isCurrentUser" /> </template> diff --git a/app/assets/javascripts/vue_shared/components/members/table/members_table.vue b/app/assets/javascripts/vue_shared/components/members/table/members_table.vue index 4401250a665..b72633f0cee 100644 --- a/app/assets/javascripts/vue_shared/components/members/table/members_table.vue +++ b/app/assets/javascripts/vue_shared/components/members/table/members_table.vue @@ -44,8 +44,12 @@ export default { show-empty > <template #cell(account)="{ item: member }"> - <members-table-cell #default="{ memberType }" :member="member"> - <member-avatar :member-type="memberType" :member="member" /> + <members-table-cell #default="{ memberType, isCurrentUser }" :member="member"> + <member-avatar + :member-type="memberType" + :is-current-user="isCurrentUser" + :member="member" + /> </members-table-cell> </template> diff --git a/app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue b/app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue index c859217c96f..0688c5d3c9d 100644 --- a/app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue +++ b/app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue @@ -11,7 +11,7 @@ export default { }, }, computed: { - ...mapState(['sourceId']), + ...mapState(['sourceId', 'currentUserId']), isGroup() { return Boolean(this.member.sharedWithGroup); }, @@ -35,11 +35,15 @@ export default { isDirectMember() { return this.member.source?.id === this.sourceId; }, + isCurrentUser() { + return this.member.user?.id === this.currentUserId; + }, }, render() { return this.$scopedSlots.default({ memberType: this.memberType, isDirectMember: this.isDirectMember, + isCurrentUser: this.isCurrentUser, }); }, }; diff --git a/app/assets/javascripts/vue_shared/components/members/utils.js b/app/assets/javascripts/vue_shared/components/members/utils.js new file mode 100644 index 00000000000..782a0b7f96b --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/members/utils.js @@ -0,0 +1,19 @@ +import { __ } from '~/locale'; + +export const generateBadges = (member, isCurrentUser) => [ + { + show: isCurrentUser, + text: __("It's you"), + variant: 'success', + }, + { + show: member.user?.blocked, + text: __('Blocked'), + variant: 'danger', + }, + { + show: member.user?.twoFactorEnabled, + text: __('2FA'), + variant: 'info', + }, +]; diff --git a/app/finders/packages/generic/package_finder.rb b/app/finders/packages/generic/package_finder.rb new file mode 100644 index 00000000000..3a260e11fa3 --- /dev/null +++ b/app/finders/packages/generic/package_finder.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Packages + module Generic + class PackageFinder + def initialize(project) + @project = project + end + + def execute!(package_name, package_version) + project + .packages + .generic + .by_name_and_version!(package_name, package_version) + end + + private + + attr_reader :project + end + end +end diff --git a/app/models/application_setting/term.rb b/app/models/application_setting/term.rb index 723540c9b91..bab036f5697 100644 --- a/app/models/application_setting/term.rb +++ b/app/models/application_setting/term.rb @@ -14,6 +14,8 @@ class ApplicationSetting end def accepted_by_user?(user) + return true if user.project_bot? + user.accepted_term_id == id || term_agreements.accepted.where(user: user).exists? end diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index 61167546b25..caf2522e3dd 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -26,7 +26,7 @@ class Packages::Package < ApplicationRecord validates :project, presence: true validates :name, presence: true - validates :name, format: { with: Gitlab::Regex.package_name_regex }, unless: :conan? + validates :name, format: { with: Gitlab::Regex.package_name_regex }, unless: -> { conan? || generic? } validates :name, uniqueness: { scope: %i[project_id version package_type] }, unless: :conan? @@ -35,8 +35,9 @@ class Packages::Package < ApplicationRecord validate :valid_npm_package_name, if: :npm? validate :valid_composer_global_name, if: :composer? validate :package_already_taken, if: :npm? - validates :version, format: { with: Gitlab::Regex.semver_regex }, if: -> { npm? || nuget? } validates :name, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan? + validates :name, format: { with: Gitlab::Regex.generic_package_name_regex }, if: :generic? + validates :version, format: { with: Gitlab::Regex.semver_regex }, if: -> { npm? || nuget? } validates :version, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan? validates :version, format: { with: Gitlab::Regex.maven_version_regex }, if: -> { version? && maven? } validates :version, format: { with: Gitlab::Regex.pypi_version_regex }, if: :pypi? @@ -120,6 +121,10 @@ class Packages::Package < ApplicationRecord .where(packages_package_files: { file_name: file_name, file_sha256: sha256 }).last! end + def self.by_name_and_version!(name, version) + find_by!(name: name, version: version) + end + def self.pluck_names pluck(:name) end diff --git a/app/models/user.rb b/app/models/user.rb index 9305e2518c1..e229c270e83 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1674,6 +1674,8 @@ class User < ApplicationRecord end def terms_accepted? + return true if project_bot? + accepted_term_id.present? end diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index 8f4c89a9e77..6d2c5870e43 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -1,6 +1,6 @@ - page_title @group.name -- page_description @group.description unless page_description -- header_title group_title(@group) unless header_title +- page_description @group.description_html unless page_description +- header_title group_title(@group) unless header_title - nav "group" - display_subscription_banner! - display_namespace_storage_limit_alert! diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 222ca02b1df..a0c82380023 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -1,6 +1,6 @@ - page_title @project.full_name -- page_description @project.description unless page_description -- header_title project_title(@project) unless header_title +- page_description @project.description_html unless page_description +- header_title project_title(@project) unless header_title - nav "project" - display_subscription_banner! - display_namespace_storage_limit_alert! diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index c16ebd95429..7ee6c2b137a 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -2,7 +2,7 @@ - add_to_breadcrumbs _("Issues"), project_issues_path(@project) - breadcrumb_title @issue.to_reference - page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues") -- page_description @issue.description +- page_description @issue.description_html - page_card_attributes @issue.card_attributes - if @issue.relocation_target - page_canonical_link @issue.relocation_target.present(current_user: current_user).web_url diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index fcb79782acf..84b108d69ad 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -3,7 +3,7 @@ - add_to_breadcrumbs _("Merge Requests"), project_merge_requests_path(@project) - breadcrumb_title @merge_request.to_reference - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", _("Merge Requests") -- page_description @merge_request.description +- page_description @merge_request.description_html - page_card_attributes @merge_request.card_attributes - suggest_changes_help_path = help_page_path('user/discussions/index.md', anchor: 'suggest-changes') - number_of_pipelines = @pipelines.size diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index e7cc75e871a..2514d2cce32 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -1,7 +1,7 @@ - add_to_breadcrumbs _('Milestones'), project_milestones_path(@project) - breadcrumb_title @milestone.title - page_title @milestone.title, _('Milestones') -- page_description @milestone.description +- page_description @milestone.description_html - add_page_specific_style 'page_bundles/milestone' = render 'shared/milestones/header', milestone: @milestone diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index fbda9b79e82..f1733ce2b51 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -2,7 +2,7 @@ - @hide_breadcrumbs = true - @no_container = true - page_title @user.blocked? ? s_('UserProfile|Blocked user') : @user.name -- page_description @user.bio +- page_description @user.bio_html - header_title @user.name, user_path(@user) - link_classes = "flex-grow-1 mx-1 " diff --git a/changelogs/unreleased/nfriend-strip-markdown-from-og-description.yml b/changelogs/unreleased/nfriend-strip-markdown-from-og-description.yml new file mode 100644 index 00000000000..75d53bf26ed --- /dev/null +++ b/changelogs/unreleased/nfriend-strip-markdown-from-og-description.yml @@ -0,0 +1,5 @@ +--- +title: Strip markdown from og:description meta tags +merge_request: 42918 +author: +type: added diff --git a/changelogs/unreleased/okr-ingress-modsecurity-settings.yml b/changelogs/unreleased/okr-ingress-modsecurity-settings.yml new file mode 100644 index 00000000000..b5b22d25a0a --- /dev/null +++ b/changelogs/unreleased/okr-ingress-modsecurity-settings.yml @@ -0,0 +1,5 @@ +--- +title: Migrate deprecated button to GlButton in ingress_modsecurity_settings.vue +merge_request: 43717 +author: +type: other diff --git a/changelogs/unreleased/pat-bot-terms.yml b/changelogs/unreleased/pat-bot-terms.yml new file mode 100644 index 00000000000..dceb285e5e0 --- /dev/null +++ b/changelogs/unreleased/pat-bot-terms.yml @@ -0,0 +1,5 @@ +--- +title: Auto-accept TOS if project bot +merge_request: 43067 +author: +type: fixed diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 08eb9a5d37a..917b94a4f62 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -11641,7 +11641,7 @@ type Mutation { pipelineRetry(input: PipelineRetryInput!): PipelineRetryPayload removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload @deprecated(reason: "Use awardEmojiRemove. Deprecated in 13.2") removeProjectFromSecurityDashboard(input: RemoveProjectFromSecurityDashboardInput!): RemoveProjectFromSecurityDashboardPayload - revertVulnerabilityToDetected(input: RevertVulnerabilityToDetectedInput!): RevertVulnerabilityToDetectedPayload + revertVulnerabilityToDetected(input: RevertVulnerabilityToDetectedInput!): RevertVulnerabilityToDetectedPayload @deprecated(reason: "Use vulnerabilityRevertToDetected. Deprecated in 13.5") runDastScan(input: RunDASTScanInput!): RunDASTScanPayload @deprecated(reason: "Use DastOnDemandScanCreate. Deprecated in 13.4") todoMarkDone(input: TodoMarkDoneInput!): TodoMarkDonePayload todoRestore(input: TodoRestoreInput!): TodoRestorePayload @@ -11673,6 +11673,7 @@ type Mutation { vulnerabilityConfirm(input: VulnerabilityConfirmInput!): VulnerabilityConfirmPayload vulnerabilityDismiss(input: VulnerabilityDismissInput!): VulnerabilityDismissPayload vulnerabilityResolve(input: VulnerabilityResolveInput!): VulnerabilityResolvePayload + vulnerabilityRevertToDetected(input: VulnerabilityRevertToDetectedInput!): VulnerabilityRevertToDetectedPayload } """ @@ -20471,6 +20472,41 @@ type VulnerabilityResolvePayload { } """ +Autogenerated input type of VulnerabilityRevertToDetected +""" +input VulnerabilityRevertToDetectedInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the vulnerability to be reverted + """ + id: VulnerabilityID! +} + +""" +Autogenerated return type of VulnerabilityRevertToDetected +""" +type VulnerabilityRevertToDetectedPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Errors encountered during execution of the mutation. + """ + errors: [String!]! + + """ + The vulnerability after revert + """ + vulnerability: Vulnerability +} + +""" Represents a vulnerability scanner """ type VulnerabilityScanner { diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 54330f60941..0a8fabb1e5b 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -33961,8 +33961,8 @@ "name": "RevertVulnerabilityToDetectedPayload", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null + "isDeprecated": true, + "deprecationReason": "Use vulnerabilityRevertToDetected. Deprecated in 13.5" }, { "name": "runDastScan", @@ -34503,6 +34503,33 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "vulnerabilityRevertToDetected", + "description": null, + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "VulnerabilityRevertToDetectedInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "VulnerabilityRevertToDetectedPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -59576,6 +59603,108 @@ "possibleTypes": null }, { + "kind": "INPUT_OBJECT", + "name": "VulnerabilityRevertToDetectedInput", + "description": "Autogenerated input type of VulnerabilityRevertToDetected", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": "ID of the vulnerability to be reverted", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "VulnerabilityID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VulnerabilityRevertToDetectedPayload", + "description": "Autogenerated return type of VulnerabilityRevertToDetected", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": "Errors encountered during execution of the mutation.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "vulnerability", + "description": "The vulnerability after revert", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Vulnerability", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { "kind": "OBJECT", "name": "VulnerabilityScanner", "description": "Represents a vulnerability scanner", diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 041ab8587b2..7496c3272d7 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -2947,6 +2947,16 @@ Autogenerated return type of VulnerabilityResolve. | `errors` | String! => Array | Errors encountered during execution of the mutation. | | `vulnerability` | Vulnerability | The vulnerability after state change | +### VulnerabilityRevertToDetectedPayload + +Autogenerated return type of VulnerabilityRevertToDetected. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `clientMutationId` | String | A unique identifier for the client performing the mutation. | +| `errors` | String! => Array | Errors encountered during execution of the mutation. | +| `vulnerability` | Vulnerability | The vulnerability after revert | + ### VulnerabilityScanner Represents a vulnerability scanner. diff --git a/doc/ci/introduction/index.md b/doc/ci/introduction/index.md index db2749233e8..12df0751c47 100644 --- a/doc/ci/introduction/index.md +++ b/doc/ci/introduction/index.md @@ -154,7 +154,7 @@ commits to a feature branch in a remote repository in GitLab, the CI/CD pipeline set for your project is triggered. By doing so, GitLab CI/CD: -- Runs automated scripts (sequential or parallel) to: +- Runs automated scripts (sequentially or in parallel) to: - Build and test your app. - Preview the changes per merge request with Review Apps, as you would see in your `localhost`. diff --git a/doc/ci/migration/jenkins.md b/doc/ci/migration/jenkins.md index a7ec085a6b2..1130c11f472 100644 --- a/doc/ci/migration/jenkins.md +++ b/doc/ci/migration/jenkins.md @@ -83,7 +83,7 @@ There are some high level differences between the products worth mentioning: - You can control which jobs run in which cases, depending on how they are triggered, with the [`rules` syntax](../yaml/README.md#rules). -- GitLab [pipeline scheduling concepts](../pipelines/schedules.md) are also different than with Jenkins. +- GitLab [pipeline scheduling concepts](../pipelines/schedules.md) are also different from Jenkins. - You can reuse pipeline configurations using the [`include` keyword](../yaml/README.md#include) and [templates](#templates). Your templates can be kept in a central repository (with different permissions), and then any project can use them. This central project could also diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 9ad21eceb05..ccae41017c4 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -33,11 +33,8 @@ We have complete examples of configuring pipelines: > - <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> Learn how [Verizon reduced rebuilds](https://about.gitlab.com/blog/2019/02/14/verizon-customer-story/) > from 30 days to under 8 hours with GitLab. -NOTE: **Note:** If you have a [mirrored repository that GitLab pulls from](../../user/project/repository/repository_mirroring.md#pulling-from-a-remote-repository), -you may need to enable pipeline triggering. Go to your project's - -**Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**. +you may need to enable pipeline triggering. Go to your project's **Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**. ## Introduction @@ -961,7 +958,7 @@ GitLab performs a reverse deep merge based on the keys. GitLab: - Merges the `rspec` contents into `.tests` recursively. - Doesn't merge the values of the keys. -The result is this `rspec` job: +The result is this `rspec` job, where `script: rake test` is overwritten by `script: rake rspec`: ```yaml rspec: @@ -974,9 +971,6 @@ rspec: - $RSPEC ``` -NOTE: **Note:** -Note that `script: rake test` has been overwritten by `script: rake rspec`. - If you do want to include the `rake test`, see [`before_script` and `after_script`](#before_script-and-after_script). `.tests` in this example is a [hidden job](#hide-jobs), but it's diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index a39f93a26e1..cc6777a1ce7 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -83,9 +83,9 @@ project in a simple and automatic way: 1. [Auto Build](stages.md#auto-build) 1. [Auto Test](stages.md#auto-test) -1. [Auto Code Quality](stages.md#auto-code-quality) **(STARTER)** -1. [Auto SAST (Static Application Security Testing)](stages.md#auto-sast) **(ULTIMATE)** -1. [Auto Secret Detection](stages.md#auto-secret-detection) **(ULTIMATE)** +1. [Auto Code Quality](stages.md#auto-code-quality) +1. [Auto SAST (Static Application Security Testing)](stages.md#auto-sast) +1. [Auto Secret Detection](stages.md#auto-secret-detection) 1. [Auto Dependency Scanning](stages.md#auto-dependency-scanning) **(ULTIMATE)** 1. [Auto License Compliance](stages.md#auto-license-compliance) **(ULTIMATE)** 1. [Auto Container Scanning](stages.md#auto-container-scanning) **(ULTIMATE)** diff --git a/doc/topics/autodevops/stages.md b/doc/topics/autodevops/stages.md index 0e9f0812a9a..44eebb748a6 100644 --- a/doc/topics/autodevops/stages.md +++ b/doc/topics/autodevops/stages.md @@ -124,7 +124,10 @@ The supported buildpacks are: If your application needs a buildpack that is not in the above list, you might want to use a [custom buildpack](customize.md#custom-buildpacks). -## Auto Code Quality **(STARTER)** +## Auto Code Quality + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1984) in [GitLab Starter](https://about.gitlab.com/pricing/) 9.3. +> - Made [available in all tiers](https://gitlab.com/gitlab-org/gitlab/-/issues/212499) in GitLab 13.2. Auto Code Quality uses the [Code Quality image](https://gitlab.com/gitlab-org/ci-cd/codequality) to run @@ -133,9 +136,10 @@ report, it's uploaded as an artifact which you can later download and check out. The merge request widget also displays any [differences between the source and target branches](../../user/project/merge_requests/code_quality.md). -## Auto SAST **(ULTIMATE)** +## Auto SAST -> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.3. +> - Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.3. +> - Select functionality made available in all tiers beginning in 13.1 Static Application Security Testing (SAST) uses the [SAST Docker image](https://gitlab.com/gitlab-org/security-products/sast) to run static @@ -151,9 +155,10 @@ warnings. To learn more about [how SAST works](../../user/application_security/sast/index.md), see the documentation. -## Auto Secret Detection **(ULTIMATE)** +## Auto Secret Detection -> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.1. +> - Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.1. +> - [Select functionality made available in all tiers](../../user/application_security/secret_detection/#making-secret-detection-available-to-all-gitlab-tiers) in 13.3 Secret Detection uses the [Secret Detection Docker image](https://gitlab.com/gitlab-org/security-products/analyzers/secrets) to run Secret Detection on the current code, and checks for leaked secrets. The diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb index a24580b358a..a0c29ada950 100644 --- a/lib/api/generic_packages.rb +++ b/lib/api/generic_packages.rb @@ -30,7 +30,7 @@ module API route_setting :authentication, job_token_allowed: true params do - requires :package_name, type: String, desc: 'Package name' + requires :package_name, type: String, desc: 'Package name', regexp: Gitlab::Regex.generic_package_name_regex, file_path: true requires :package_version, type: String, desc: 'Package version', regexp: Gitlab::Regex.generic_package_version_regex requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.generic_package_file_name_regex, file_path: true end @@ -44,7 +44,7 @@ module API end params do - requires :package_name, type: String, desc: 'Package name' + requires :package_name, type: String, desc: 'Package name', regexp: Gitlab::Regex.generic_package_name_regex, file_path: true requires :package_version, type: String, desc: 'Package version', regexp: Gitlab::Regex.generic_package_version_regex requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.generic_package_file_name_regex, file_path: true requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)' @@ -69,6 +69,29 @@ module API forbidden! end + + desc 'Download package file' do + detail 'This feature was introduced in GitLab 13.5' + end + + params do + requires :package_name, type: String, desc: 'Package name', regexp: Gitlab::Regex.generic_package_name_regex, file_path: true + requires :package_version, type: String, desc: 'Package version', regexp: Gitlab::Regex.generic_package_version_regex + requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.generic_package_file_name_regex, file_path: true + end + + route_setting :authentication, job_token_allowed: true + + get do + authorize_read_package!(project) + + package = ::Packages::Generic::PackageFinder.new(project).execute!(params[:package_name], params[:package_version]) + package_file = ::Packages::PackageFileFinder.new(package, params[:file_name]).execute! + + track_event('pull_package') + + present_carrierwave_file!(package_file.file) + end end end end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 245621d20a8..6511b84e947 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -142,9 +142,13 @@ module Gitlab /\A\d+\.\d+\.\d+\z/ end - def generic_package_file_name_regex + def generic_package_name_regex maven_file_name_regex end + + def generic_package_file_name_regex + generic_package_name_regex + end end extend self diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ee042a2a960..da36d4e9f96 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -22175,6 +22175,9 @@ msgstr "" msgid "Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects." msgstr "" +msgid "SAML" +msgstr "" + msgid "SAML SSO" msgstr "" diff --git a/spec/factories/packages/package_file.rb b/spec/factories/packages/package_file.rb index da35f246ea0..bcca48fb086 100644 --- a/spec/factories/packages/package_file.rb +++ b/spec/factories/packages/package_file.rb @@ -140,6 +140,14 @@ FactoryBot.define do size { 1149.bytes } end + trait(:generic) do + package + file_fixture { 'spec/fixtures/packages/generic/myfile.tar.gz' } + file_name { "#{package.name}.tar.gz" } + file_sha256 { '440e5e148a25331bbd7991575f7d54933c0ebf6cc735a18ee5066ac1381bb590' } + size { 1149.bytes } + end + trait(:object_storage) do file_store { Packages::PackageFileUploader::Store::REMOTE } end diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb index ec30f34199d..304573ecd6e 100644 --- a/spec/features/groups/show_spec.rb +++ b/spec/features/groups/show_spec.rb @@ -184,4 +184,17 @@ RSpec.describe 'Group show page' do expect(page).to have_selector('.notifications-btn.disabled', visible: true) end end + + context 'page og:description' do + let(:group) { create(:group, description: '**Lorem** _ipsum_ dolor sit [amet](https://example.com)') } + let(:maintainer) { create(:user) } + + before do + group.add_maintainer(maintainer) + sign_in(maintainer) + visit path + end + + it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet' + end end diff --git a/spec/features/issues/user_views_issue_spec.rb b/spec/features/issues/user_views_issue_spec.rb index 3f18764aa58..9b1c8be1513 100644 --- a/spec/features/issues/user_views_issue_spec.rb +++ b/spec/features/issues/user_views_issue_spec.rb @@ -5,7 +5,7 @@ require "spec_helper" RSpec.describe "User views issue" do let_it_be(:project) { create(:project_empty_repo, :public) } let_it_be(:user) { create(:user) } - let_it_be(:issue) { create(:issue, project: project, description: "# Description header", author: user) } + let_it_be(:issue) { create(:issue, project: project, description: "# Description header\n\n**Lorem** _ipsum_ dolor sit [amet](https://example.com)", author: user) } let_it_be(:note) { create(:note, noteable: issue, project: project, author: user) } before_all do @@ -20,6 +20,8 @@ RSpec.describe "User views issue" do it { expect(page).to have_header_with_correct_id_and_link(1, "Description header", "description-header") } + it_behaves_like 'page meta description', ' Description header Lorem ipsum dolor sit amet' + it 'shows the merge request and issue actions', :aggregate_failures do expect(page).to have_link('New issue') expect(page).to have_button('Create merge request') diff --git a/spec/features/merge_request/user_sees_page_metadata_spec.rb b/spec/features/merge_request/user_sees_page_metadata_spec.rb new file mode 100644 index 00000000000..7b3e07152a0 --- /dev/null +++ b/spec/features/merge_request/user_sees_page_metadata_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Merge request > User sees page metadata' do + let(:merge_request) { create(:merge_request, description: '**Lorem** _ipsum_ dolor sit [amet](https://example.com)') } + let(:project) { merge_request.target_project } + let(:user) { project.creator } + + before do + project.add_maintainer(user) + sign_in(user) + visit project_merge_request_path(project, merge_request) + end + + it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet' +end diff --git a/spec/features/milestones/user_views_milestone_spec.rb b/spec/features/milestones/user_views_milestone_spec.rb index 11c6fa521d5..9c19f842427 100644 --- a/spec/features/milestones/user_views_milestone_spec.rb +++ b/spec/features/milestones/user_views_milestone_spec.rb @@ -6,7 +6,7 @@ RSpec.describe "User views milestone" do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, :repository, group: group) } - let_it_be(:milestone) { create(:milestone, project: project) } + let_it_be(:milestone) { create(:milestone, project: project, description: '**Lorem** _ipsum_ dolor sit [amet](https://example.com)') } let_it_be(:labels) { create_list(:label, 2, project: project) } before_all do @@ -17,6 +17,14 @@ RSpec.describe "User views milestone" do sign_in(user) end + context 'page description' do + before do + visit(project_milestone_path(project, milestone)) + end + + it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet' + end + it "avoids N+1 database queries" do issue_params = { project: project, assignees: [user], author: user, milestone: milestone, labels: labels }.freeze diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 970500985ae..6c1e1eab968 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -99,6 +99,15 @@ RSpec.describe 'Project' do expect(page).to have_css('.home-panel-description .is-expanded') end end + + context 'page description' do + before do + project.update_attribute(:description, '**Lorem** _ipsum_ dolor sit [amet](https://example.com)') + visit path + end + + it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet' + end end describe 'project topics' do diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb index dd5c2442d00..b3c8cf8d326 100644 --- a/spec/features/users/show_spec.rb +++ b/spec/features/users/show_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe 'User page' do include ExternalAuthorizationServiceHelpers - let(:user) { create(:user) } + let(:user) { create(:user, bio: '**Lorem** _ipsum_ dolor sit [amet](https://example.com)') } context 'with public profile' do it 'shows all the tabs' do @@ -174,4 +174,12 @@ RSpec.describe 'User page' do end end end + + context 'page description' do + before do + visit(user_path(user)) + end + + it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet' + end end diff --git a/spec/features/users/terms_spec.rb b/spec/features/users/terms_spec.rb index 5275845fe5b..7500f2fe59a 100644 --- a/spec/features/users/terms_spec.rb +++ b/spec/features/users/terms_spec.rb @@ -26,6 +26,21 @@ RSpec.describe 'Users > Terms' do expect(page).not_to have_content('Continue') end + context 'when user is a project bot' do + let(:project_bot) { create(:user, :project_bot) } + + before do + enforce_terms + end + + it 'auto accepts the terms' do + visit terms_path + + expect(page).not_to have_content('Accept terms') + expect(project_bot.terms_accepted?).to be(true) + end + end + context 'when signed in' do let(:user) { create(:user) } diff --git a/spec/finders/packages/generic/package_finder_spec.rb b/spec/finders/packages/generic/package_finder_spec.rb new file mode 100644 index 00000000000..ed34268e7a9 --- /dev/null +++ b/spec/finders/packages/generic/package_finder_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Packages::Generic::PackageFinder do + let_it_be(:project) { create(:project) } + let_it_be(:package) { create(:generic_package, project: project) } + + describe '#execute!' do + subject(:finder) { described_class.new(project) } + + it 'finds package by name and version' do + found_package = finder.execute!(package.name, package.version) + + expect(found_package).to eq(package) + end + + it 'ignores packages with same name but different version' do + create(:generic_package, project: project, name: package.name, version: '3.1.4') + + found_package = finder.execute!(package.name, package.version) + + expect(found_package).to eq(package) + end + + it 'raises ActiveRecord::RecordNotFound if package is not found' do + expect { finder.execute!(package.name, '3.1.4') } + .to raise_error(ActiveRecord::RecordNotFound) + end + end +end diff --git a/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js b/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js index 3a9a608b2e2..0e46693a4bf 100644 --- a/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js +++ b/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js @@ -28,8 +28,10 @@ describe('IngressModsecuritySettings', () => { }); }; - const findSaveButton = () => wrapper.find('.btn-success'); - const findCancelButton = () => wrapper.find('[variant="secondary"]'); + const findSaveButton = () => + wrapper.find('[data-qa-selector="save_ingress_modsecurity_settings"]'); + const findCancelButton = () => + wrapper.find('[data-qa-selector="cancel_ingress_modsecurity_settings"]'); const findModSecurityToggle = () => wrapper.find(GlToggle); const findModSecurityDropdown = () => wrapper.find(GlDeprecatedDropdown); diff --git a/spec/frontend/registry/settings/store/actions_spec.js b/spec/frontend/registry/settings/store/actions_spec.js deleted file mode 100644 index 51b89f96ef2..00000000000 --- a/spec/frontend/registry/settings/store/actions_spec.js +++ /dev/null @@ -1,90 +0,0 @@ -import testAction from 'helpers/vuex_action_helper'; -import Api from '~/api'; -import * as actions from '~/registry/settings/store/actions'; -import * as types from '~/registry/settings/store/mutation_types'; - -describe('Actions Registry Store', () => { - describe.each` - actionName | mutationName | payload - ${'setInitialState'} | ${types.SET_INITIAL_STATE} | ${'foo'} - ${'updateSettings'} | ${types.UPDATE_SETTINGS} | ${'foo'} - ${'toggleLoading'} | ${types.TOGGLE_LOADING} | ${undefined} - ${'resetSettings'} | ${types.RESET_SETTINGS} | ${undefined} - `( - '$actionName invokes $mutationName with payload $payload', - ({ actionName, mutationName, payload }) => { - it('should set state', done => { - testAction(actions[actionName], payload, {}, [{ type: mutationName, payload }], [], done); - }); - }, - ); - - describe('receiveSettingsSuccess', () => { - it('calls SET_SETTINGS', () => { - testAction( - actions.receiveSettingsSuccess, - 'foo', - {}, - [{ type: types.SET_SETTINGS, payload: 'foo' }], - [], - ); - }); - }); - - describe('fetchSettings', () => { - const state = { - projectId: 'bar', - }; - - const payload = { - data: { - container_expiration_policy: 'foo', - }, - }; - - it('should fetch the data from the API', done => { - Api.project = jest.fn().mockResolvedValue(payload); - testAction( - actions.fetchSettings, - null, - state, - [], - [ - { type: 'toggleLoading' }, - { type: 'receiveSettingsSuccess', payload: payload.data.container_expiration_policy }, - { type: 'toggleLoading' }, - ], - done, - ); - }); - }); - - describe('saveSettings', () => { - const state = { - projectId: 'bar', - settings: 'baz', - }; - - const payload = { - data: { - tag_expiration_policies: 'foo', - }, - }; - - it('should fetch the data from the API', done => { - Api.updateProject = jest.fn().mockResolvedValue(payload); - testAction( - actions.saveSettings, - null, - state, - [], - [ - { type: 'toggleLoading' }, - { type: 'receiveSettingsSuccess', payload: payload.data.container_expiration_policy }, - { type: 'toggleLoading' }, - ], - done, - ); - }); - }); -}); diff --git a/spec/frontend/registry/settings/store/getters_spec.js b/spec/frontend/registry/settings/store/getters_spec.js deleted file mode 100644 index b781d09466c..00000000000 --- a/spec/frontend/registry/settings/store/getters_spec.js +++ /dev/null @@ -1,72 +0,0 @@ -import * as getters from '~/registry/settings/store/getters'; -import * as utils from '~/registry/shared/utils'; -import { formOptions } from '../../shared/mock_data'; - -describe('Getters registry settings store', () => { - const settings = { - enabled: true, - cadence: 'foo', - keep_n: 'bar', - older_than: 'baz', - name_regex: 'name-foo', - name_regex_keep: 'name-keep-bar', - }; - - describe.each` - getter | variable | formOption - ${'getCadence'} | ${'cadence'} | ${'cadence'} - ${'getKeepN'} | ${'keep_n'} | ${'keepN'} - ${'getOlderThan'} | ${'older_than'} | ${'olderThan'} - `('Options getter', ({ getter, variable, formOption }) => { - beforeEach(() => { - utils.findDefaultOption = jest.fn(); - }); - - it(`${getter} returns ${variable} when ${variable} exists in settings`, () => { - expect(getters[getter]({ settings })).toBe(settings[variable]); - }); - - it(`${getter} calls findDefaultOption when ${variable} does not exists in settings`, () => { - getters[getter]({ settings: {}, formOptions }); - expect(utils.findDefaultOption).toHaveBeenCalledWith(formOptions[formOption]); - }); - }); - - describe('getSettings', () => { - it('returns the content of settings', () => { - const computedGetters = { - getCadence: settings.cadence, - getOlderThan: settings.older_than, - getKeepN: settings.keep_n, - }; - expect(getters.getSettings({ settings }, computedGetters)).toEqual(settings); - }); - }); - - describe('getIsEdited', () => { - it('returns false when original is equal to settings', () => { - const same = { foo: 'bar' }; - expect(getters.getIsEdited({ original: same, settings: same })).toBe(false); - }); - - it('returns true when original is different from settings', () => { - expect(getters.getIsEdited({ original: { foo: 'bar' }, settings: { foo: 'baz' } })).toBe( - true, - ); - }); - }); - - describe('getIsDisabled', () => { - it.each` - original | enableHistoricEntries | result - ${undefined} | ${false} | ${true} - ${{ foo: 'bar' }} | ${undefined} | ${false} - ${{}} | ${false} | ${false} - `( - 'returns $result when original is $original and enableHistoricEntries is $enableHistoricEntries', - ({ original, enableHistoricEntries, result }) => { - expect(getters.getIsDisabled({ original, enableHistoricEntries })).toBe(result); - }, - ); - }); -}); diff --git a/spec/frontend/registry/settings/store/mutations_spec.js b/spec/frontend/registry/settings/store/mutations_spec.js deleted file mode 100644 index 1d85e38eb36..00000000000 --- a/spec/frontend/registry/settings/store/mutations_spec.js +++ /dev/null @@ -1,80 +0,0 @@ -import mutations from '~/registry/settings/store/mutations'; -import * as types from '~/registry/settings/store/mutation_types'; -import createState from '~/registry/settings/store/state'; -import { formOptions, stringifiedFormOptions } from '../../shared/mock_data'; - -describe('Mutations Registry Store', () => { - let mockState; - - beforeEach(() => { - mockState = createState(); - }); - - describe('SET_INITIAL_STATE', () => { - it('should set the initial state', () => { - const payload = { - projectId: 'foo', - enableHistoricEntries: false, - adminSettingsPath: 'foo', - isAdmin: true, - }; - const expectedState = { ...mockState, ...payload, formOptions }; - mutations[types.SET_INITIAL_STATE](mockState, { - ...payload, - ...stringifiedFormOptions, - }); - - expect(mockState).toEqual(expectedState); - }); - }); - - describe('UPDATE_SETTINGS', () => { - it('should update the settings', () => { - mockState.settings = { foo: 'bar' }; - const payload = { foo: 'baz' }; - const expectedState = { ...mockState, settings: payload }; - mutations[types.UPDATE_SETTINGS](mockState, { settings: payload }); - expect(mockState.settings).toEqual(expectedState.settings); - }); - }); - - describe('SET_SETTINGS', () => { - it('should set the settings and original', () => { - const payload = { foo: 'baz' }; - const expectedState = { ...mockState, settings: payload }; - mutations[types.SET_SETTINGS](mockState, payload); - expect(mockState.settings).toEqual(expectedState.settings); - expect(mockState.original).toEqual(expectedState.settings); - }); - - it('should keep the default state when settings is not present', () => { - const originalSettings = { ...mockState.settings }; - mutations[types.SET_SETTINGS](mockState); - expect(mockState.settings).toEqual(originalSettings); - expect(mockState.original).toEqual(undefined); - }); - }); - - describe('RESET_SETTINGS', () => { - it('should copy original over settings', () => { - mockState.settings = { foo: 'bar' }; - mockState.original = { foo: 'baz' }; - mutations[types.RESET_SETTINGS](mockState); - expect(mockState.settings).toEqual(mockState.original); - }); - - it('if original is undefined it should initialize to empty object', () => { - mockState.settings = { foo: 'bar' }; - mockState.original = undefined; - mutations[types.RESET_SETTINGS](mockState); - expect(mockState.settings).toEqual({}); - }); - }); - - describe('TOGGLE_LOADING', () => { - it('should toggle the loading', () => { - mutations[types.TOGGLE_LOADING](mockState); - expect(mockState.isLoading).toEqual(true); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js b/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js index f688e5789cb..79d5129b5ef 100644 --- a/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js +++ b/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js @@ -1,25 +1,26 @@ import { mount, createWrapper } from '@vue/test-utils'; -import { getByText as getByTextHelper } from '@testing-library/dom'; -import { GlAvatarLink } from '@gitlab/ui'; -import { member, orphanedMember } from '../mock_data'; +import { within } from '@testing-library/dom'; +import { GlAvatarLink, GlBadge } from '@gitlab/ui'; +import { member as memberMock, orphanedMember } from '../mock_data'; import UserAvatar from '~/vue_shared/components/members/avatars/user_avatar.vue'; describe('MemberList', () => { let wrapper; - const { user } = member; + const { user } = memberMock; const createComponent = (propsData = {}) => { wrapper = mount(UserAvatar, { propsData: { - member, + member: memberMock, + isCurrentUser: false, ...propsData, }, }); }; const getByText = (text, options) => - createWrapper(getByTextHelper(wrapper.element, text, options)); + createWrapper(within(wrapper.element).findByText(text, options)); afterEach(() => { wrapper.destroy(); @@ -63,4 +64,25 @@ describe('MemberList', () => { expect(getByText('Orphaned member').exists()).toBe(true); }); }); + + describe('badges', () => { + it.each` + member | badgeText + ${{ ...memberMock, usingLicense: true }} | ${'Is using seat'} + ${{ ...memberMock, user: { ...memberMock.user, blocked: true } }} | ${'Blocked'} + ${{ ...memberMock, user: { ...memberMock.user, twoFactorEnabled: true } }} | ${'2FA'} + ${{ ...memberMock, groupSso: true }} | ${'SAML'} + ${{ ...memberMock, groupManagedAccount: true }} | ${'Managed Account'} + `('renders the "$badgeText" badge', ({ member, badgeText }) => { + createComponent({ member }); + + expect(wrapper.find(GlBadge).text()).toBe(badgeText); + }); + + it('renders the "It\'s you" badge when member is current user', () => { + createComponent({ isCurrentUser: true }); + + expect(getByText("It's you").exists()).toBe(true); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/members/table/member_avatar_spec.js b/spec/frontend/vue_shared/components/members/table/member_avatar_spec.js index cf3b28f7bf6..a171dd830c1 100644 --- a/spec/frontend/vue_shared/components/members/table/member_avatar_spec.js +++ b/spec/frontend/vue_shared/components/members/table/member_avatar_spec.js @@ -11,7 +11,10 @@ describe('MemberList', () => { const createComponent = propsData => { wrapper = shallowMount(MemberAvatar, { - propsData, + propsData: { + isCurrentUser: false, + ...propsData, + }, }); }; diff --git a/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js b/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js index 372e07d5e27..960d9bc164c 100644 --- a/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js +++ b/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js @@ -15,6 +15,10 @@ describe('MemberList', () => { type: Boolean, required: true, }, + isCurrentUser: { + type: Boolean, + required: true, + }, }, render(createElement) { return createElement('div', this.memberType); @@ -29,6 +33,7 @@ describe('MemberList', () => { return new Vuex.Store({ state: { sourceId: 1, + currentUserId: 1, ...state, }, }); @@ -42,8 +47,13 @@ describe('MemberList', () => { propsData, store: createStore(state), scopedSlots: { - default: - '<wrapped-component :member-type="props.memberType" :is-direct-member="props.isDirectMember" />', + default: ` + <wrapped-component + :member-type="props.memberType" + :is-direct-member="props.isDirectMember" + :is-current-user="props.isCurrentUser" + /> + `, }, }); }; @@ -93,4 +103,28 @@ describe('MemberList', () => { expect(findWrappedComponent().props('isDirectMember')).toBe(false); }); }); + + describe('isCurrentUser', () => { + it('returns `true` when `member.user` has the same ID as `currentUserId`', () => { + createComponent({ + member: { + ...memberMock, + user: { + ...memberMock.user, + id: 1, + }, + }, + }); + + expect(findWrappedComponent().props('isCurrentUser')).toBe(true); + }); + + it('returns `false` when `member.user` does not have the same ID as `currentUserId`', () => { + createComponent({ + member: memberMock, + }); + + expect(findWrappedComponent().props('isCurrentUser')).toBe(false); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/members/utils_spec.js b/spec/frontend/vue_shared/components/members/utils_spec.js new file mode 100644 index 00000000000..f183abc08d6 --- /dev/null +++ b/spec/frontend/vue_shared/components/members/utils_spec.js @@ -0,0 +1,29 @@ +import { generateBadges } from '~/vue_shared/components/members/utils'; +import { member as memberMock } from './mock_data'; + +describe('Members Utils', () => { + describe('generateBadges', () => { + it('has correct properties for each badge', () => { + const badges = generateBadges(memberMock, true); + + badges.forEach(badge => { + expect(badge).toEqual( + expect.objectContaining({ + show: expect.any(Boolean), + text: expect.any(String), + variant: expect.stringMatching(/muted|neutral|info|success|danger|warning/), + }), + ); + }); + }); + + it.each` + member | expected + ${memberMock} | ${{ show: true, text: "It's you", variant: 'success' }} + ${{ ...memberMock, user: { ...memberMock.user, blocked: true } }} | ${{ show: true, text: 'Blocked', variant: 'danger' }} + ${{ ...memberMock, user: { ...memberMock.user, twoFactorEnabled: true } }} | ${{ show: true, text: '2FA', variant: 'info' }} + `('returns expected output for "$expected.text" badge', ({ member, expected }) => { + expect(generateBadges(member, true)).toContainEqual(expect.objectContaining(expected)); + }); + }); +}); diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index d1fde517488..704a4e7b224 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -599,6 +599,20 @@ RSpec.describe Gitlab::Regex do it { is_expected.not_to match('') } end + describe '.generic_package_name_regex' do + subject { described_class.generic_package_name_regex } + + it { is_expected.to match('123') } + it { is_expected.to match('foo') } + it { is_expected.to match('foo.bar.baz-2.0-20190901.47283-1') } + it { is_expected.not_to match('../../foo') } + it { is_expected.not_to match('..\..\foo') } + it { is_expected.not_to match('%2f%2e%2e%2f%2essh%2fauthorized_keys') } + it { is_expected.not_to match('$foo/bar') } + it { is_expected.not_to match('my file name') } + it { is_expected.not_to match('!!()()') } + end + describe '.generic_package_file_name_regex' do subject { described_class.generic_package_file_name_regex } diff --git a/spec/models/application_setting/term_spec.rb b/spec/models/application_setting/term_spec.rb index 82347453437..51a6027698f 100644 --- a/spec/models/application_setting/term_spec.rb +++ b/spec/models/application_setting/term_spec.rb @@ -17,6 +17,7 @@ RSpec.describe ApplicationSetting::Term do describe '#accepted_by_user?' do let(:user) { create(:user) } + let(:project_bot) { create(:user, :project_bot) } let(:term) { create(:term) } it 'is true when the user accepted the terms' do @@ -25,6 +26,10 @@ RSpec.describe ApplicationSetting::Term do expect(term.accepted_by_user?(user)).to be(true) end + it 'is true when user is a bot' do + expect(term.accepted_by_user?(project_bot)).to be(true) + end + it 'is false when the user declined the terms' do decline_terms(term, user) diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index a66e96d8a19..26851c93ac3 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -44,7 +44,7 @@ RSpec.describe Ci::JobArtifact do let!(:metrics_report) { create(:ci_job_artifact, :junit) } let!(:codequality_report) { create(:ci_job_artifact, :codequality) } - it { is_expected.to eq([metrics_report, codequality_report]) } + it { is_expected.to match_array([metrics_report, codequality_report]) } end end diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb index ea1f75d04e7..6a3969802f3 100644 --- a/spec/models/packages/package_spec.rb +++ b/spec/models/packages/package_spec.rb @@ -108,6 +108,20 @@ RSpec.describe Packages::Package, type: :model do it { is_expected.not_to allow_value('.foobar').for(:name) } it { is_expected.not_to allow_value('%foo%bar').for(:name) } end + + context 'generic package' do + subject { build_stubbed(:generic_package) } + + it { is_expected.to allow_value('123').for(:name) } + it { is_expected.to allow_value('foo').for(:name) } + it { is_expected.to allow_value('foo.bar.baz-2.0-20190901.47283-1').for(:name) } + it { is_expected.not_to allow_value('../../foo').for(:name) } + it { is_expected.not_to allow_value('..\..\foo').for(:name) } + it { is_expected.not_to allow_value('%2f%2e%2e%2f%2essh%2fauthorized_keys').for(:name) } + it { is_expected.not_to allow_value('$foo/bar').for(:name) } + it { is_expected.not_to allow_value('my file name').for(:name) } + it { is_expected.not_to allow_value('!!().for(:name)().for(:name)').for(:name) } + end end describe '#version' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 1841288cd4b..0f71c7790d4 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -4330,28 +4330,32 @@ RSpec.describe User do describe '#required_terms_not_accepted?' do let(:user) { build(:user) } + let(:project_bot) { create(:user, :project_bot) } subject { user.required_terms_not_accepted? } context "when terms are not enforced" do - it { is_expected.to be_falsy } + it { is_expected.to be_falsey } end - context "when terms are enforced and accepted by the user" do + context "when terms are enforced" do before do enforce_terms - accept_terms(user) end - it { is_expected.to be_falsy } - end + it "is not accepted by the user" do + expect(subject).to be_truthy + end - context "when terms are enforced but the user has not accepted" do - before do - enforce_terms + it "is accepted by the user" do + accept_terms(user) + + expect(subject).to be_falsey end - it { is_expected.to be_truthy } + it "auto accepts the term for project bots" do + expect(project_bot.required_terms_not_accepted?).to be_falsey + end end end diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb index bebeed9402e..2cb686167f1 100644 --- a/spec/requests/api/generic_packages_spec.rb +++ b/spec/requests/api/generic_packages_spec.rb @@ -33,7 +33,19 @@ RSpec.describe API::GenericPackages do { Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => value || ci_build.token } end - describe 'PUT /api/v4/projects/:id/packages/generic/mypackage/0.0.1/myfile.tar.gz/authorize' do + shared_examples 'secure endpoint' do + before do + project.add_developer(user) + end + + it 'rejects malicious request' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + describe 'PUT /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name/authorize' do context 'with valid project' do using RSpec::Parameterized::TableSyntax @@ -73,41 +85,49 @@ RSpec.describe API::GenericPackages do end it "responds with #{params[:expected_status]}" do - headers = workhorse_header.merge(auth_header) - url = "/projects/#{project.id}/packages/generic/mypackage/0.0.1/myfile.tar.gz/authorize" - - put api(url), headers: headers + authorize_upload_file(workhorse_header.merge(auth_header)) expect(response).to have_gitlab_http_status(expected_status) end end end - it 'rejects a malicious request' do - project.add_developer(user) - headers = workhorse_header.merge(personal_access_token_header) - url = "/projects/#{project.id}/packages/generic/mypackage/0.0.1/%2e%2e%2f.ssh%2fauthorized_keys/authorize" + context 'application security' do + using RSpec::Parameterized::TableSyntax - put api(url), headers: headers + where(:param_name, :param_value) do + :package_name | 'my-package/../' + :package_name | 'my-package%2f%2e%2e%2f' + :file_name | '../.ssh%2fauthorized_keys' + :file_name | '%2e%2e%2f.ssh%2fauthorized_keys' + end - expect(response).to have_gitlab_http_status(:bad_request) + with_them do + subject { authorize_upload_file(workhorse_header.merge(personal_access_token_header), param_name => param_value) } + + it_behaves_like 'secure endpoint' + end end context 'generic_packages feature flag is disabled' do it 'responds with 404 Not Found' do stub_feature_flags(generic_packages: false) project.add_developer(user) - headers = workhorse_header.merge(personal_access_token_header) - url = "/projects/#{project.id}/packages/generic/mypackage/0.0.1/myfile.tar.gz/authorize" - put api(url), headers: headers + authorize_upload_file(workhorse_header.merge(personal_access_token_header)) expect(response).to have_gitlab_http_status(:not_found) end end + + def authorize_upload_file(request_headers, package_name: 'mypackage', file_name: 'myfile.tar.gz') + url = "/projects/#{project.id}/packages/generic/#{package_name}/0.0.1/#{file_name}/authorize" + + put api(url), headers: request_headers + end end - describe 'PUT /api/v4/projects/:id/packages/generic/mypackage/0.0.1/myfile.tar.gz' do + describe 'PUT /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name' do include WorkhorseHelpers let(:file_upload) { fixture_file_upload('spec/fixtures/packages/generic/myfile.tar.gz') } @@ -246,17 +266,27 @@ RSpec.describe API::GenericPackages do expect(response).to have_gitlab_http_status(:forbidden) end + end - it 'rejects a malicious request' do - headers = workhorse_header.merge(personal_access_token_header) - upload_file(params, headers, file_name: '%2e%2e%2f.ssh%2fauthorized_keys') + context 'application security' do + using RSpec::Parameterized::TableSyntax - expect(response).to have_gitlab_http_status(:bad_request) + where(:param_name, :param_value) do + :package_name | 'my-package/../' + :package_name | 'my-package%2f%2e%2e%2f' + :file_name | '../.ssh%2fauthorized_keys' + :file_name | '%2e%2e%2f.ssh%2fauthorized_keys' + end + + with_them do + subject { upload_file(params, workhorse_header.merge(personal_access_token_header), param_name => param_value) } + + it_behaves_like 'secure endpoint' end end - def upload_file(params, request_headers, send_rewritten_field: true, file_name: 'myfile.tar.gz') - url = "/projects/#{project.id}/packages/generic/mypackage/0.0.1/#{file_name}" + def upload_file(params, request_headers, send_rewritten_field: true, package_name: 'mypackage', file_name: 'myfile.tar.gz') + url = "/projects/#{project.id}/packages/generic/#{package_name}/0.0.1/#{file_name}" workhorse_finalize( api(url), @@ -268,4 +298,138 @@ RSpec.describe API::GenericPackages do ) end end + + describe 'GET /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name' do + using RSpec::Parameterized::TableSyntax + + let_it_be(:package) { create(:generic_package, project: project) } + let_it_be(:package_file) { create(:package_file, :generic, package: package) } + + context 'authentication' do + where(:project_visibility, :user_role, :member?, :authenticate_with, :expected_status) do + 'PUBLIC' | :developer | true | :personal_access_token | :success + 'PUBLIC' | :guest | true | :personal_access_token | :success + 'PUBLIC' | :developer | true | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :guest | true | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :developer | false | :personal_access_token | :success + 'PUBLIC' | :guest | false | :personal_access_token | :success + 'PUBLIC' | :developer | false | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :guest | false | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :anonymous | false | :none | :unauthorized + 'PRIVATE' | :developer | true | :personal_access_token | :success + 'PRIVATE' | :guest | true | :personal_access_token | :forbidden + 'PRIVATE' | :developer | true | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :guest | true | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :developer | false | :personal_access_token | :not_found + 'PRIVATE' | :guest | false | :personal_access_token | :not_found + 'PRIVATE' | :developer | false | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :guest | false | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :anonymous | false | :none | :unauthorized + 'PUBLIC' | :developer | true | :job_token | :success + 'PUBLIC' | :developer | true | :invalid_job_token | :unauthorized + 'PUBLIC' | :developer | false | :job_token | :success + 'PUBLIC' | :developer | false | :invalid_job_token | :unauthorized + 'PRIVATE' | :developer | true | :job_token | :success + 'PRIVATE' | :developer | true | :invalid_job_token | :unauthorized + 'PRIVATE' | :developer | false | :job_token | :not_found + 'PRIVATE' | :developer | false | :invalid_job_token | :unauthorized + end + + with_them do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility, false)) + project.send("add_#{user_role}", user) if member? && user_role != :anonymous + end + + it "responds with #{params[:expected_status]}" do + download_file(auth_header) + + expect(response).to have_gitlab_http_status(expected_status) + end + end + end + + context 'event tracking' do + before do + project.add_developer(user) + end + + subject { download_file(personal_access_token_header) } + + it_behaves_like 'a gitlab tracking event', described_class.name, 'pull_package' + end + + it 'rejects a malicious file name request' do + project.add_developer(user) + + download_file(personal_access_token_header, file_name: '../.ssh%2fauthorized_keys') + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'rejects a malicious file name request' do + project.add_developer(user) + + download_file(personal_access_token_header, file_name: '%2e%2e%2f.ssh%2fauthorized_keys') + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'rejects a malicious package name request' do + project.add_developer(user) + + download_file(personal_access_token_header, package_name: 'my-package/../') + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'rejects a malicious package name request' do + project.add_developer(user) + + download_file(personal_access_token_header, package_name: 'my-package%2f%2e%2e%2f') + + expect(response).to have_gitlab_http_status(:bad_request) + end + + context 'application security' do + using RSpec::Parameterized::TableSyntax + + where(:param_name, :param_value) do + :package_name | 'my-package/../' + :package_name | 'my-package%2f%2e%2e%2f' + :file_name | '../.ssh%2fauthorized_keys' + :file_name | '%2e%2e%2f.ssh%2fauthorized_keys' + end + + with_them do + subject { download_file(personal_access_token_header, param_name => param_value) } + + it_behaves_like 'secure endpoint' + end + end + + it 'responds with 404 Not Found for non existing package' do + project.add_developer(user) + + download_file(personal_access_token_header, package_name: 'no-such-package') + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'responds with 404 Not Found for non existing package file' do + project.add_developer(user) + + download_file(personal_access_token_header, file_name: 'no-such-file') + + expect(response).to have_gitlab_http_status(:not_found) + end + + def download_file(request_headers, package_name: nil, file_name: nil) + package_name ||= package.name + file_name ||= package_file.file_name + url = "/projects/#{project.id}/packages/generic/#{package_name}/#{package.version}/#{file_name}" + + get api(url), headers: request_headers + end + end end diff --git a/spec/support/shared_examples/features/page_description_shared_examples.rb b/spec/support/shared_examples/features/page_description_shared_examples.rb new file mode 100644 index 00000000000..81653220b4c --- /dev/null +++ b/spec/support/shared_examples/features/page_description_shared_examples.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'page meta description' do |expected_description| + it 'renders the page with description, og:description, and twitter:description meta tags that contains a plain-text version of the markdown', :aggregate_failures do + %w(name='description' property='og:description' property='twitter:description').each do |selector| + expect(page).to have_selector("meta[#{selector}][content='#{expected_description}']", visible: false) + end + end +end |