diff options
81 files changed, 820 insertions, 176 deletions
@@ -476,7 +476,7 @@ gem 'ssh_data', '~> 1.2' gem 'spamcheck', '~> 0.1.0' # Gitaly GRPC protocol definitions -gem 'gitaly', '~> 14.8.0.pre.rc1' +gem 'gitaly', '~> 14.9.0.pre.rc2' # KAS GRPC protocol definitions gem 'kas-grpc', '~> 0.0.2' diff --git a/Gemfile.lock b/Gemfile.lock index a66a2fff1bd..eb64191b6e6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -455,7 +455,7 @@ GEM rails (>= 3.2.0) git (1.7.0) rchardet (~> 1.8) - gitaly (14.8.0.pre.rc1) + gitaly (14.9.0.pre.rc2) grpc (~> 1.0) github-markup (1.7.0) gitlab (4.16.1) @@ -1475,7 +1475,7 @@ DEPENDENCIES gettext (~> 3.3) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly (~> 14.8.0.pre.rc1) + gitaly (~> 14.9.0.pre.rc2) github-markup (~> 1.7.0) gitlab-chronic (~> 0.10.5) gitlab-dangerfiles (~> 2.10.2) diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js index 7e4d3ebb686..96cc774a280 100644 --- a/app/assets/javascripts/boards/boards_util.js +++ b/app/assets/javascripts/boards/boards_util.js @@ -1,5 +1,6 @@ import { sortBy, cloneDeep } from 'lodash'; -import { isGid } from '~/graphql_shared/utils'; +import { TYPE_BOARD, TYPE_ITERATION, TYPE_MILESTONE, TYPE_USER } from '~/graphql_shared/constants'; +import { isGid, convertToGraphQLId } from '~/graphql_shared/utils'; import { ListType, MilestoneIDs, AssigneeFilterType, MilestoneFilterType } from './constants'; export function getMilestone() { @@ -80,19 +81,22 @@ export function formatListsPageInfo(lists) { } export function fullBoardId(boardId) { - return `gid://gitlab/Board/${boardId}`; + if (!boardId) { + return null; + } + return convertToGraphQLId(TYPE_BOARD, boardId); } export function fullIterationId(id) { - return `gid://gitlab/Iteration/${id}`; + return convertToGraphQLId(TYPE_ITERATION, id); } export function fullUserId(id) { - return `gid://gitlab/User/${id}`; + return convertToGraphQLId(TYPE_USER, id); } export function fullMilestoneId(id) { - return `gid://gitlab/Milestone/${id}`; + return convertToGraphQLId(TYPE_MILESTONE, id); } export function fullLabelId(label) { diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue index cc048e2af1a..f39f9751c83 100644 --- a/app/assets/javascripts/boards/components/board_form.vue +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -1,11 +1,8 @@ <script> import { GlModal, GlAlert } from '@gitlab/ui'; import { mapGetters, mapActions, mapState } from 'vuex'; -import { TYPE_USER, TYPE_ITERATION, TYPE_MILESTONE } from '~/graphql_shared/constants'; -import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getParameterByName, visitUrl } from '~/lib/utils/url_utility'; import { __, s__ } from '~/locale'; -import { fullLabelId } from '../boards_util'; import { formType } from '../constants'; import createBoardMutation from '../graphql/board_create.mutation.graphql'; @@ -158,33 +155,8 @@ export default { groupPath: this.isGroupBoard ? this.fullPath : undefined, }; }, - issueBoardScopeMutationVariables() { - return { - weight: this.board.weight, - assigneeId: this.board.assignee?.id - ? convertToGraphQLId(TYPE_USER, this.board.assignee.id) - : null, - // Temporarily converting to milestone ID due to https://gitlab.com/gitlab-org/gitlab/-/issues/344779 - milestoneId: this.board.milestone?.id - ? convertToGraphQLId(TYPE_MILESTONE, getIdFromGraphQLId(this.board.milestone.id)) - : null, - // Temporarily converting to iteration ID due to https://gitlab.com/gitlab-org/gitlab/-/issues/344779 - iterationId: this.board.iteration?.id - ? convertToGraphQLId(TYPE_ITERATION, getIdFromGraphQLId(this.board.iteration.id)) - : null, - }; - }, - boardScopeMutationVariables() { - return { - labelIds: this.board.labels.map(fullLabelId), - ...(this.isIssueBoard && this.issueBoardScopeMutationVariables), - }; - }, mutationVariables() { - return { - ...this.baseMutationVariables, - ...(this.scopedIssueBoardFeatureEnabled ? this.boardScopeMutationVariables : {}), - }; + return this.baseMutationVariables; }, }, mounted() { diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue index 5d37cd4930f..91fdfd668fc 100644 --- a/app/assets/javascripts/boards/components/boards_selector.vue +++ b/app/assets/javascripts/boards/components/boards_selector.vue @@ -101,6 +101,7 @@ export default { }, update(data) { const board = data.workspace?.board; + this.setBoardConfig(board); return { ...board, labels: board?.labels?.nodes, @@ -170,7 +171,7 @@ export default { eventHub.$off('showBoardModal', this.showPage); }, methods: { - ...mapActions(['setError']), + ...mapActions(['setError', 'setBoardConfig']), showPage(page) { this.currentPage = page; }, diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index d2a64246aa7..b31b56e6839 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -28,6 +28,12 @@ const apolloProvider = new VueApollo({ function mountBoardApp(el) { const { boardId, groupId, fullPath, rootPath } = el.dataset; + store.dispatch('fetchBoard', { + fullPath, + fullBoardId: fullBoardId(boardId), + boardType: el.dataset.parent, + }); + store.dispatch('setInitialBoardData', { boardId, fullBoardId: fullBoardId(boardId), @@ -35,17 +41,6 @@ function mountBoardApp(el) { boardType: el.dataset.parent, disabled: parseBoolean(el.dataset.disabled) || true, issuableType: issuableTypes.issue, - boardConfig: { - milestoneId: parseInt(el.dataset.boardMilestoneId, 10), - milestoneTitle: el.dataset.boardMilestoneTitle || '', - iterationId: parseInt(el.dataset.boardIterationId, 10), - iterationTitle: el.dataset.boardIterationTitle || '', - assigneeId: el.dataset.boardAssigneeId, - assigneeUsername: el.dataset.boardAssigneeUsername, - labels: el.dataset.labels ? JSON.parse(el.dataset.labels) : [], - labelIds: el.dataset.labelIds ? JSON.parse(el.dataset.labelIds) : [], - weight: el.dataset.boardWeight ? parseInt(el.dataset.boardWeight, 10) : null, - }, }); // eslint-disable-next-line no-new diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index 1ebfcfc331b..82307da2572 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -36,6 +36,8 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { queryToObject } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; import { gqlClient } from '../graphql'; +import projectBoardQuery from '../graphql/project_board.query.graphql'; +import groupBoardQuery from '../graphql/group_board.query.graphql'; import boardLabelsQuery from '../graphql/board_labels.query.graphql'; import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql'; import groupProjectsQuery from '../graphql/group_projects.query.graphql'; @@ -46,10 +48,44 @@ import projectBoardMilestonesQuery from '../graphql/project_board_milestones.que import * as types from './mutation_types'; export default { + fetchBoard: ({ commit, dispatch }, { fullPath, fullBoardId, boardType }) => { + const variables = { + fullPath, + boardId: fullBoardId, + }; + + return gqlClient + .query({ + query: boardType === BoardType.group ? groupBoardQuery : projectBoardQuery, + variables, + }) + .then(({ data }) => { + const board = data.workspace?.board; + commit(types.RECEIVE_BOARD_SUCCESS, board); + dispatch('setBoardConfig', board); + }) + .catch(() => commit(types.RECEIVE_BOARD_FAILURE)); + }, + setInitialBoardData: ({ commit }, data) => { commit(types.SET_INITIAL_BOARD_DATA, data); }, + setBoardConfig: ({ commit }, board) => { + const config = { + milestoneId: board.milestone?.id || null, + milestoneTitle: board.milestone?.title || null, + iterationId: board.iteration?.id || null, + iterationTitle: board.iteration?.title || null, + assigneeId: board.assignee?.id || null, + assigneeUsername: board.assignee?.username || null, + labels: board.labels?.nodes || [], + labelIds: board.labels?.nodes?.map((label) => label.id) || [], + weight: board.weight, + }; + commit(types.SET_BOARD_CONFIG, config); + }, + setActiveId({ commit }, { id, sidebarType }) { commit(types.SET_ACTIVE_ID, { id, sidebarType }); }, diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js index 31b78014525..668a3b5e0f9 100644 --- a/app/assets/javascripts/boards/stores/mutation_types.js +++ b/app/assets/javascripts/boards/stores/mutation_types.js @@ -1,4 +1,7 @@ +export const RECEIVE_BOARD_SUCCESS = 'RECEIVE_BOARD_SUCCESS'; +export const RECEIVE_BOARD_FAILURE = 'RECEIVE_BOARD_FAILURE'; export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA'; +export const SET_BOARD_CONFIG = 'SET_BOARD_CONFIG'; export const SET_FILTERS = 'SET_FILTERS'; export const CREATE_LIST_SUCCESS = 'CREATE_LIST_SUCCESS'; export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE'; diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js index 2a2ce7652e6..9a50dcf05b8 100644 --- a/app/assets/javascripts/boards/stores/mutations.js +++ b/app/assets/javascripts/boards/stores/mutations.js @@ -33,10 +33,20 @@ export const addItemToList = ({ state, listId, itemId, moveBeforeId, moveAfterId }; export default { + [mutationTypes.RECEIVE_BOARD_SUCCESS]: (state, board) => { + state.board = { + ...board, + labels: board?.labels?.nodes || [], + }; + }, + + [mutationTypes.RECEIVE_BOARD_FAILURE]: (state) => { + state.error = s__('Boards|An error occurred while fetching the board. Please reload the page.'); + }, + [mutationTypes.SET_INITIAL_BOARD_DATA](state, data) { const { allowSubEpics, - boardConfig, boardId, boardType, disabled, @@ -45,7 +55,6 @@ export default { issuableType, } = data; state.allowSubEpics = allowSubEpics; - state.boardConfig = boardConfig; state.boardId = boardId; state.boardType = boardType; state.disabled = disabled; @@ -54,6 +63,10 @@ export default { state.issuableType = issuableType; }, + [mutationTypes.SET_BOARD_CONFIG](state, boardConfig) { + state.boardConfig = boardConfig; + }, + [mutationTypes.RECEIVE_BOARD_LISTS_SUCCESS]: (state, lists) => { state.boardLists = lists; }, diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js index 80c51c966d2..7af4e5a8798 100644 --- a/app/assets/javascripts/boards/stores/state.js +++ b/app/assets/javascripts/boards/stores/state.js @@ -1,6 +1,7 @@ import { inactiveId, ListType } from '~/boards/constants'; export default () => ({ + board: {}, boardType: null, issuableType: null, fullPath: null, diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 7282a92a847..8cb2e9e249b 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -3,6 +3,7 @@ import '~/lib/utils/jquery_at_who'; import { escape as lodashEscape, sortBy, template, escapeRegExp } from 'lodash'; import * as Emoji from '~/emoji'; import axios from '~/lib/utils/axios_utils'; +import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js'; import { s__, __, sprintf } from '~/locale'; import { isUserBusy } from '~/set_status_modal/utils'; import SidebarMediator from '~/sidebar/sidebar_mediator'; @@ -957,9 +958,14 @@ GfmAutoComplete.Contacts = { return `<li><small>${firstName} ${lastName}</small> ${escape(email)}</li>`; }, }; + +const loadingSpinner = loadingIconForLegacyJS({ + inline: true, + classes: ['gl-mr-2'], +}).outerHTML; + GfmAutoComplete.Loading = { - template: - '<li style="pointer-events: none;"><span class="spinner align-text-bottom mr-1"></span>Loading...</li>', + template: `<li style="pointer-events: none;">${loadingSpinner}Loading...</li>`, }; export default GfmAutoComplete; diff --git a/app/assets/javascripts/graphql_shared/constants.js b/app/assets/javascripts/graphql_shared/constants.js index 3b36c3e6ac5..4ebb49b4756 100644 --- a/app/assets/javascripts/graphql_shared/constants.js +++ b/app/assets/javascripts/graphql_shared/constants.js @@ -1,9 +1,11 @@ export const MINIMUM_SEARCH_LENGTH = 3; +export const TYPE_BOARD = 'Board'; export const TYPE_CI_RUNNER = 'Ci::Runner'; export const TYPE_CRM_CONTACT = 'CustomerRelations::Contact'; export const TYPE_DISCUSSION = 'Discussion'; export const TYPE_EPIC = 'Epic'; +export const TYPE_EPIC_BOARD = 'Boards::EpicBoard'; export const TYPE_GROUP = 'Group'; export const TYPE_ISSUE = 'Issue'; export const TYPE_ITERATION = 'Iteration'; diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index a4accf0ee8f..ac2eb34260c 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -9,7 +9,7 @@ const LINK_TAG_PATTERN = '[{text}](url)'; // a bullet point character (*+-) and an optional checkbox ([ ] [x]) // OR a number with a . after it and an optional checkbox ([ ] [x]) // followed by one or more whitespace characters -const LIST_LINE_HEAD_PATTERN = /^(?<indent>\s*)(?<leader>((?<isOl>[*+-])|(?<isUl>\d+\.))( \[([x ])\])?\s)(?<content>.)?/; +const LIST_LINE_HEAD_PATTERN = /^(?<indent>\s*)(?<leader>((?<isUl>[*+-])|(?<isOl>\d+\.))( \[([x ])\])?\s)(?<content>.)?/; function selectedText(text, textarea) { return text.substring(textarea.selectionStart, textarea.selectionEnd); @@ -31,8 +31,19 @@ function lineBefore(text, textarea, trimNewlines = true) { return split[split.length - 1]; } -function lineAfter(text, textarea) { - return text.substring(textarea.selectionEnd).trim().split('\n')[0]; +function lineAfter(text, textarea, trimNewlines = true) { + let split = text.substring(textarea.selectionEnd); + + if (trimNewlines) { + split = split.trim(); + } else { + // remove possible leading newline to get at the real line + split = split.replace(/^\n/, ''); + } + + split = split.split('\n'); + + return split[0]; } function convertMonacoSelectionToAceFormat(sel) { @@ -329,6 +340,25 @@ function handleSurroundSelectedText(e, textArea) { } /* eslint-enable @gitlab/require-i18n-strings */ +/** + * Returns the content for a new line following a list item. + * + * @param {Object} result - regex match of the current line + * @param {Object?} nextLineResult - regex match of the next line + * @returns string with the new list item + */ +function continueOlText(result, nextLineResult) { + const { indent, leader } = result.groups; + const { indent: nextIndent, isOl: nextIsOl } = nextLineResult?.groups ?? {}; + + const [numStr, postfix = ''] = leader.split('.'); + + const incrementBy = nextIsOl && nextIndent === indent ? 0 : 1; + const num = parseInt(numStr, 10) + incrementBy; + + return `${indent}${num}.${postfix}`; +} + function handleContinueList(e, textArea) { if (!gon.features?.markdownContinueLists) return; if (!(e.key === 'Enter')) return; @@ -339,7 +369,7 @@ function handleContinueList(e, textArea) { const result = currentLine.match(LIST_LINE_HEAD_PATTERN); if (result) { - const { indent, content, leader } = result.groups; + const { leader, indent, content, isOl } = result.groups; const prevLineEmpty = !content; if (prevLineEmpty) { @@ -349,12 +379,22 @@ function handleContinueList(e, textArea) { return; } - const itemInsert = `${indent}${leader}`; + let itemToInsert; + + if (isOl) { + const nextLine = lineAfter(textArea.value, textArea, false); + const nextLineResult = nextLine.match(LIST_LINE_HEAD_PATTERN); + + itemToInsert = continueOlText(result, nextLineResult); + } else { + // isUl + itemToInsert = `${indent}${leader}`; + } e.preventDefault(); updateText({ - tag: itemInsert, + tag: itemToInsert, textArea, blockTag: '', wrap: false, diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index d2841156e55..b7159fd6835 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -1,6 +1,7 @@ /* eslint-disable consistent-return */ import $ from 'jquery'; +import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js'; import { spriteIcon } from '~/lib/utils/common_utils'; import FilesCommentButton from './files_comment_button'; import createFlash from './flash'; @@ -10,7 +11,7 @@ import { __ } from './locale'; import syntaxHighlight from './syntax_highlight'; const WRAPPER = '<div class="diff-content"></div>'; -const LOADING_HTML = '<span class="spinner"></span>'; +const LOADING_HTML = loadingIconForLegacyJS().outerHTML; const ERROR_HTML = `<div class="nothing-here-block">${spriteIcon( 'warning-solid', 's16', diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue index 5aae1812de3..4a78cbacec0 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue @@ -35,16 +35,20 @@ export default { }, highlightedContent() { let highlightedContent; + let { language } = this; if (this.hljs) { - if (!this.language) { - highlightedContent = this.hljs.highlightAuto(this.content).value; + if (!language) { + const hljsHighlightAuto = this.hljs.highlightAuto(this.content); + + highlightedContent = hljsHighlightAuto.value; + language = hljsHighlightAuto.language; } else if (this.languageDefinition) { highlightedContent = this.hljs.highlight(this.content, { language: this.language }).value; } } - return wrapLines(highlightedContent); + return wrapLines(highlightedContent, language); }, }, watch: { @@ -110,7 +114,7 @@ export default { data-qa-selector="blob_viewer_file_content" > <line-numbers :lines="lineNumbers" /> - <pre class="code gl-pb-0!"><code v-safe-html="highlightedContent"></code> + <pre class="code highlight gl-pb-0!"><code v-safe-html="highlightedContent"></code> </pre> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/utils.js b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js index e64e564bf61..d726a8a55ff 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/utils.js +++ b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js @@ -1,11 +1,13 @@ -export const wrapLines = (content) => { +export const wrapLines = (content, language) => { + const isValidLanguage = /^[a-z\d\-_]+$/.test(language); // To prevent the possibility of a vulnerability we only allow languages that contain alphanumeric characters ([a-z\d), dashes (-) or underscores (_). + return ( content && content .split('\n') .map((line, i) => { let formattedLine; - const idAttribute = `id="LC${i + 1}"`; + const attributes = `id="LC${i + 1}" lang="${isValidLanguage ? language : ''}"`; if (line.includes('<span class="hljs') && !line.includes('</span>')) { /** @@ -14,9 +16,9 @@ export const wrapLines = (content) => { * example (before): <span class="hljs-code">```bash * example (after): <span id="LC67" class="hljs-code">```bash */ - formattedLine = line.replace(/(?=class="hljs)/, `${idAttribute} `); + formattedLine = line.replace(/(?=class="hljs)/, `${attributes} `); } else { - formattedLine = `<span ${idAttribute} class="line">${line}</span>`; + formattedLine = `<span ${attributes} class="line">${line}</span>`; } return formattedLine; diff --git a/app/models/application_record.rb b/app/models/application_record.rb index c9c46250a6d..75e1bab294f 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -5,6 +5,7 @@ class ApplicationRecord < ActiveRecord::Base include Transactions include LegacyBulkInsert include CrossDatabaseModification + include SensitiveSerializableHash self.abstract_class = true diff --git a/app/models/concerns/sensitive_serializable_hash.rb b/app/models/concerns/sensitive_serializable_hash.rb new file mode 100644 index 00000000000..725ec60e9b6 --- /dev/null +++ b/app/models/concerns/sensitive_serializable_hash.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module SensitiveSerializableHash + extend ActiveSupport::Concern + + included do + class_attribute :attributes_exempt_from_serializable_hash, default: [] + end + + class_methods do + def prevent_from_serialization(*keys) + self.attributes_exempt_from_serializable_hash ||= [] + self.attributes_exempt_from_serializable_hash.concat keys + end + end + + # Override serializable_hash to exclude sensitive attributes by default + # + # In general, prefer NOT to use serializable_hash / to_json / as_json in favor + # of serializers / entities instead which has an allowlist of attributes + def serializable_hash(options = nil) + return super unless prevent_sensitive_fields_from_serializable_hash? + return super if options && options[:unsafe_serialization_hash] + + options = options.try(:dup) || {} + options[:except] = Array(options[:except]).dup + + options[:except].concat self.class.attributes_exempt_from_serializable_hash + + if self.class.respond_to?(:encrypted_attributes) + options[:except].concat self.class.encrypted_attributes.keys + + # Per https://github.com/attr-encrypted/attr_encrypted/blob/a96693e9a2a25f4f910bf915e29b0f364f277032/lib/attr_encrypted.rb#L413 + options[:except].concat self.class.encrypted_attributes.values.map { |v| v[:attribute] } + options[:except].concat self.class.encrypted_attributes.values.map { |v| "#{v[:attribute]}_iv" } + end + + super(options) + end + + private + + def prevent_sensitive_fields_from_serializable_hash? + Feature.enabled?(:prevent_sensitive_fields_from_serializable_hash, default_enabled: :yaml) + end +end diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index f44ad8ebe90..d91ec161b84 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -8,6 +8,10 @@ module TokenAuthenticatable @encrypted_token_authenticatable_fields ||= [] end + def token_authenticatable_fields + @token_authenticatable_fields ||= [] + end + private def add_authentication_token_field(token_field, options = {}) @@ -23,6 +27,8 @@ module TokenAuthenticatable strategy = TokenAuthenticatableStrategies::Base .fabricate(self, token_field, options) + prevent_from_serialization(*strategy.token_fields) if respond_to?(:prevent_from_serialization) + if options.fetch(:unique, true) define_singleton_method("find_by_#{token_field}") do |token| strategy.find_token_authenticatable(token) @@ -82,9 +88,5 @@ module TokenAuthenticatable @token_authenticatable_module ||= const_set(:TokenAuthenticatable, Module.new).tap(&method(:include)) end - - def token_authenticatable_fields - @token_authenticatable_fields ||= [] - end end end diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb index 2cec4ab460e..2b677f37c89 100644 --- a/app/models/concerns/token_authenticatable_strategies/base.rb +++ b/app/models/concerns/token_authenticatable_strategies/base.rb @@ -23,6 +23,14 @@ module TokenAuthenticatableStrategies raise NotImplementedError end + def token_fields + result = [token_field] + + result << @expires_at_field if expirable? + + result + end + # Default implementation returns the token as-is def format_token(instance, token) instance.send("format_#{@token_field}", token) # rubocop:disable GitlabSecurity/PublicSend diff --git a/app/models/concerns/token_authenticatable_strategies/digest.rb b/app/models/concerns/token_authenticatable_strategies/digest.rb index 9926662ed66..5c94f25949f 100644 --- a/app/models/concerns/token_authenticatable_strategies/digest.rb +++ b/app/models/concerns/token_authenticatable_strategies/digest.rb @@ -2,6 +2,10 @@ module TokenAuthenticatableStrategies class Digest < Base + def token_fields + super + [token_field_name] + end + def find_token_authenticatable(token, unscoped = false) return unless token diff --git a/app/models/concerns/token_authenticatable_strategies/encrypted.rb b/app/models/concerns/token_authenticatable_strategies/encrypted.rb index e957d09fbc6..1db88c27181 100644 --- a/app/models/concerns/token_authenticatable_strategies/encrypted.rb +++ b/app/models/concerns/token_authenticatable_strategies/encrypted.rb @@ -2,6 +2,10 @@ module TokenAuthenticatableStrategies class Encrypted < Base + def token_fields + super + [encrypted_field] + end + def find_token_authenticatable(token, unscoped = false) return if token.blank? diff --git a/app/models/wiki.rb b/app/models/wiki.rb index e114e30d589..307f31edfef 100644 --- a/app/models/wiki.rb +++ b/app/models/wiki.rb @@ -150,10 +150,10 @@ class Wiki # the page. # # Returns an initialized WikiPage instance or nil - def find_page(title, version = nil) + def find_page(title, version = nil, load_content: true) page_title, page_dir = page_title_and_dir(title) - if page = wiki.page(title: page_title, version: version, dir: page_dir) + if page = wiki.page(title: page_title, version: version, dir: page_dir, load_content: load_content) WikiPage.new(self, page) end end diff --git a/config/feature_flags/development/prevent_sensitive_fields_from_serializable_hash.yml b/config/feature_flags/development/prevent_sensitive_fields_from_serializable_hash.yml new file mode 100644 index 00000000000..7bcbe6b79c2 --- /dev/null +++ b/config/feature_flags/development/prevent_sensitive_fields_from_serializable_hash.yml @@ -0,0 +1,8 @@ +--- +name: prevent_sensitive_fields_from_serializable_hash +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81773 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353878 +milestone: '14.9' +type: development +group: group::sharding +default_enabled: false diff --git a/danger/documentation/Dangerfile b/danger/documentation/Dangerfile index 918c787075e..41c75a9f176 100644 --- a/danger/documentation/Dangerfile +++ b/danger/documentation/Dangerfile @@ -5,7 +5,7 @@ def feature_mr? end DOCUMENTATION_UPDATE_MISSING = <<~MSG -~"feature::addition" and ~"feature::enhancement" merge requests normally have a documentation change. Consider adding a documentation update or confirming the documentation plan with the [Technical Writer counterpart](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers). +~"feature::addition" and ~"feature::enhancement" merge requests normally have a documentation change. Consider adding a documentation update or confirming the documentation plan with the [Technical Writer counterpart](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments). For more information, see: @@ -36,6 +36,6 @@ markdown(<<~MARKDOWN) The review does not need to block merging this merge request. See the: - [Metadata for the `*.md` files](https://docs.gitlab.com/ee/development/documentation/#metadata) that you've changed. The first few lines of each `*.md` file identify the stage and group most closely associated with your docs change. - - The [Technical Writer assigned](https://about.gitlab.com/handbook/engineering/technical-writing/#designated-technical-writers) for that stage and group. + - The [Technical Writer assigned](https://about.gitlab.com/handbook/engineering/technical-writing/#assignments) for that stage and group. - [Documentation workflows](https://docs.gitlab.com/ee/development/documentation/workflow.html) for information on when to assign a merge request for review. MARKDOWN diff --git a/doc/administration/encrypted_configuration.md b/doc/administration/encrypted_configuration.md index 9224def4a5a..baa6e967507 100644 --- a/doc/administration/encrypted_configuration.md +++ b/doc/administration/encrypted_configuration.md @@ -1,7 +1,7 @@ --- stage: Enablement group: Distribution -info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers" +info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments" type: reference --- diff --git a/doc/administration/package_information/index.md b/doc/administration/package_information/index.md index 2781f789409..c908f526569 100644 --- a/doc/administration/package_information/index.md +++ b/doc/administration/package_information/index.md @@ -1,7 +1,7 @@ --- stage: Enablement group: Distribution -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Package information **(FREE SELF)** diff --git a/doc/administration/package_information/licensing.md b/doc/administration/package_information/licensing.md index 02358c66993..a7bf5c52d7b 100644 --- a/doc/administration/package_information/licensing.md +++ b/doc/administration/package_information/licensing.md @@ -1,7 +1,7 @@ --- stage: Enablement group: Distribution -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Package Licensing **(FREE SELF)** diff --git a/doc/administration/package_information/omnibus_packages.md b/doc/administration/package_information/omnibus_packages.md index 115d6c394ad..21e3f5711ba 100644 --- a/doc/administration/package_information/omnibus_packages.md +++ b/doc/administration/package_information/omnibus_packages.md @@ -1,7 +1,7 @@ --- stage: Enablement group: Distribution -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Omnibus based packages and images **(FREE SELF)** diff --git a/doc/administration/package_information/postgresql_versions.md b/doc/administration/package_information/postgresql_versions.md index 97a35fb29ed..e707cb70187 100644 --- a/doc/administration/package_information/postgresql_versions.md +++ b/doc/administration/package_information/postgresql_versions.md @@ -1,7 +1,7 @@ --- stage: Enablement group: Distribution -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # PostgreSQL versions shipped with Omnibus GitLab **(FREE SELF)** diff --git a/doc/administration/package_information/signed_packages.md b/doc/administration/package_information/signed_packages.md index fb605f8d5be..d7bcfa113ff 100644 --- a/doc/administration/package_information/signed_packages.md +++ b/doc/administration/package_information/signed_packages.md @@ -1,7 +1,7 @@ --- stage: Enablement group: Distribution -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Package Signatures **(FREE SELF)** diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 6715bd78656..2ed55c9803c 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1,7 +1,7 @@ --- stage: Ecosystem group: Integrations -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- <!--- diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index 51fb12d9620..7b38ac39b96 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -2,7 +2,7 @@ type: reference, howto stage: Manage group: Authentication and Authorization -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # OAuth 2.0 identity provider API **(FREE)** diff --git a/doc/architecture/blueprints/container_registry_metadata_database/index.md b/doc/architecture/blueprints/container_registry_metadata_database/index.md index c1aac235085..ba0bee04f35 100644 --- a/doc/architecture/blueprints/container_registry_metadata_database/index.md +++ b/doc/architecture/blueprints/container_registry_metadata_database/index.md @@ -1,7 +1,7 @@ --- stage: Package group: Package -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments comments: false description: 'Container Registry metadata database' --- diff --git a/doc/architecture/blueprints/gitlab_to_kubernetes_communication/index.md b/doc/architecture/blueprints/gitlab_to_kubernetes_communication/index.md index 7039eb4cd02..b22636ac1d9 100644 --- a/doc/architecture/blueprints/gitlab_to_kubernetes_communication/index.md +++ b/doc/architecture/blueprints/gitlab_to_kubernetes_communication/index.md @@ -1,7 +1,7 @@ --- stage: Configure group: Configure -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments comments: false description: 'GitLab to Kubernetes communication' --- diff --git a/doc/ci/yaml/gitlab_ci_yaml.md b/doc/ci/yaml/gitlab_ci_yaml.md index 9bab4a7fc77..f3773fbf143 100644 --- a/doc/ci/yaml/gitlab_ci_yaml.md +++ b/doc/ci/yaml/gitlab_ci_yaml.md @@ -1,7 +1,7 @@ --- stage: Verify group: Pipeline Authoring -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments type: reference --- diff --git a/doc/development/bulk_import.md b/doc/development/bulk_import.md index ff0c8a19ca1..a2620faed35 100644 --- a/doc/development/bulk_import.md +++ b/doc/development/bulk_import.md @@ -1,7 +1,7 @@ --- stage: Manage group: Import -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # GitLab Group Migration diff --git a/doc/development/contributing/design.md b/doc/development/contributing/design.md index efa8d4b0c41..463a7ee0e0b 100644 --- a/doc/development/contributing/design.md +++ b/doc/development/contributing/design.md @@ -35,7 +35,7 @@ Check these aspects both when _designing_ and _reviewing_ UI changes. - Use clear and consistent [terminology](https://design.gitlab.com/content/terminology/). - Check grammar and spelling. - Consider help content and follow its [guidelines](https://design.gitlab.com/usability/helping-users/). -- Request review from the [appropriate Technical Writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers), +- Request review from the [appropriate Technical Writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments), indicating any specific files or lines they should review, and how to preview or understand the location/context of the text from the user's perspective. diff --git a/doc/development/export_csv.md b/doc/development/export_csv.md index ff827023a50..998e5b1fb3b 100644 --- a/doc/development/export_csv.md +++ b/doc/development/export_csv.md @@ -1,7 +1,7 @@ --- stage: Manage group: Import -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Export to CSV diff --git a/doc/install/aws/manual_install_aws.md b/doc/install/aws/manual_install_aws.md index eeb8c57bddf..b0e71cbc77e 100644 --- a/doc/install/aws/manual_install_aws.md +++ b/doc/install/aws/manual_install_aws.md @@ -390,7 +390,7 @@ persistence and is used to store session data, temporary cache information, and chance to deploy Redis in multiple availability zones. 1. In the settings section: 1. Give the cluster a name (`gitlab-redis`) and a description. - 1. For the version, select the latest of the `5.0` series (for example, `5.0.6`). + 1. For the version, select the latest. 1. Leave the port as `6379` since this is what we used in our Redis security group above. 1. Select the node type (at least `cache.t3.medium`, but adjust to your needs) and the number of replicas. 1. In the advanced settings section: diff --git a/doc/integration/security_partners/index.md b/doc/integration/security_partners/index.md index 2c7641124a0..51c2c72769e 100644 --- a/doc/integration/security_partners/index.md +++ b/doc/integration/security_partners/index.md @@ -1,7 +1,7 @@ --- stage: Secure group: Static Analysis -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments type: index --- diff --git a/doc/update/package/convert_to_ee.md b/doc/update/package/convert_to_ee.md index 887fdcb4f34..fe3bc8090ab 100644 --- a/doc/update/package/convert_to_ee.md +++ b/doc/update/package/convert_to_ee.md @@ -1,7 +1,7 @@ --- stage: Enablement group: Distribution -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Convert Community Edition to Enterprise Edition **(FREE SELF)** diff --git a/doc/update/package/downgrade.md b/doc/update/package/downgrade.md index 96b17a7632b..81f7d089bea 100644 --- a/doc/update/package/downgrade.md +++ b/doc/update/package/downgrade.md @@ -1,7 +1,7 @@ --- stage: Enablement group: Distribution -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Downgrade **(FREE SELF)** diff --git a/doc/update/package/index.md b/doc/update/package/index.md index d7c18f15e44..b6417e10917 100644 --- a/doc/update/package/index.md +++ b/doc/update/package/index.md @@ -1,7 +1,7 @@ --- stage: Enablement group: Distribution -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Upgrade GitLab by using the GitLab package **(FREE SELF)** diff --git a/doc/user/admin_area/reporting/spamcheck.md b/doc/user/admin_area/reporting/spamcheck.md index 7f5732d88ea..1021e4eb63a 100644 --- a/doc/user/admin_area/reporting/spamcheck.md +++ b/doc/user/admin_area/reporting/spamcheck.md @@ -1,7 +1,7 @@ --- stage: Enablement group: Distribution -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Spamcheck anti-spam service **(PREMIUM SELF)** diff --git a/doc/user/application_security/api_fuzzing/create_har_files.md b/doc/user/application_security/api_fuzzing/create_har_files.md index db0b2a32bcf..1ba19359fde 100644 --- a/doc/user/application_security/api_fuzzing/create_har_files.md +++ b/doc/user/application_security/api_fuzzing/create_har_files.md @@ -1,7 +1,7 @@ --- stage: Secure group: Dynamic Analysis -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments type: howto --- diff --git a/doc/user/clusters/agent/gitops.md b/doc/user/clusters/agent/gitops.md index 8e6449fe639..8f0e2255121 100644 --- a/doc/user/clusters/agent/gitops.md +++ b/doc/user/clusters/agent/gitops.md @@ -1,7 +1,7 @@ --- stage: Configure group: Configure -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Using a GitOps workflow for Kubernetes **(PREMIUM)** diff --git a/doc/user/clusters/agent/gitops/secrets_management.md b/doc/user/clusters/agent/gitops/secrets_management.md index 01720bd602f..cf520c881bf 100644 --- a/doc/user/clusters/agent/gitops/secrets_management.md +++ b/doc/user/clusters/agent/gitops/secrets_management.md @@ -1,7 +1,7 @@ --- stage: Configure group: Configure -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Managing Kubernetes secrets in a GitOps workflow diff --git a/doc/user/clusters/agent/repository.md b/doc/user/clusters/agent/repository.md index 6def4513614..19132a7fe10 100644 --- a/doc/user/clusters/agent/repository.md +++ b/doc/user/clusters/agent/repository.md @@ -1,7 +1,7 @@ --- stage: Configure group: Configure -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Working with the agent for Kubernetes **(FREE)** diff --git a/doc/user/clusters/agent/vulnerabilities.md b/doc/user/clusters/agent/vulnerabilities.md index 41a80f43467..480b09ff2ab 100644 --- a/doc/user/clusters/agent/vulnerabilities.md +++ b/doc/user/clusters/agent/vulnerabilities.md @@ -1,7 +1,7 @@ --- stage: Configure group: Configure -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Container vulnerability scanning **(ULTIMATE)** diff --git a/doc/user/group/value_stream_analytics/index.md b/doc/user/group/value_stream_analytics/index.md index f0ee9a8da56..5ea8512ed5a 100644 --- a/doc/user/group/value_stream_analytics/index.md +++ b/doc/user/group/value_stream_analytics/index.md @@ -2,7 +2,7 @@ type: reference stage: Manage group: Optimize -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Value stream analytics for groups **(PREMIUM)** diff --git a/doc/user/project/clusters/protect/container_host_security/index.md b/doc/user/project/clusters/protect/container_host_security/index.md index a0297a7a86f..f6f31ee8f36 100644 --- a/doc/user/project/clusters/protect/container_host_security/index.md +++ b/doc/user/project/clusters/protect/container_host_security/index.md @@ -1,7 +1,7 @@ --- stage: Protect group: Container Security -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Container Host Security **(FREE)** diff --git a/doc/user/project/clusters/protect/container_host_security/quick_start_guide.md b/doc/user/project/clusters/protect/container_host_security/quick_start_guide.md index 4e56b7e5140..25e2f6ddb91 100644 --- a/doc/user/project/clusters/protect/container_host_security/quick_start_guide.md +++ b/doc/user/project/clusters/protect/container_host_security/quick_start_guide.md @@ -1,7 +1,7 @@ --- stage: Protect group: Container Security -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Getting started with Container Host Security **(FREE)** diff --git a/doc/user/project/clusters/protect/container_network_security/index.md b/doc/user/project/clusters/protect/container_network_security/index.md index 7176a1cf1b9..eeaf7e82ef4 100644 --- a/doc/user/project/clusters/protect/container_network_security/index.md +++ b/doc/user/project/clusters/protect/container_network_security/index.md @@ -1,7 +1,7 @@ --- stage: Protect group: Container Security -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Container Network Security **(FREE)** diff --git a/doc/user/project/clusters/protect/container_network_security/quick_start_guide.md b/doc/user/project/clusters/protect/container_network_security/quick_start_guide.md index e6c91302d7b..43f4ea6c326 100644 --- a/doc/user/project/clusters/protect/container_network_security/quick_start_guide.md +++ b/doc/user/project/clusters/protect/container_network_security/quick_start_guide.md @@ -1,7 +1,7 @@ --- stage: Protect group: Container Security -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Getting started with Container Network Security **(FREE)** diff --git a/doc/user/project/clusters/protect/index.md b/doc/user/project/clusters/protect/index.md index 473195f4c17..3a80132fb50 100644 --- a/doc/user/project/clusters/protect/index.md +++ b/doc/user/project/clusters/protect/index.md @@ -1,7 +1,7 @@ --- stage: Protect group: Container Security -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Protecting your deployed applications **(FREE)** diff --git a/doc/user/usage_quotas.md b/doc/user/usage_quotas.md index 5e310be4c9c..84a2449f481 100644 --- a/doc/user/usage_quotas.md +++ b/doc/user/usage_quotas.md @@ -2,7 +2,7 @@ type: howto stage: Fulfillment group: Utilization -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- # Storage usage quota **(FREE)** diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb index 194f5da0a5c..4bab94968d7 100644 --- a/lib/gitlab/git/wiki.rb +++ b/lib/gitlab/git/wiki.rb @@ -93,9 +93,9 @@ module Gitlab end end - def page(title:, version: nil, dir: nil) + def page(title:, version: nil, dir: nil, load_content: true) wrapped_gitaly_errors do - gitaly_find_page(title: title, version: version, dir: dir) + gitaly_find_page(title: title, version: version, dir: dir, load_content: load_content) end end @@ -121,10 +121,10 @@ module Gitlab gitaly_wiki_client.update_page(page_path, title, format, content, commit_details) end - def gitaly_find_page(title:, version: nil, dir: nil) + def gitaly_find_page(title:, version: nil, dir: nil, load_content: true) return unless title.present? - wiki_page, version = gitaly_wiki_client.find_page(title: title, version: version, dir: dir) + wiki_page, version = gitaly_wiki_client.find_page(title: title, version: version, dir: dir, load_content: load_content) return unless wiki_page Gitlab::Git::WikiPage.new(wiki_page, version) diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb index 3613cd01122..ca839b232cf 100644 --- a/lib/gitlab/gitaly_client/wiki_service.rb +++ b/lib/gitlab/gitaly_client/wiki_service.rb @@ -64,12 +64,13 @@ module Gitlab GitalyClient.call(@repository.storage, :wiki_service, :wiki_update_page, enum, timeout: GitalyClient.medium_timeout) end - def find_page(title:, version: nil, dir: nil) + def find_page(title:, version: nil, dir: nil, load_content: true) request = Gitaly::WikiFindPageRequest.new( repository: @gitaly_repo, title: encode_binary(title), revision: encode_binary(version), - directory: encode_binary(dir) + directory: encode_binary(dir), + skip_content: !load_content ) response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_page, request, timeout: GitalyClient.fast_timeout) diff --git a/lib/gitlab/json_cache.rb b/lib/gitlab/json_cache.rb index d2916a01809..d5c018cfc68 100644 --- a/lib/gitlab/json_cache.rb +++ b/lib/gitlab/json_cache.rb @@ -43,7 +43,9 @@ module Gitlab end def write(key, value, options = nil) - backend.write(cache_key(key), value.to_json, options) + # As we use json as the serialization format, return everything from + # ActiveModel objects included encrypted values. + backend.write(cache_key(key), value.to_json(unsafe_serialization_hash: true), options) end def fetch(key, options = {}, &block) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 0c34c8b3556..cb2427fe50a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5885,6 +5885,9 @@ msgstr "" msgid "Boards|An error occurred while fetching the board swimlanes. Please reload the page." msgstr "" +msgid "Boards|An error occurred while fetching the board. Please reload the page." +msgstr "" + msgid "Boards|An error occurred while generating lists. Please reload the page." msgstr "" diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_pipeline_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_pipeline_spec.rb new file mode 100644 index 00000000000..484c32956e3 --- /dev/null +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_pipeline_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require_relative 'gitlab_project_migration_common' + +module QA + RSpec.describe 'Manage' do + describe 'Gitlab migration' do + include_context 'with gitlab project migration' + + context 'with ci pipeline' do + let!(:source_project_with_readme) { true } + + let(:source_pipelines) do + source_project.pipelines.map do |pipeline| + pipeline.except(:id, :web_url, :project_id) + end + end + + let(:imported_pipelines) do + imported_project.pipelines.map do |pipeline| + pipeline.except(:id, :web_url, :project_id) + end + end + + before do + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.api_client = api_client + commit.project = source_project + commit.commit_message = 'Add .gitlab-ci.yml' + commit.add_files( + [ + { + file_path: '.gitlab-ci.yml', + content: <<~YML + test-success: + script: echo 'OK' + YML + } + ] + ) + end + + Support::Waiter.wait_until(max_duration: 10, sleep_interval: 1) do + !source_project.pipelines.empty? + end + end + + it( + 'successfully imports ci pipeline', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/354650' + ) do + expect_import_finished + + expect(imported_pipelines).to eq(source_pipelines) + end + end + end + end +end diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_project_migration_common.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_project_migration_common.rb index a86b8903645..70f19e9f3d7 100644 --- a/qa/qa/specs/features/api/1_manage/migration/gitlab_project_migration_common.rb +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_project_migration_common.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true module QA - # Disable on staging until bulk_import_projects toggle is on by default + # Disable on live envs until bulk_import_projects toggle is on by default # Otherwise tests running in parallel can disable feature in the middle of other test - RSpec.shared_context 'with gitlab project migration', :requires_admin, except: { subdomain: :staging } do + RSpec.shared_context 'with gitlab project migration', :requires_admin, :skip_live_env do let(:source_project_with_readme) { false } let(:import_wait_duration) { { max_duration: 300, sleep_interval: 2 } } let(:admin_api_client) { Runtime::API::Client.as_admin } @@ -79,8 +79,6 @@ module QA # Log failures for easier debugging Runtime::Logger.warn("Import failures: #{import_failures}") if example.exception && !import_failures.empty? ensure - Runtime::Feature.disable(:bulk_import_projects) - user.remove_via_api! end end diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb index 6951d8298e5..d472134a2c7 100644 --- a/spec/features/markdown/copy_as_gfm_spec.rb +++ b/spec/features/markdown/copy_as_gfm_spec.rb @@ -7,10 +7,6 @@ RSpec.describe 'Copy as GFM', :js do include RepoHelpers include ActionView::Helpers::JavaScriptHelper - before do - stub_feature_flags(refactor_blob_viewer: false) # This stub will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/350454 - end - describe 'Copying rendered GFM' do before do @feat = MarkdownFeature.new @@ -764,8 +760,8 @@ RSpec.describe 'Copy as GFM', :js do context 'selecting one word of text' do it 'copies as inline code' do verify( - '.line[id="LC9"] .no', - '`RuntimeError`' + '.line[id="LC10"]', + '`end`' ) end end @@ -834,6 +830,7 @@ RSpec.describe 'Copy as GFM', :js do end def verify(selector, gfm, target: nil) + expect(page).to have_selector('.js-syntax-highlight') html = html_for_selector(selector) output_gfm = html_to_gfm(html, 'transformCodeSelection', target: target) wait_for_requests diff --git a/spec/features/projects/jobs/user_triggers_manual_job_with_variables_spec.rb b/spec/features/projects/jobs/user_triggers_manual_job_with_variables_spec.rb index e8a14694d88..eea7e070a35 100644 --- a/spec/features/projects/jobs/user_triggers_manual_job_with_variables_spec.rb +++ b/spec/features/projects/jobs/user_triggers_manual_job_with_variables_spec.rb @@ -28,7 +28,7 @@ RSpec.describe 'User triggers manual job with variables', :js do wait_for_requests - expect(build.job_variables.as_json).to contain_exactly( + expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly( hash_including('key' => 'key_name', 'value' => 'key_value')) end end diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js index 26a5bf34595..0c044deb78c 100644 --- a/spec/frontend/boards/components/boards_selector_spec.js +++ b/spec/frontend/boards/components/boards_selector_spec.js @@ -41,6 +41,7 @@ describe('BoardsSelector', () => { ...defaultStore, actions: { setError: jest.fn(), + setBoardConfig: jest.fn(), }, getters: { isGroupBoard: () => isGroupBoard, diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index 24096fddea6..ec9342cffc2 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -8,6 +8,37 @@ import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue'; +export const mockBoard = { + milestone: { + id: 'gid://gitlab/Milestone/114', + title: '14.9', + }, + iteration: { + id: 'gid://gitlab/Iteration/124', + title: 'Iteration 9', + }, + assignee: { + id: 'gid://gitlab/User/1', + username: 'admin', + }, + labels: { + nodes: [{ id: 'gid://gitlab/Label/32', title: 'Deliverable' }], + }, + weight: 2, +}; + +export const mockBoardConfig = { + milestoneId: 'gid://gitlab/Milestone/114', + milestoneTitle: '14.9', + iterationId: 'gid://gitlab/Iteration/124', + iterationTitle: 'Iteration 9', + assigneeId: 'gid://gitlab/User/1', + assigneeUsername: 'admin', + labels: [{ id: 'gid://gitlab/Label/32', title: 'Deliverable' }], + labelIds: ['gid://gitlab/Label/32'], + weight: 2, +}; + export const boardObj = { id: 1, name: 'test', diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index 0eca0cb3ee5..ad661a31556 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -32,6 +32,8 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import projectBoardMilestones from '~/boards/graphql/project_board_milestones.query.graphql'; import groupBoardMilestones from '~/boards/graphql/group_board_milestones.query.graphql'; import { + mockBoard, + mockBoardConfig, mockLists, mockListsById, mockIssue, @@ -60,6 +62,52 @@ beforeEach(() => { window.gon = { features: {} }; }); +describe('fetchBoard', () => { + const payload = { + fullPath: 'gitlab-org', + fullBoardId: 'gid://gitlab/Board/1', + boardType: 'project', + }; + + const queryResponse = { + data: { + workspace: { + board: mockBoard, + }, + }, + }; + + it('should commit mutation RECEIVE_BOARD_SUCCESS and dispatch setBoardConfig on success', async () => { + jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse); + + await testAction({ + action: actions.fetchBoard, + payload, + expectedMutations: [ + { + type: types.RECEIVE_BOARD_SUCCESS, + payload: mockBoard, + }, + ], + expectedActions: [{ type: 'setBoardConfig', payload: mockBoard }], + }); + }); + + it('should commit mutation RECEIVE_BOARD_FAILURE on failure', async () => { + jest.spyOn(gqlClient, 'query').mockResolvedValue(Promise.reject()); + + await testAction({ + action: actions.fetchBoard, + payload, + expectedMutations: [ + { + type: types.RECEIVE_BOARD_FAILURE, + }, + ], + }); + }); +}); + describe('setInitialBoardData', () => { it('sets data object', () => { const mockData = { @@ -67,13 +115,21 @@ describe('setInitialBoardData', () => { bar: 'baz', }; - return testAction( - actions.setInitialBoardData, - mockData, - {}, - [{ type: types.SET_INITIAL_BOARD_DATA, payload: mockData }], - [], - ); + return testAction({ + action: actions.setInitialBoardData, + payload: mockData, + expectedMutations: [{ type: types.SET_INITIAL_BOARD_DATA, payload: mockData }], + }); + }); +}); + +describe('setBoardConfig', () => { + it('sets board config object from board object', () => { + return testAction({ + action: actions.setBoardConfig, + payload: mockBoard, + expectedMutations: [{ type: types.SET_BOARD_CONFIG, payload: mockBoardConfig }], + }); }); }); @@ -87,7 +143,7 @@ describe('setFilters', () => { }, ], [ - "and use 'assigneeWildcardId' as filter variable for 'assigneId' param", + "and use 'assigneeWildcardId' as filter variable for 'assigneeId' param", { filters: { assigneeId: 'None' }, filterVariables: { assigneeWildcardId: 'NONE', not: {} }, diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js index 0e830258327..738737bf4b6 100644 --- a/spec/frontend/boards/stores/mutations_spec.js +++ b/spec/frontend/boards/stores/mutations_spec.js @@ -4,6 +4,7 @@ import * as types from '~/boards/stores/mutation_types'; import mutations from '~/boards/stores/mutations'; import defaultState from '~/boards/stores/state'; import { + mockBoard, mockLists, rawIssue, mockIssue, @@ -33,6 +34,27 @@ describe('Board Store Mutations', () => { state = defaultState(); }); + describe('RECEIVE_BOARD_SUCCESS', () => { + it('Should set board to state', () => { + mutations[types.RECEIVE_BOARD_SUCCESS](state, mockBoard); + + expect(state.board).toEqual({ + ...mockBoard, + labels: mockBoard.labels.nodes, + }); + }); + }); + + describe('RECEIVE_BOARD_FAILURE', () => { + it('Should set error in state', () => { + mutations[types.RECEIVE_BOARD_FAILURE](state); + + expect(state.error).toEqual( + 'An error occurred while fetching the board. Please reload the page.', + ); + }); + }); + describe('SET_INITIAL_BOARD_DATA', () => { it('Should set initial Boards data to state', () => { const allowSubEpics = true; @@ -40,9 +62,6 @@ describe('Board Store Mutations', () => { const fullPath = 'gitlab-org'; const boardType = 'group'; const disabled = false; - const boardConfig = { - milestoneTitle: 'Milestone 1', - }; const issuableType = issuableTypes.issue; mutations[types.SET_INITIAL_BOARD_DATA](state, { @@ -51,7 +70,6 @@ describe('Board Store Mutations', () => { fullPath, boardType, disabled, - boardConfig, issuableType, }); @@ -60,11 +78,23 @@ describe('Board Store Mutations', () => { expect(state.fullPath).toEqual(fullPath); expect(state.boardType).toEqual(boardType); expect(state.disabled).toEqual(disabled); - expect(state.boardConfig).toEqual(boardConfig); expect(state.issuableType).toEqual(issuableType); }); }); + describe('SET_BOARD_CONFIG', () => { + it('Should set board config data o state', () => { + const boardConfig = { + milestoneId: 1, + milestoneTitle: 'Milestone 1', + }; + + mutations[types.SET_BOARD_CONFIG](state, boardConfig); + + expect(state.boardConfig).toEqual(boardConfig); + }); + }); + describe('RECEIVE_BOARD_LISTS_SUCCESS', () => { it('Should set boardLists to state', () => { mutations[types.RECEIVE_BOARD_LISTS_SUCCESS](state, initialBoardListsState); diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js index a78dab1a44d..a5877aa6e3e 100644 --- a/spec/frontend/lib/utils/text_markdown_spec.js +++ b/spec/frontend/lib/utils/text_markdown_spec.js @@ -181,12 +181,13 @@ describe('init markdown', () => { ${'- [ ] item'} | ${'- [ ] item\n- [ ] '} ${'- [x] item'} | ${'- [x] item\n- [x] '} ${'- item\n - second'} | ${'- item\n - second\n - '} - ${'1. item'} | ${'1. item\n1. '} - ${'1. [ ] item'} | ${'1. [ ] item\n1. [ ] '} - ${'1. [x] item'} | ${'1. [x] item\n1. [x] '} - ${'108. item'} | ${'108. item\n108. '} + ${'1. item'} | ${'1. item\n2. '} + ${'1. [ ] item'} | ${'1. [ ] item\n2. [ ] '} + ${'1. [x] item'} | ${'1. [x] item\n2. [x] '} + ${'108. item'} | ${'108. item\n109. '} ${'108. item\n - second'} | ${'108. item\n - second\n - '} - ${'108. item\n 1. second'} | ${'108. item\n 1. second\n 1. '} + ${'108. item\n 1. second'} | ${'108. item\n 1. second\n 2. '} + ${'non-item, will not change'} | ${'non-item, will not change'} `('adds correct list continuation characters', ({ text, expected }) => { textArea.value = text; textArea.setSelectionRange(text.length, text.length); @@ -207,10 +208,10 @@ describe('init markdown', () => { ${'- [ ] item\n- [ ] '} | ${'- [ ] item\n'} ${'- [x] item\n- [x] '} | ${'- [x] item\n'} ${'- item\n - second\n - '} | ${'- item\n - second\n'} - ${'1. item\n1. '} | ${'1. item\n'} - ${'1. [ ] item\n1. [ ] '} | ${'1. [ ] item\n'} - ${'1. [x] item\n1. [x] '} | ${'1. [x] item\n'} - ${'108. item\n108. '} | ${'108. item\n'} + ${'1. item\n2. '} | ${'1. item\n'} + ${'1. [ ] item\n2. [ ] '} | ${'1. [ ] item\n'} + ${'1. [x] item\n2. [x] '} | ${'1. [x] item\n'} + ${'108. item\n109. '} | ${'108. item\n'} ${'108. item\n - second\n - '} | ${'108. item\n - second\n'} ${'108. item\n 1. second\n 1. '} | ${'108. item\n 1. second\n'} `('adds correct list continuation characters', ({ text, expected }) => { @@ -243,6 +244,23 @@ describe('init markdown', () => { expect(textArea.value).toEqual(expected); }); + it.each` + text | add_at | expected + ${'1. one\n2. two\n3. three'} | ${13} | ${'1. one\n2. two\n2. \n3. three'} + ${'108. item\n 5. second\n 6. six\n 7. seven'} | ${36} | ${'108. item\n 5. second\n 6. six\n 6. \n 7. seven'} + `( + 'adds correct numbered continuation characters when in middle of list', + ({ text, add_at, expected }) => { + textArea.value = text; + textArea.setSelectionRange(add_at, add_at); + + textArea.addEventListener('keydown', keypressNoteText); + textArea.dispatchEvent(enterEvent); + + expect(textArea.value).toEqual(expected); + }, + ); + it('does nothing if feature flag disabled', () => { gon.features = { markdownContinueLists: false }; @@ -262,8 +280,8 @@ describe('init markdown', () => { }); describe('with selection', () => { - const text = 'initial selected value'; - const selected = 'selected'; + let text = 'initial selected value'; + let selected = 'selected'; let selectedIndex; beforeEach(() => { @@ -409,6 +427,46 @@ describe('init markdown', () => { expectedText.indexOf(expectedSelectionText, 1) + expectedSelectionText.length, ); }); + + it('adds block tags on line above and below selection', () => { + selected = 'this text\nis multiple\nlines'; + text = `before \n${selected}\nafter `; + + textArea.value = text; + selectedIndex = text.indexOf(selected); + textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length); + + insertMarkdownText({ + textArea, + text, + tag: '', + blockTag: '***', + selected, + wrap: true, + }); + + expect(textArea.value).toEqual(`before \n***\n${selected}\n***\nafter `); + }); + + it('removes block tags on line above and below selection', () => { + selected = 'this text\nis multiple\nlines'; + text = `before \n***\n${selected}\n***\nafter `; + + textArea.value = text; + selectedIndex = text.indexOf(selected); + textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length); + + insertMarkdownText({ + textArea, + text, + tag: '', + blockTag: '***', + selected, + wrap: true, + }); + + expect(textArea.value).toEqual(`before \n${selected}\nafter `); + }); }); }); }); @@ -460,7 +518,31 @@ describe('init markdown', () => { expect(editor.replaceSelectedText).toHaveBeenCalledWith(`***\n${selected}\n***\n`, undefined); }); - it('uses ace editor to navigate back tag length when nothing is selected', () => { + it('removes block tags on line above and below selection', () => { + const selected = 'this text\nis multiple\nlines'; + const text = `before\n***\n${selected}\n***\nafter`; + + editor.getSelection = jest.fn().mockReturnValue({ + startLineNumber: 2, + startColumn: 1, + endLineNumber: 4, + endColumn: 2, + setSelectionRange: jest.fn(), + }); + + insertMarkdownText({ + text, + tag: '', + blockTag: '***', + selected, + wrap: true, + editor, + }); + + expect(editor.replaceSelectedText).toHaveBeenCalledWith(`${selected}\n`, undefined); + }); + + it('uses editor to navigate back tag length when nothing is selected', () => { editor.getSelection = jest.fn().mockReturnValue({ startLineNumber: 1, startColumn: 1, @@ -480,7 +562,7 @@ describe('init markdown', () => { expect(editor.moveCursor).toHaveBeenCalledWith(-1); }); - it('ace editor does not navigate back when there is selected text', () => { + it('editor does not navigate back when there is selected text', () => { insertMarkdownText({ text: editor.getValue, tag: '*', diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js index 2010bac7060..ab579945e22 100644 --- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js @@ -7,6 +7,7 @@ import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vu import { ROUGE_TO_HLJS_LANGUAGE_MAP } from '~/vue_shared/components/source_viewer/constants'; import LineNumbers from '~/vue_shared/components/line_numbers.vue'; import waitForPromises from 'helpers/wait_for_promises'; +import * as sourceViewerUtils from '~/vue_shared/components/source_viewer/utils'; jest.mock('highlight.js/lib/core'); Vue.use(VueRouter); @@ -36,6 +37,7 @@ describe('Source Viewer component', () => { beforeEach(() => { hljs.highlight.mockImplementation(() => ({ value: highlightedContent })); hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent })); + jest.spyOn(sourceViewerUtils, 'wrapLines'); return createComponent(); }); @@ -73,6 +75,10 @@ describe('Source Viewer component', () => { expect(findLoadingIcon().exists()).toBe(true); }); + it('calls the wrapLines helper method with highlightedContent and mappedLanguage', () => { + expect(sourceViewerUtils.wrapLines).toHaveBeenCalledWith(highlightedContent, mappedLanguage); + }); + it('renders Line Numbers', () => { expect(findLineNumbers().props('lines')).toBe(1); }); diff --git a/spec/frontend/vue_shared/components/source_viewer/utils_spec.js b/spec/frontend/vue_shared/components/source_viewer/utils_spec.js index 937c3b26c67..0631e7efd54 100644 --- a/spec/frontend/vue_shared/components/source_viewer/utils_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/utils_spec.js @@ -2,12 +2,25 @@ import { wrapLines } from '~/vue_shared/components/source_viewer/utils'; describe('Wrap lines', () => { it.each` - input | output - ${'line 1'} | ${'<span id="LC1" class="line">line 1</span>'} - ${'line 1\nline 2'} | ${`<span id="LC1" class="line">line 1</span>\n<span id="LC2" class="line">line 2</span>`} - ${'<span class="hljs-code">line 1\nline 2</span>'} | ${`<span id="LC1" class="hljs-code">line 1\n<span id="LC2" class="line">line 2</span></span>`} - ${'<span class="hljs-code">```bash'} | ${'<span id="LC1" class="hljs-code">```bash'} - `('returns lines wrapped in spans containing line numbers', ({ input, output }) => { - expect(wrapLines(input)).toBe(output); + content | language | output + ${'line 1'} | ${'javascript'} | ${'<span id="LC1" lang="javascript" class="line">line 1</span>'} + ${'line 1\nline 2'} | ${'html'} | ${`<span id="LC1" lang="html" class="line">line 1</span>\n<span id="LC2" lang="html" class="line">line 2</span>`} + ${'<span class="hljs-code">line 1\nline 2</span>'} | ${'html'} | ${`<span id="LC1" lang="html" class="hljs-code">line 1\n<span id="LC2" lang="html" class="line">line 2</span></span>`} + ${'<span class="hljs-code">```bash'} | ${'bash'} | ${'<span id="LC1" lang="bash" class="hljs-code">```bash'} + ${'<span class="hljs-code">```bash'} | ${'valid-language1'} | ${'<span id="LC1" lang="valid-language1" class="hljs-code">```bash'} + ${'<span class="hljs-code">```bash'} | ${'valid_language2'} | ${'<span id="LC1" lang="valid_language2" class="hljs-code">```bash'} + `('returns lines wrapped in spans containing line numbers', ({ content, language, output }) => { + expect(wrapLines(content, language)).toBe(output); + }); + + it.each` + language + ${'invalidLanguage>'} + ${'"invalidLanguage"'} + ${'<invalidLanguage'} + `('returns lines safely without XSS language is not valid', ({ language }) => { + expect(wrapLines('<span class="hljs-code">```bash', language)).toBe( + '<span id="LC1" lang="" class="hljs-code">```bash', + ); }); }); diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb index ee0c0e2708e..dddcf8c40fc 100644 --- a/spec/lib/gitlab/git/wiki_spec.rb +++ b/spec/lib/gitlab/git/wiki_spec.rb @@ -48,14 +48,26 @@ RSpec.describe Gitlab::Git::Wiki do end it 'returns the right page' do - expect(subject.page(title: 'page1', dir: '').url_path).to eq 'page1' - expect(subject.page(title: 'page1', dir: 'foo').url_path).to eq 'foo/page1' + page = subject.page(title: 'page1', dir: '') + expect(page.url_path).to eq 'page1' + expect(page.raw_data).to eq 'content' + + page = subject.page(title: 'page1', dir: 'foo') + expect(page.url_path).to eq 'foo/page1' + expect(page.raw_data).to eq 'content foo/page1' end it 'returns nil for invalid arguments' do expect(subject.page(title: '')).to be_nil expect(subject.page(title: 'foo', version: ':')).to be_nil end + + it 'does not return content if load_content param is set to false' do + page = subject.page(title: 'page1', dir: '', load_content: false) + + expect(page.url_path).to eq 'page1' + expect(page.raw_data).to be_empty + end end describe '#preview_slug' do diff --git a/spec/models/concerns/sensitive_serializable_hash_spec.rb b/spec/models/concerns/sensitive_serializable_hash_spec.rb new file mode 100644 index 00000000000..923f9e80c1f --- /dev/null +++ b/spec/models/concerns/sensitive_serializable_hash_spec.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SensitiveSerializableHash do + describe '.prevent_from_serialization' do + let(:test_class) do + Class.new do + include ActiveModel::Serialization + include SensitiveSerializableHash + + attr_accessor :name, :super_secret + + prevent_from_serialization :super_secret + + def attributes + { 'name' => nil, 'super_secret' => nil } + end + end + end + + let(:model) { test_class.new } + + it 'does not include the field in serializable_hash' do + expect(model.serializable_hash).not_to include('super_secret') + end + + context 'unsafe_serialization_hash option' do + it 'includes the field in serializable_hash' do + expect(model.serializable_hash(unsafe_serialization_hash: true)).to include('super_secret') + end + end + + context 'when prevent_sensitive_fields_from_serializable_hash feature flag is disabled' do + before do + stub_feature_flags(prevent_sensitive_fields_from_serializable_hash: false) + end + + it 'includes the field in serializable_hash' do + expect(model.serializable_hash).to include('super_secret') + end + end + end + + describe '#serializable_hash' do + shared_examples "attr_encrypted attribute" do |klass, attribute_name| + context "#{klass.name}\##{attribute_name}" do + let(:attributes) { [attribute_name, "encrypted_#{attribute_name}", "encrypted_#{attribute_name}_iv"] } + + it 'has a encrypted_attributes field' do + expect(klass.encrypted_attributes).to include(attribute_name.to_sym) + end + + it 'does not include the attribute in serializable_hash', :aggregate_failures do + attributes.each do |attribute| + expect(model.attributes).to include(attribute) # double-check the attribute does exist + + expect(model.serializable_hash).not_to include(attribute) + expect(model.to_json).not_to include(attribute) + expect(model.as_json).not_to include(attribute) + end + end + + context 'unsafe_serialization_hash option' do + it 'includes the field in serializable_hash' do + attributes.each do |attribute| + expect(model.attributes).to include(attribute) # double-check the attribute does exist + + expect(model.serializable_hash(unsafe_serialization_hash: true)).to include(attribute) + expect(model.to_json(unsafe_serialization_hash: true)).to include(attribute) + expect(model.as_json(unsafe_serialization_hash: true)).to include(attribute) + end + end + end + end + end + + it_behaves_like 'attr_encrypted attribute', WebHook, 'token' do + let_it_be(:model) { create(:system_hook) } + end + + it_behaves_like 'attr_encrypted attribute', Ci::InstanceVariable, 'value' do + let_it_be(:model) { create(:ci_instance_variable) } + end + + shared_examples "add_authentication_token_field attribute" do |klass, attribute_name, encrypted_attribute: true, digest_attribute: false| + context "#{klass.name}\##{attribute_name}" do + let(:attributes) do + if digest_attribute + ["#{attribute_name}_digest"] + elsif encrypted_attribute + [attribute_name, "#{attribute_name}_encrypted"] + else + [attribute_name] + end + end + + it 'has a add_authentication_token_field field' do + expect(klass.token_authenticatable_fields).to include(attribute_name.to_sym) + end + + it 'does not include the attribute in serializable_hash', :aggregate_failures do + attributes.each do |attribute| + expect(model.attributes).to include(attribute) # double-check the attribute does exist + + expect(model.serializable_hash).not_to include(attribute) + expect(model.to_json).not_to include(attribute) + expect(model.as_json).not_to include(attribute) + end + end + + context 'unsafe_serialization_hash option' do + it 'includes the field in serializable_hash' do + attributes.each do |attribute| + expect(model.attributes).to include(attribute) # double-check the attribute does exist + + expect(model.serializable_hash(unsafe_serialization_hash: true)).to include(attribute) + expect(model.to_json(unsafe_serialization_hash: true)).to include(attribute) + expect(model.as_json(unsafe_serialization_hash: true)).to include(attribute) + end + end + end + end + end + + it_behaves_like 'add_authentication_token_field attribute', Ci::Runner, 'token' do + let_it_be(:model) { create(:ci_runner) } + + it 'does not include token_expires_at in serializable_hash' do + attribute = 'token_expires_at' + + expect(model.attributes).to include(attribute) # double-check the attribute does exist + + expect(model.serializable_hash).not_to include(attribute) + expect(model.to_json).not_to include(attribute) + expect(model.as_json).not_to include(attribute) + end + end + + it_behaves_like 'add_authentication_token_field attribute', ApplicationSetting, 'health_check_access_token', encrypted_attribute: false do + # health_check_access_token_encrypted column does not exist + let_it_be(:model) { create(:application_setting) } + end + + it_behaves_like 'add_authentication_token_field attribute', PersonalAccessToken, 'token', encrypted_attribute: false, digest_attribute: true do + # PersonalAccessToken only has token_digest column + let_it_be(:model) { create(:personal_access_token) } + end + end +end diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index 8a9e0248ed3..5fec8ad1113 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -9,6 +9,12 @@ RSpec.shared_examples 'TokenAuthenticatable' do it { is_expected.to respond_to("set_#{token_field}") } it { is_expected.to respond_to("reset_#{token_field}!") } end + + describe 'SensitiveSerializableHash' do + it 'includes the token field in list of sensitive attributes prevented from serialization' do + expect(described_class.attributes_exempt_from_serializable_hash).to include(token_field) + end + end end RSpec.describe User, 'TokenAuthenticatable' do diff --git a/spec/models/concerns/token_authenticatable_strategies/base_spec.rb b/spec/models/concerns/token_authenticatable_strategies/base_spec.rb index bccef9b9554..89ddc797a9d 100644 --- a/spec/models/concerns/token_authenticatable_strategies/base_spec.rb +++ b/spec/models/concerns/token_authenticatable_strategies/base_spec.rb @@ -6,6 +6,24 @@ RSpec.describe TokenAuthenticatableStrategies::Base do let(:instance) { double(:instance) } let(:field) { double(:field) } + describe '#token_fields' do + let(:strategy) { described_class.new(instance, field, options) } + let(:field) { 'some_token' } + let(:options) { {} } + + it 'includes the token field' do + expect(strategy.token_fields).to contain_exactly(field) + end + + context 'with expires_at option' do + let(:options) { { expires_at: true } } + + it 'includes the token_expires_at field' do + expect(strategy.token_fields).to contain_exactly(field, 'some_token_expires_at') + end + end + end + describe '.fabricate' do context 'when digest stragegy is specified' do it 'fabricates digest strategy object' do diff --git a/spec/models/concerns/token_authenticatable_strategies/digest_spec.rb b/spec/models/concerns/token_authenticatable_strategies/digest_spec.rb new file mode 100644 index 00000000000..bcd6e1e7316 --- /dev/null +++ b/spec/models/concerns/token_authenticatable_strategies/digest_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe TokenAuthenticatableStrategies::Digest do + let(:model) { class_double('Project') } + let(:options) { { digest: true } } + + subject(:strategy) do + described_class.new(model, 'some_field', options) + end + + describe '#token_fields' do + it 'includes the digest field' do + expect(strategy.token_fields).to contain_exactly('some_field', 'some_field_digest') + end + end +end diff --git a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb index 458dfb47394..e0ebb86585a 100644 --- a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb +++ b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb @@ -14,10 +14,18 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do Gitlab::CryptoHelper.aes256_gcm_encrypt('my-value') end - subject do + subject(:strategy) do described_class.new(model, 'some_field', options) end + describe '#token_fields' do + let(:options) { { encrypted: :required } } + + it 'includes the encrypted field' do + expect(strategy.token_fields).to contain_exactly('some_field', 'some_field_encrypted') + end + end + describe '#find_token_authenticatable' do context 'when encryption is required' do let(:options) { { encrypted: :required } } diff --git a/spec/services/ci/job_artifacts/create_service_spec.rb b/spec/services/ci/job_artifacts/create_service_spec.rb index 2d309bfe425..b8487e438a9 100644 --- a/spec/services/ci/job_artifacts/create_service_spec.rb +++ b/spec/services/ci/job_artifacts/create_service_spec.rb @@ -175,7 +175,7 @@ RSpec.describe Ci::JobArtifacts::CreateService do end expect(subject[:status]).to eq(:success) - expect(job.job_variables.as_json).to contain_exactly( + expect(job.job_variables.as_json(only: [:key, :value, :source])).to contain_exactly( hash_including('key' => 'KEY1', 'value' => 'VAR1', 'source' => 'dotenv'), hash_including('key' => 'KEY2', 'value' => 'VAR2', 'source' => 'dotenv')) end diff --git a/spec/services/ci/parse_dotenv_artifact_service_spec.rb b/spec/services/ci/parse_dotenv_artifact_service_spec.rb index 6bf22b7c8b2..aaab849cd93 100644 --- a/spec/services/ci/parse_dotenv_artifact_service_spec.rb +++ b/spec/services/ci/parse_dotenv_artifact_service_spec.rb @@ -18,7 +18,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do it 'parses the artifact' do expect(subject[:status]).to eq(:success) - expect(build.job_variables.as_json).to contain_exactly( + expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly( hash_including('key' => 'KEY1', 'value' => 'VAR1'), hash_including('key' => 'KEY2', 'value' => 'VAR2')) end @@ -57,7 +57,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do expect(subject[:status]).to eq(:success) - expect(build.job_variables.as_json).to contain_exactly( + expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly( hash_including('key' => 'KEY1', 'value' => 'VAR4'), hash_including('key' => 'KEY2', 'value' => 'VAR3')) end @@ -101,7 +101,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do it 'trims the trailing space' do subject - expect(build.job_variables.as_json).to contain_exactly( + expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly( hash_including('key' => 'KEY1', 'value' => 'VAR1')) end end @@ -112,7 +112,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do it 'parses the dotenv data' do subject - expect(build.job_variables.as_json).to contain_exactly( + expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly( hash_including('key' => 'KEY', 'value' => 'VARCONTAINING=EQLS')) end end @@ -133,7 +133,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do it 'parses the dotenv data' do subject - expect(build.job_variables.as_json).to contain_exactly( + expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly( hash_including('key' => 'skateboard', 'value' => '🛹')) end end @@ -154,7 +154,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do it 'parses the dotenv data' do subject - expect(build.job_variables.as_json).to contain_exactly( + expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly( hash_including('key' => 'KEY1', 'value' => 'V A R 1')) end end @@ -165,7 +165,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do it 'parses the value as-is' do subject - expect(build.job_variables.as_json).to contain_exactly( + expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly( hash_including('key' => 'KEY1', 'value' => '"VAR1"')) end end @@ -176,7 +176,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do it 'parses the value as-is' do subject - expect(build.job_variables.as_json).to contain_exactly( + expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly( hash_including('key' => 'KEY1', 'value' => "'VAR1'")) end end @@ -187,7 +187,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do it 'parses the value as-is' do subject - expect(build.job_variables.as_json).to contain_exactly( + expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly( hash_including('key' => 'KEY1', 'value' => '" VAR1 "')) end end @@ -208,7 +208,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do it 'parses the dotenv data' do subject - expect(build.job_variables.as_json).to contain_exactly( + expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly( hash_including('key' => 'KEY1', 'value' => '')) end end @@ -250,7 +250,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do it 'does not support variable expansion in dotenv parser' do subject - expect(build.job_variables.as_json).to contain_exactly( + expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly( hash_including('key' => 'KEY1', 'value' => 'VAR1'), hash_including('key' => 'KEY2', 'value' => '${KEY1}_Test')) end @@ -284,7 +284,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do it 'does not support comment in dotenv parser' do subject - expect(build.job_variables.as_json).to contain_exactly( + expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly( hash_including('key' => 'KEY1', 'value' => 'VAR1 # This is variable')) end end diff --git a/tooling/graphql/docs/helper.rb b/tooling/graphql/docs/helper.rb index ca153c806c4..e4f14129f3b 100644 --- a/tooling/graphql/docs/helper.rb +++ b/tooling/graphql/docs/helper.rb @@ -57,7 +57,7 @@ module Tooling --- stage: Ecosystem group: Integrations - info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers + info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- <!--- |