Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4ed4dc08a8
commit
a7e81add72
62 changed files with 819 additions and 491 deletions
|
@ -1 +1 @@
|
|||
87acab96b9eb16381a49f2c08a2eaa9664a2fa75
|
||||
3f5e218def93024f3aafe590c22cd1b29f744105
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'));
|
||||
};
|
|
@ -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);
|
||||
};
|
|
@ -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();
|
|
@ -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';
|
|
@ -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;
|
||||
},
|
||||
};
|
|
@ -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: {},
|
||||
});
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
];
|
22
app/finders/packages/generic/package_finder.rb
Normal file
22
app/finders/packages/generic/package_finder.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1674,6 +1674,8 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
def terms_accepted?
|
||||
return true if project_bot?
|
||||
|
||||
accepted_term_id.present?
|
||||
end
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- page_title @group.name
|
||||
- page_description @group.description unless page_description
|
||||
- page_description @group.description_html unless page_description
|
||||
- header_title group_title(@group) unless header_title
|
||||
- nav "group"
|
||||
- display_subscription_banner!
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- page_title @project.full_name
|
||||
- page_description @project.description unless page_description
|
||||
- page_description @project.description_html unless page_description
|
||||
- header_title project_title(@project) unless header_title
|
||||
- nav "project"
|
||||
- display_subscription_banner!
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 "
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Strip markdown from og:description meta tags
|
||||
merge_request: 42918
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Migrate deprecated button to GlButton in ingress_modsecurity_settings.vue
|
||||
merge_request: 43717
|
||||
author:
|
||||
type: other
|
5
changelogs/unreleased/pat-bot-terms.yml
Normal file
5
changelogs/unreleased/pat-bot-terms.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Auto-accept TOS if project bot
|
||||
merge_request: 43067
|
||||
author:
|
||||
type: fixed
|
|
@ -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
|
||||
}
|
||||
|
||||
"""
|
||||
|
@ -20470,6 +20471,41 @@ type VulnerabilityResolvePayload {
|
|||
vulnerability: Vulnerability
|
||||
}
|
||||
|
||||
"""
|
||||
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
|
||||
"""
|
||||
|
|
|
@ -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,
|
||||
|
@ -59575,6 +59602,108 @@
|
|||
"enumValues": null,
|
||||
"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",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
17
spec/features/merge_request/user_sees_page_metadata_spec.rb
Normal file
17
spec/features/merge_request/user_sees_page_metadata_spec.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
31
spec/finders/packages/generic/package_finder_spec.rb
Normal file
31
spec/finders/packages/generic/package_finder_spec.rb
Normal file
|
@ -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
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,7 +11,10 @@ describe('MemberList', () => {
|
|||
|
||||
const createComponent = propsData => {
|
||||
wrapper = shallowMount(MemberAvatar, {
|
||||
propsData,
|
||||
propsData: {
|
||||
isCurrentUser: false,
|
||||
...propsData,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
29
spec/frontend/vue_shared/components/members/utils_spec.js
Normal file
29
spec/frontend/vue_shared/components/members/utils_spec.js
Normal file
|
@ -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));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
end
|
||||
|
||||
it "is not accepted by the user" do
|
||||
expect(subject).to be_truthy
|
||||
end
|
||||
|
||||
it "is accepted by the user" do
|
||||
accept_terms(user)
|
||||
|
||||
expect(subject).to be_falsey
|
||||
end
|
||||
|
||||
it { is_expected.to be_falsy }
|
||||
it "auto accepts the term for project bots" do
|
||||
expect(project_bot.required_terms_not_accepted?).to be_falsey
|
||||
end
|
||||
|
||||
context "when terms are enforced but the user has not accepted" do
|
||||
before do
|
||||
enforce_terms
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in a new issue