Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-05-15 09:12:59 +00:00
parent 6c50700233
commit 800c3eceda
93 changed files with 714 additions and 691 deletions

View File

@ -2339,12 +2339,14 @@ Gitlab/BoundedContexts:
- 'ee/app/graphql/resolvers/security_orchestration/policy_violations_resolver.rb'
- 'ee/app/graphql/resolvers/security_orchestration/scan_execution_policy_resolver.rb'
- 'ee/app/graphql/resolvers/security_orchestration/scan_result_policy_resolver.rb'
- 'ee/app/graphql/types/security_orchestration/scan_execution_policy_attributes_type.rb'
- 'ee/app/graphql/resolvers/security_orchestration/security_policy_resolver.rb'
- 'ee/app/graphql/types/security_orchestration/approval_policy_attributes_type.rb'
- 'ee/app/graphql/types/security_orchestration/scan_execution_policy_attributes_type.rb'
- 'ee/app/graphql/types/security_orchestration/policy_attributes_union.rb'
- 'ee/app/graphql/types/security_orchestration/pipeline_execution_scheduled_policy_attributes_type.rb'
- 'ee/app/graphql/types/security_orchestration/pipeline_execution_policy_attributes_type.rb'
- 'ee/app/graphql/types/security_orchestration/pipeline_execution_policy_scheduled_fields_type.rb'
- 'ee/app/graphql/types/security/vulnerability_management_policy_attributes_type.rb'
- 'ee/app/graphql/types/security_orchestration/security_policy_type.rb'
- 'ee/app/graphql/resolvers/security_orchestration/security_policy_project_suggestions_resolver.rb'
- 'ee/app/graphql/resolvers/security_report/finding_reports_comparer_resolver.rb'
- 'ee/app/graphql/resolvers/security_report/finding_resolver.rb'

View File

@ -1003,7 +1003,6 @@ RSpec/BeEq:
- 'spec/models/anti_abuse/reports/note_spec.rb'
- 'spec/models/anti_abuse/user_trust_score_spec.rb'
- 'spec/models/appearance_spec.rb'
- 'spec/models/attr_encrypted_patches_spec.rb'
- 'spec/models/authentication_event_spec.rb'
- 'spec/models/batched_git_ref_updates/deletion_spec.rb'
- 'spec/models/blob_spec.rb'

View File

@ -1 +1 @@
c2df26faa831df5935081aad4833580a791d0002
da07ea6b72e37e8d07020e7be6cd4a378ddc442a

View File

@ -19,10 +19,6 @@ extend ignore_feature_category
gem 'bundler-checksum', '~> 0.1.0', path: 'vendor/gems/bundler-checksum', require: false, feature_category: :shared
# NOTE: When incrementing the major or minor version here, also increment activerecord_version
# in vendor/gems/attr_encrypted/attr_encrypted.gemspec until we resolve
# https://gitlab.com/gitlab-org/gitlab/-/issues/375713
#
# See https://docs.gitlab.com/ee/development/gemfile.html#upgrade-rails for guidelines when upgrading Rails
gem 'rails', '~> 7.1.5.1', feature_category: :shared
@ -128,7 +124,7 @@ gem 'invisible_captcha', '~> 2.1.0', feature_category: :insider_threat
gem 'devise-two-factor', '~> 4.1.1', feature_category: :system_access
gem 'rqrcode', '~> 2.2', feature_category: :system_access
gem 'attr_encrypted', '~> 3.2.4', path: 'vendor/gems/attr_encrypted', feature_category: :shared
gem 'attr_encrypted', '~> 4.2', feature_category: :shared
# GitLab Pages
gem 'validates_hostname', '~> 1.0.13', feature_category: :pages

View File

@ -30,6 +30,7 @@
{"name":"ast","version":"2.4.2","platform":"ruby","checksum":"1e280232e6a33754cde542bc5ef85520b74db2aac73ec14acef453784447cc12"},
{"name":"async","version":"2.23.1","platform":"ruby","checksum":"612c97346948a5dbfb6b4aef12976416b01aef48ec2d41677efb25c8c32a5006"},
{"name":"atlassian-jwt","version":"0.2.1","platform":"ruby","checksum":"2fd2d87418773f2e140c038cb22e049069708aff2bd0a423a7e1740574e97823"},
{"name":"attr_encrypted","version":"4.2.0","platform":"ruby","checksum":"7e5c80159e6e38ed40dc4e2e65c4f57234fe1f376bddc40c8b773bfb9b81ad51"},
{"name":"attr_required","version":"1.0.2","platform":"ruby","checksum":"f0ebfc56b35e874f4d0ae799066dbc1f81efefe2364ca3803dc9ea6a4de6cb99"},
{"name":"awesome_print","version":"1.9.2","platform":"ruby","checksum":"e99b32b704acff16d768b3468680793ced40bfdc4537eb07e06a4be11133786e"},
{"name":"awrence","version":"1.2.1","platform":"ruby","checksum":"dd1d214c12a91f449d1ef81d7ee3babc2816944e450752e7522c65521872483e"},

View File

@ -126,12 +126,6 @@ PATH
diffy (~> 3.4)
oj (~> 3.16, >= 3.16.10)
PATH
remote: vendor/gems/attr_encrypted
specs:
attr_encrypted (3.2.4)
encryptor (~> 3.0.0)
PATH
remote: vendor/gems/bundler-checksum
specs:
@ -352,6 +346,8 @@ GEM
traces (~> 0.15)
atlassian-jwt (0.2.1)
jwt (~> 2.1)
attr_encrypted (4.2.0)
encryptor (~> 3.0.0)
attr_required (1.0.2)
awesome_print (1.9.2)
awrence (1.2.1)
@ -2079,7 +2075,7 @@ DEPENDENCIES
asciidoctor-plantuml (~> 0.0.16)
async (~> 2.23.0)
atlassian-jwt (~> 0.2.1)
attr_encrypted (~> 3.2.4)!
attr_encrypted (~> 4.2)
awesome_print
aws-sdk-cloudformation (~> 1)
aws-sdk-core (~> 3.223.0)

View File

@ -30,6 +30,7 @@
{"name":"ast","version":"2.4.2","platform":"ruby","checksum":"1e280232e6a33754cde542bc5ef85520b74db2aac73ec14acef453784447cc12"},
{"name":"async","version":"2.23.1","platform":"ruby","checksum":"612c97346948a5dbfb6b4aef12976416b01aef48ec2d41677efb25c8c32a5006"},
{"name":"atlassian-jwt","version":"0.2.1","platform":"ruby","checksum":"2fd2d87418773f2e140c038cb22e049069708aff2bd0a423a7e1740574e97823"},
{"name":"attr_encrypted","version":"4.2.0","platform":"ruby","checksum":"7e5c80159e6e38ed40dc4e2e65c4f57234fe1f376bddc40c8b773bfb9b81ad51"},
{"name":"attr_required","version":"1.0.2","platform":"ruby","checksum":"f0ebfc56b35e874f4d0ae799066dbc1f81efefe2364ca3803dc9ea6a4de6cb99"},
{"name":"awesome_print","version":"1.9.2","platform":"ruby","checksum":"e99b32b704acff16d768b3468680793ced40bfdc4537eb07e06a4be11133786e"},
{"name":"awrence","version":"1.2.1","platform":"ruby","checksum":"dd1d214c12a91f449d1ef81d7ee3babc2816944e450752e7522c65521872483e"},

View File

@ -126,12 +126,6 @@ PATH
diffy (~> 3.4)
oj (~> 3.16, >= 3.16.10)
PATH
remote: vendor/gems/attr_encrypted
specs:
attr_encrypted (3.2.4)
encryptor (~> 3.0.0)
PATH
remote: vendor/gems/bundler-checksum
specs:
@ -352,6 +346,8 @@ GEM
traces (~> 0.15)
atlassian-jwt (0.2.1)
jwt (~> 2.1)
attr_encrypted (4.2.0)
encryptor (~> 3.0.0)
attr_required (1.0.2)
awesome_print (1.9.2)
awrence (1.2.1)
@ -2079,7 +2075,7 @@ DEPENDENCIES
asciidoctor-plantuml (~> 0.0.16)
async (~> 2.23.0)
atlassian-jwt (~> 0.2.1)
attr_encrypted (~> 3.2.4)!
attr_encrypted (~> 4.2)
awesome_print
aws-sdk-cloudformation (~> 1)
aws-sdk-core (~> 3.223.0)

View File

@ -172,6 +172,7 @@
"PipelineExecutionSchedulePolicy",
"ScanExecutionPolicy",
"ScanResultPolicy",
"SecurityPolicyType",
"VulnerabilityManagementPolicy"
],
"PackageFileMetadata": [
@ -194,6 +195,13 @@
"Pipeline",
"PipelineMinimalAccess"
],
"PolicyAttributesUnion": [
"ApprovalPolicyAttributesType",
"PipelineExecutionPolicyAttributesType",
"PipelineExecutionScheduledPolicyAttributesType",
"ScanExecutionPolicyAttributesType",
"VulnerabilityManagementPolicyAttributesType"
],
"ProjectInterface": [
"Project",
"ProjectMinimalAccess"

View File

@ -23,13 +23,12 @@ import { serializeParams, update2WeekFromNow, updateUrlWithQueryParams } from '.
*
* @param {Object} options
* @param {string} options.url
* @param {string|number} options.id
* @param {Object<string, string|number>} options.params
* @param {string} options.sort
*/
const fetchTokens = async ({ url, id, params, sort }) => {
const fetchTokens = async ({ url, params, sort }) => {
const { data, headers } = await axios.get(url, {
params: { user_id: id, sort, ...params },
params: { sort, ...params },
});
const { perPage, total } = parseIntPagination(normalizeHeaders(headers));
@ -105,7 +104,6 @@ export const useAccessTokens = defineStore('accessTokens', {
const url = Api.buildUrl(this.urlShow.replace(':id', this.id));
const { total } = await fetchTokens({
url,
id: this.id,
params,
sort: this.sort,
});
@ -136,7 +134,6 @@ export const useAccessTokens = defineStore('accessTokens', {
updateUrlWithQueryParams({ params: this.params, sort: this.sort });
const { data, perPage, total } = await fetchTokens({
url,
id: this.id,
params: this.params,
sort: this.sort,
});

View File

@ -80,7 +80,7 @@ export default {
},
moreChildrenLinkText() {
return n__(
'View all (one more item)',
'View all (%d more item)',
'View all (%d more items)',
this.item.childrenCount - this.item.children.length,
);

View File

@ -37,8 +37,9 @@ export default {
required: true,
},
discussion: {
type: Array,
type: Object,
required: true,
default: () => ({}),
},
sortOrder: {
type: String,
@ -110,23 +111,26 @@ export default {
};
},
computed: {
note() {
return this.discussion[0];
notes() {
return this.discussion.notes.nodes;
},
noteId() {
return getIdFromGraphQLId(this.note.id);
firstNote() {
return this.notes[0];
},
firstNoteId() {
return getIdFromGraphQLId(this.firstNote.id);
},
hasReplies() {
return Boolean(this.replies?.length);
},
replies() {
if (this.discussion?.length > 1) {
return this.discussion.slice(1);
if (this.notes?.length > 1) {
return this.notes.slice(1);
}
return null;
},
discussionId() {
return this.firstComment?.id || '';
return this.discussion.id || '';
},
shouldShowReplyForm() {
return this.showForm || this.hasReplies;
@ -134,20 +138,17 @@ export default {
isOnlyCommentOfAThread() {
return !this.hasReplies && !this.showForm;
},
firstComment() {
return this.discussion[0]?.discussion;
},
isDiscussionResolved() {
return this.firstComment?.resolved;
return this.discussion.resolved;
},
isDiscussionResolvable() {
return this.firstComment?.resolvable && this.note?.userPermissions?.resolveNote;
return this.discussion.resolvable && this.firstNote?.userPermissions?.resolveNote;
},
},
watch: {
discussion: {
handler(newDiscussion) {
if (newDiscussion[0].discussion.resolved === false) {
if (newDiscussion.resolved === false) {
this.isExpanded = true;
}
},
@ -163,7 +164,7 @@ export default {
methods: {
handleQuoteReply({ event, discussionId, text }) {
// Ensure we're using correct discussion block for reply form.
if (this.note.discussion.id === discussionId) {
if (this.discussion.id === discussionId) {
// Prevent 'r' being written.
if (event && typeof event.preventDefault === 'function') {
event.preventDefault();
@ -214,21 +215,11 @@ export default {
__typename: 'UserCore',
};
}
const toggledDiscussionNotes = [...this.discussion].map((note) => {
return {
...note,
discussion: {
...note.discussion,
resolved,
resolvedBy,
},
};
});
return {
id: this.discussionId,
notes: {
nodes: [...toggledDiscussionNotes],
},
...this.discussion,
resolved,
resolvedBy,
};
},
async resolveDiscussion() {
@ -259,7 +250,7 @@ export default {
<work-item-note
v-if="isOnlyCommentOfAThread"
:is-first-note="true"
:note="note"
:note="firstNote"
:discussion-id="discussionId"
:full-path="fullPath"
:has-replies="hasReplies"
@ -276,19 +267,20 @@ export default {
:work-item-id="workItemId"
:work-item-iid="workItemIid"
:is-resolving="isResolving"
:resolved-by="discussion.resolvedBy"
:hide-fullscreen-markdown-button="hideFullscreenMarkdownButton"
:uploads-path="uploadsPath"
@startEditing="$emit('startEditing')"
@resolve="resolveDiscussion"
@startReplying="showReplyForm"
@deleteNote="$emit('deleteNote', note)"
@reportAbuse="$emit('reportAbuse', note)"
@deleteNote="$emit('deleteNote', firstNote)"
@reportAbuse="$emit('reportAbuse', firstNote)"
@cancelEditing="$emit('cancelEditing')"
@error="$emit('error', $event)"
/>
<timeline-entry-item
v-else
:data-note-id="noteId"
:data-note-id="firstNoteId"
:data-discussion-id="discussionId"
class="note note-discussion gl-px-0"
>
@ -300,7 +292,7 @@ export default {
<ul class="notes" data-testid="note-container">
<work-item-note
is-first-note
:note="note"
:note="firstNote"
:discussion-id="discussionId"
:full-path="fullPath"
:has-replies="hasReplies"
@ -317,12 +309,13 @@ export default {
:is-discussion-resolved="isDiscussionResolved"
:is-discussion-resolvable="isDiscussionResolvable"
:is-resolving="isResolving"
:resolved-by="discussion.resolvedBy"
:hide-fullscreen-markdown-button="hideFullscreenMarkdownButton"
:uploads-path="uploadsPath"
@startReplying="showReplyForm"
@startEditing="$emit('startEditing')"
@deleteNote="$emit('deleteNote', note)"
@reportAbuse="$emit('reportAbuse', note)"
@deleteNote="$emit('deleteNote', firstNote)"
@reportAbuse="$emit('reportAbuse', firstNote)"
@cancelEditing="$emit('cancelEditing')"
@resolve="resolveDiscussion"
@error="$emit('error', $event)"
@ -335,37 +328,37 @@ export default {
@toggle="toggleDiscussion({ discussionId })"
/>
<template v-if="isExpanded">
<template v-for="reply in replies">
<work-item-note
:key="threadKey(reply)"
:discussion-id="discussionId"
:full-path="fullPath"
:note="reply"
:work-item-type="workItemType"
:is-modal="isModal"
:autocomplete-data-sources="autocompleteDataSources"
:markdown-preview-path="markdownPreviewPath"
:new-comment-template-paths="newCommentTemplatePaths"
:assignees="assignees"
:work-item-id="workItemId"
:work-item-iid="workItemIid"
:can-set-work-item-metadata="canSetWorkItemMetadata"
:is-discussion-resolved="isDiscussionResolved"
:is-discussion-resolvable="isDiscussionResolvable"
:is-resolving="isResolving"
:hide-fullscreen-markdown-button="hideFullscreenMarkdownButton"
:uploads-path="uploadsPath"
@startReplying="showReplyForm"
@deleteNote="$emit('deleteNote', reply)"
@reportAbuse="$emit('reportAbuse', reply)"
@startEditing="$emit('startEditing')"
@cancelEditing="$emit('cancelEditing')"
@error="$emit('error', $event)"
/>
</template>
<work-item-note
v-for="reply in replies"
:key="threadKey(reply)"
:discussion-id="discussionId"
:full-path="fullPath"
:note="reply"
:work-item-type="workItemType"
:is-modal="isModal"
:autocomplete-data-sources="autocompleteDataSources"
:markdown-preview-path="markdownPreviewPath"
:new-comment-template-paths="newCommentTemplatePaths"
:assignees="assignees"
:work-item-id="workItemId"
:work-item-iid="workItemIid"
:can-set-work-item-metadata="canSetWorkItemMetadata"
:is-discussion-resolved="isDiscussionResolved"
:is-discussion-resolvable="isDiscussionResolvable"
:is-resolving="isResolving"
:resolved-by="discussion.resolvedBy"
:hide-fullscreen-markdown-button="hideFullscreenMarkdownButton"
:uploads-path="uploadsPath"
@startReplying="showReplyForm"
@deleteNote="$emit('deleteNote', reply)"
@reportAbuse="$emit('reportAbuse', reply)"
@startEditing="$emit('startEditing')"
@cancelEditing="$emit('cancelEditing')"
@error="$emit('error', $event)"
/>
<work-item-note-replying
v-if="isReplying"
:is-internal-note="note.internal"
:is-internal-note="firstNote.internal"
:body="replyingText"
/>
<work-item-add-note
@ -384,7 +377,7 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:new-comment-template-paths="newCommentTemplatePaths"
:is-discussion-locked="isDiscussionLocked"
:is-internal-thread="note.internal"
:is-internal-thread="firstNote.internal"
:is-work-item-confidential="isWorkItemConfidential"
:is-discussion-resolved="isDiscussionResolved"
:is-discussion-resolvable="isDiscussionResolvable"

View File

@ -112,6 +112,11 @@ export default {
required: false,
default: false,
},
resolvedBy: {
type: Object,
required: false,
default: () => ({}),
},
hideFullscreenMarkdownButton: {
type: Boolean,
required: false,
@ -213,9 +218,6 @@ export default {
isWorkItemConfidential() {
return this.workItem.confidential;
},
discussionResolvedBy() {
return this.note.discussion.resolvedBy;
},
},
mounted() {
gfmEventHub.$on('edit-note', this.handleEditNote);
@ -413,7 +415,7 @@ export default {
:can-resolve="canResolve"
:is-resolved="isDiscussionResolved"
:is-resolving="isResolving"
:resolved-by="discussionResolvedBy"
:resolved-by="resolvedBy"
@startReplying="showReplyForm"
@startEditing="startEditing"
@resolve="$emit('resolve')"

View File

@ -463,7 +463,7 @@ export default {
},
getDiscussionKey(discussion) {
// discussion key is important like this since after first comment changes
const discussionId = discussion.notes.nodes[0].id;
const discussionId = discussion.id;
return discussionId.split('/')[discussionId.split('/').length - 1];
},
isSystemNote(note) {
@ -488,10 +488,7 @@ export default {
);
},
isDiscussionExpandedOnLoad(discussion) {
return !this.isDiscussionResolved(discussion) || this.isHashTargeted(discussion);
},
isDiscussionResolved(discussion) {
return discussion.notes.nodes[0]?.discussion?.resolved;
return !discussion.resolved || this.isHashTargeted(discussion);
},
async fetchMoreNotes() {
this.isLoadingMore = true;
@ -508,14 +505,14 @@ export default {
showDeleteNoteModal(note, discussion) {
const isLastNote = discussion.notes.nodes.length === 1;
this.$refs.deleteNoteModal.show();
this.noteToDelete = { ...note, isLastNote };
this.noteToDelete = { ...note, isLastNote, discussionId: discussion.id };
},
cancelDeletingNote() {
this.noteToDelete = null;
},
async deleteNote() {
try {
const { id, isLastNote, discussion } = this.noteToDelete;
const { id, isLastNote, discussionId } = this.noteToDelete;
await this.$apollo.mutate({
mutation: deleteNoteMutation,
variables: {
@ -525,7 +522,7 @@ export default {
},
update(cache) {
const deletedObject = isLastNote
? { __typename: TYPENAME_DISCUSSION, id: discussion.id }
? { __typename: TYPENAME_DISCUSSION, id: discussionId }
: { __typename: TYPENAME_NOTE, id };
cache.modify({
id: cache.identify(deletedObject),
@ -590,7 +587,7 @@ export default {
<work-item-discussion
:key="getDiscussionKey(discussion)"
ref="workItemDiscussion"
:discussion="discussion.notes.nodes"
:discussion="discussion"
:full-path="fullPath"
:work-item-id="workItemId"
:work-item-iid="workItemIid"

View File

@ -147,6 +147,7 @@ export const WORK_ITEM_TO_ISSUABLE_MAP = {
[WIDGET_TYPE_AWARD_EMOJI]: 'awardEmoji',
[WIDGET_TYPE_TIME_TRACKING]: 'timeEstimate',
[WIDGET_TYPE_COLOR]: 'color',
[WIDGET_TYPE_STATUS]: 'status',
};
export const LINKED_CATEGORIES_MAP = {

View File

@ -6,6 +6,12 @@ mutation createWorkItemNote($input: CreateNoteInput!) {
id
discussion {
id
resolved
resolvable
resolvedBy {
id
name
}
notes {
nodes {
...WorkItemNote

View File

@ -4,6 +4,12 @@ mutation toggleWorkItemNoteResolveDiscussion($id: DiscussionID!, $resolve: Boole
discussionToggleResolve(input: { id: $id, resolve: $resolve }) {
discussion {
id
resolved
resolvable
resolvedBy {
id
name
}
notes {
nodes {
...WorkItemNote

View File

@ -16,6 +16,12 @@ fragment WorkItemDiscussionNote on Note {
}
discussion {
id
resolved
resolvable
resolvedBy {
id
name
}
notes {
nodes {
...WorkItemNote

View File

@ -19,15 +19,6 @@ fragment WorkItemNote on Note {
...User
webPath
}
discussion {
id
resolved
resolvable
resolvedBy {
id
name
}
}
author {
...User
}

View File

@ -20,6 +20,12 @@ query workItemNotesByIid($fullPath: ID!, $iid: String!, $after: String, $pageSiz
}
nodes {
id
resolved
resolvable
resolvedBy {
id
name
}
notes {
nodes {
...WorkItemNote

View File

@ -42,7 +42,7 @@ module AccessTokensHelper
create: user_settings_personal_access_tokens_path,
revoke: api_v4_personal_access_tokens_path,
rotate: api_v4_personal_access_tokens_path,
show: api_v4_personal_access_tokens_path
show: "#{api_v4_personal_access_tokens_path}?user_id=:id"
}
}
end

View File

@ -332,10 +332,10 @@ class ApplicationSetting < ApplicationRecord
allow_blank: true
jsonb_accessor :token_prefixes,
instance_token_prefix: [:string, { default: 'gl' }]
instance_token_prefix: [:string, { default: '' }]
validates :instance_token_prefix,
format: { with: %r{\A[a-zA-Z0-9-]+\z} },
format: { with: %r{\A[a-zA-Z0-9]+\z} },
length: { maximum: 20, message: N_('is too long (maximum is %{count} characters)') },
allow_blank: true

View File

@ -159,7 +159,7 @@ module ApplicationSettingImplementation
password_authentication_enabled_for_web: Settings.gitlab['signin_enabled'],
performance_bar_allowed_group_id: nil,
personal_access_token_prefix: 'glpat-',
instance_token_prefix: 'gl',
instance_token_prefix: '',
plantuml_enabled: false,
plantuml_url: nil,
diagramsnet_enabled: true,

View File

@ -8,11 +8,11 @@ module Gitlab
def sensitive_attributes
attributes = []
if respond_to?(:attr_encrypted_attributes)
# Per https://github.com/attr-encrypted/attr_encrypted/blob/a96693e9a2a25f4f910bf915e29b0f364f277032/lib/attr_encrypted.rb#L413
attributes.concat attr_encrypted_attributes.keys
attributes.concat attr_encrypted_attributes.values.map { |v| v[:attribute] } # rubocop:disable Rails/Pluck -- Not a ActiveRecord object
attributes.concat attr_encrypted_attributes.values.map { |v| :"#{v[:attribute]}_iv" }
if respond_to?(:attr_encrypted_encrypted_attributes)
# Per https://github.com/attr-encrypted/attr_encrypted/blob/c2aa160c2327f2613fbca913e9fd20bce6e98880/lib/attr_encrypted.rb#L413
attributes.concat attr_encrypted_encrypted_attributes.keys
attributes.concat attr_encrypted_encrypted_attributes.values.map { |v| v[:attribute] } # rubocop:disable Rails/Pluck -- Not a ActiveRecord object
attributes.concat attr_encrypted_encrypted_attributes.values.map { |v| :"#{v[:attribute]}_iv" }
end
if respond_to?(:token_authenticatable_sensitive_fields)

View File

@ -672,10 +672,10 @@ module Integrations
def reencrypt_properties
unless properties.nil? || properties.empty?
attr_encrypted_attributes = self.class.attr_encrypted_attributes[:properties]
attr_encrypted_attributes = self.class.attr_encrypted_encrypted_attributes[:properties]
key = dynamic_encryption_key_for_operation(attr_encrypted_attributes[:key])
iv = generate_iv(attr_encrypted_attributes[:algorithm])
ep = self.class.attr_encrypt(:properties, properties, { key: key, iv: iv })
ep = self.class.attr_encrypted_encrypt(:properties, properties, { key: key, iv: iv })
end
{ 'encrypted_properties' => ep, 'encrypted_properties_iv' => iv }

View File

@ -170,20 +170,20 @@ module WebHooks
def decrypt_url_was
options = {
key: dynamic_encryption_key_for_operation(attr_encrypted_attributes[:url][:key]),
key: dynamic_encryption_key_for_operation(attr_encrypted_encrypted_attributes[:url][:key]),
iv: Base64.decode64(encrypted_url_iv_was)
}
self.class.attr_decrypt(:url, encrypted_url_was, options)
self.class.attr_encrypted_decrypt(:url, encrypted_url_was, options)
end
def url_variables_were
options = {
key: dynamic_encryption_key_for_operation(attr_encrypted_attributes[:url_variables][:key]),
key: dynamic_encryption_key_for_operation(attr_encrypted_encrypted_attributes[:url_variables][:key]),
iv: encrypted_url_variables_iv_was
}
self.class.attr_decrypt(:url_variables, encrypted_url_variables_was, options)
self.class.attr_encrypted_decrypt(:url_variables, encrypted_url_variables_was, options)
end
def initialize_url_variables

View File

@ -58,9 +58,7 @@ class DeployToken < ApplicationRecord
def self.prefix_for_deploy_token
return DEPLOY_TOKEN_PREFIX unless Feature.enabled?(:custom_prefix_for_all_token_types, :instance)
# Manually remove gl - we'll add this from the configuration.
# Once the feature flag has been removed, we can change DEPLOY_TOKEN_PREFIX to `ft-`
::Authn::TokenField::PrefixHelper.prepend_instance_prefix(DEPLOY_TOKEN_PREFIX.delete_prefix('gl'))
::Authn::TokenField::PrefixHelper.prepend_instance_prefix(DEPLOY_TOKEN_PREFIX)
end
def valid_for_dependency_proxy?

View File

@ -47,7 +47,7 @@ class GrafanaIntegration < ApplicationRecord
end
def token
attr_decrypt(:token, encrypted_token)
attr_encrypted_decrypt(:token, encrypted_token)
end
def check_token_changes

View File

@ -6,7 +6,6 @@ module Operations
DEFAULT_UNLEASH_API_VERSION = 1
FEATURE_FLAGS_CLIENT_TOKEN_PREFIX = 'glffct-'
FEATURE_FLAGS_CLIENT_TOKEN_PREFIX_PREFIX_WITHOUT_INSTANCE_PREFIX = 'ffct-'
self.table_name = 'operations_feature_flags_clients'
@ -35,13 +34,9 @@ module Operations
end
def self.prefix_for_feature_flags_client_token
if Feature.enabled?(:custom_prefix_for_all_token_types, :instance)
::Authn::TokenField::PrefixHelper.prepend_instance_prefix(
FEATURE_FLAGS_CLIENT_TOKEN_PREFIX_PREFIX_WITHOUT_INSTANCE_PREFIX
)
else
FEATURE_FLAGS_CLIENT_TOKEN_PREFIX
end
return FEATURE_FLAGS_CLIENT_TOKEN_PREFIX unless Feature.enabled?(:custom_prefix_for_all_token_types, :instance)
::Authn::TokenField::PrefixHelper.prepend_instance_prefix(FEATURE_FLAGS_CLIENT_TOKEN_PREFIX)
end
def unleash_api_version

View File

@ -3,18 +3,22 @@
class SentNotification < ApplicationRecord
include EachBatch
INVALID_NOTEABLE = Class.new(StandardError)
belongs_to :project
belongs_to :noteable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :recipient, class_name: "User"
belongs_to :issue_email_participant
belongs_to :namespace
validates :recipient, presence: true
validates :recipient, :namespace_id, presence: true
validates :reply_key, presence: true, uniqueness: true
validates :noteable_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
validates :commit_id, :project, presence: true, if: :for_commit?
validates :in_reply_to_discussion_id, format: { with: /\A\h{40}\z/, allow_nil: true }
validate :note_valid
before_validation :ensure_sharding_key
before_create :ensure_created_at
class << self
@ -109,6 +113,30 @@ class SentNotification < ApplicationRecord
self.created_at = Time.current
end
def ensure_sharding_key
self.namespace_id = namespace_id_from_noteable
end
def namespace_id_from_noteable
case noteable
when DesignManagement::Design, Issue
noteable.namespace_id
when MergeRequest, ProjectSnippet
noteable.project.project_namespace_id
when Commit
project&.project_namespace_id
when WikiPage::Meta
noteable.namespace_id || noteable.project.project_namespace_id
else
# Raising an error here to make sure that the correct sharding key is set if support
# for a new `noteable_type` is added.
raise(
INVALID_NOTEABLE,
_("%{noteable_type} is not supported") % { noteable_type: noteable_type }
)
end
end
def reply_params
{
noteable_type: self.noteable_type,
@ -129,3 +157,5 @@ class SentNotification < ApplicationRecord
end
end
end
SentNotification.prepend_mod

View File

@ -2954,9 +2954,7 @@ class User < ApplicationRecord
def self.prefix_for_feed_token
return FEED_TOKEN_PREFIX unless Feature.enabled?(:custom_prefix_for_all_token_types, :instance)
# Manually remove gl - we'll add this from the configuration.
# Once the feature flag has been removed, we can change FEED_TOKEN_PREFIX to `ft-`
::Authn::TokenField::PrefixHelper.prepend_instance_prefix(FEED_TOKEN_PREFIX.delete_prefix('gl'))
::Authn::TokenField::PrefixHelper.prepend_instance_prefix(FEED_TOKEN_PREFIX)
end
# method overridden in EE

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class UserPreference < ApplicationRecord
include SafelyChangeColumnDefault
# We could use enums, but Rails 4 doesn't support multiple
# enum options with same name for multiple fields, also it creates
# extra methods that aren't really needed here.
@ -10,6 +12,8 @@ class UserPreference < ApplicationRecord
belongs_to :user
belongs_to :home_organization, class_name: "Organizations::Organization", optional: true
columns_changing_default :organization_groups_projects_display
scope :with_user, -> { joins(:user) }
scope :gitpod_enabled, -> { where(gitpod_enabled: true) }

View File

@ -3,8 +3,8 @@ name: bbm_retry_sidekiq_shutdown_exception
description:
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366720
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/182060
rollout_issue_url:
milestone: '18.0'
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/542900
milestone: '18.1'
group: group::database frameworks
type: experiment
type: gitlab_com_derisk
default_enabled: false

View File

@ -8,23 +8,10 @@ class MigrateAnonymousSearchesFlagToApplicationSettings < Gitlab::Database::Migr
self.table_name = 'application_settings'
end
def up
ApplicationSetting.reset_column_information
# Marking this as a noop because the backfill migration incorrectly ignores the
# default_enabled configuration value, causing wrong feature flag value to be added in setting.
# Revert MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/190832
def up; end
application_setting = ApplicationSetting.last
return unless application_setting
search_settings = application_setting.search
search_settings[:anonymous_searches_allowed] = feature_flag_enabled?('allow_anonymous_searches')
application_setting.update_columns(search: search_settings, updated_at: Time.current)
end
def down
application_setting = ApplicationSetting.last
return unless application_setting
search_settings_hash = application_setting.search
search_settings_hash.delete('anonymous_searches_allowed')
application_setting.update_column(:search, search_settings_hash)
end
def down; end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class ChangeUserPreferenceOrganizationGroupsProjectsDisplayDefault < Gitlab::Database::Migration[2.3]
milestone '18.0'
def up
change_column_default :user_preferences, :organization_groups_projects_display, from: 0, to: 1
end
def down
change_column_default :user_preferences, :organization_groups_projects_display, from: 1, to: 0
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class RemoveDefaultFromSentNotificationsNamespaceId < Gitlab::Database::Migration[2.3]
milestone '18.1'
def change
change_column_default :sent_notifications, :namespace_id, from: 0, to: nil
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class ScheduleIndexForPCiPipelineVariablesOnProjectId < Gitlab::Database::Migration[2.3]
include Gitlab::Database::PartitioningMigrationHelpers
milestone '18.1'
TABLE_NAME = :p_ci_pipeline_variables
INDEX_NAME = :index_p_ci_pipeline_variables_on_project_id
COLUMN_NAME = :project_id
def up
prepare_partitioned_async_index(TABLE_NAME, COLUMN_NAME, name: INDEX_NAME)
end
def down
unprepare_partitioned_async_index(TABLE_NAME, COLUMN_NAME, name: INDEX_NAME)
end
end

View File

@ -0,0 +1 @@
bfe0c221edcbb48e5c1511ed9dcad20b9753c7db6db389f8cc8b15ec4f2b006a

View File

@ -0,0 +1 @@
756b664964be9ee347f2fc909051dc5fcc6f3097fc6600dbd128cb6b90099818

View File

@ -0,0 +1 @@
c98f8f3735452917b50c379ce0b4b011538536927f903242944f7876fc0f1454

View File

@ -22771,7 +22771,7 @@ CREATE TABLE sent_notifications (
id bigint NOT NULL,
issue_email_participant_id bigint,
created_at timestamp with time zone NOT NULL,
namespace_id bigint DEFAULT 0 NOT NULL
namespace_id bigint NOT NULL
);
CREATE SEQUENCE sent_notifications_id_seq
@ -24277,7 +24277,7 @@ CREATE TABLE user_preferences (
early_access_program_tracking boolean DEFAULT false NOT NULL,
extensions_marketplace_opt_in_status smallint DEFAULT 0 NOT NULL,
organization_groups_projects_sort text,
organization_groups_projects_display smallint DEFAULT 0 NOT NULL,
organization_groups_projects_display smallint DEFAULT 1 NOT NULL,
dpop_enabled boolean DEFAULT false NOT NULL,
use_work_items_view boolean DEFAULT false NOT NULL,
text_editor_type smallint DEFAULT 0 NOT NULL,

View File

@ -26,7 +26,7 @@ Cells 1.0 is in development. For more information about the state of cell develo
{{< /alert >}}
This page explains how to configure the GitLab Rails console for cell functionality. For more information on the proposed design and terminology, see the design document for [Cells](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/cells/).
To test cell functionality, configure the GitLab Rails console.
## Configuration

View File

@ -21,8 +21,8 @@ these definitions yet.
These are the defined terms to describe all aspects of Geo. Using a set of clearly
defined terms helps us to communicate efficiently and avoids confusion. The language
on this page aims to be [ubiquitous](https://handbook.gitlab.com/handbook/communication/#ubiquitous-language)
and [as simple as possible](https://handbook.gitlab.com/handbook/communication/#simple-language).
on this page aims to be ubiquitous
and as simple as possible.
## Main terms

View File

@ -42,7 +42,6 @@ from [owasp.org](https://owasp.org/).
private projects. Geo replicates them all indiscriminately. "Selective sync"
exists for files and repositories (but not database content), which would permit
only less-sensitive projects to be replicated to a **secondary** site if desired.
- See also: [GitLab data classification policy](https://handbook.gitlab.com/handbook/security/data-classification-standard/).
### What data backup and retention requirements have been defined for the application?

View File

@ -371,8 +371,7 @@ This feature is available for testing, but not ready for production use.
{{< /alert >}}
You can set a custom prefix for all tokens generated on your instance.
By default, GitLab uses `gl` as the instance prefix.
You can set a custom prefix that is prepended to all tokens generated on your instance.
Custom token prefixes apply only to the following tokens:

View File

@ -19162,6 +19162,29 @@ The edge type for [`SecretPermission`](#secretpermission).
| <a id="secretpermissionedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="secretpermissionedgenode"></a>`node` | [`SecretPermission`](#secretpermission) | The item at the end of the edge. |
#### `SecurityPolicyTypeConnection`
The connection type for [`SecurityPolicyType`](#securitypolicytype).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="securitypolicytypeconnectionedges"></a>`edges` | [`[SecurityPolicyTypeEdge]`](#securitypolicytypeedge) | A list of edges. |
| <a id="securitypolicytypeconnectionnodes"></a>`nodes` | [`[SecurityPolicyType]`](#securitypolicytype) | A list of nodes. |
| <a id="securitypolicytypeconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `SecurityPolicyTypeEdge`
The edge type for [`SecurityPolicyType`](#securitypolicytype).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="securitypolicytypeedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="securitypolicytypeedgenode"></a>`node` | [`SecurityPolicyType`](#securitypolicytype) | The item at the end of the edge. |
#### `SentryErrorConnection`
The connection type for [`SentryError`](#sentryerror).
@ -21487,6 +21510,22 @@ Represents the approval policy.
| <a id="approvalpolicyuserapprovers"></a>`userApprovers` | [`[UserCore!]`](#usercore) | Approvers of the user type. |
| <a id="approvalpolicyyaml"></a>`yaml` | [`String!`](#string) | YAML definition of the policy. |
### `ApprovalPolicyAttributesType`
Represents policy fields related to the approval policy.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="approvalpolicyattributestypeactionapprovers"></a>`actionApprovers` | [`[PolicyApproversType!]`](#policyapproverstype) | Multiple approvers action. |
| <a id="approvalpolicyattributestypeallgroupapprovers"></a>`allGroupApprovers` | [`[PolicyApprovalGroup!]`](#policyapprovalgroup) | All potential approvers of the group type, including groups inaccessible to the user. |
| <a id="approvalpolicyattributestypecustomroles"></a>`customRoles` | [`[MemberRole!]`](#memberrole) | Approvers of the custom role type. Users belonging to these role(s) alone will be approvers. |
| <a id="approvalpolicyattributestypedeprecatedproperties"></a>`deprecatedProperties` {{< icon name="warning-solid" >}} | [`[String!]`](#string) | **Introduced** in GitLab 16.10. **Status**: Experiment. All deprecated properties in the policy. |
| <a id="approvalpolicyattributestyperoleapprovers"></a>`roleApprovers` | [`[MemberAccessLevelName!]`](#memberaccesslevelname) | Approvers of the role type. Users belonging to these role(s) alone will be approvers. |
| <a id="approvalpolicyattributestypesource"></a>`source` | [`SecurityPolicySource!`](#securitypolicysource) | Source of the policy. Its fields depend on the source type. |
| <a id="approvalpolicyattributestypeuserapprovers"></a>`userApprovers` | [`[UserCore!]`](#usercore) | Approvers of the user type. |
### `ApprovalProjectRule`
Describes a project approval rule regarding who can approve merge requests.
@ -29085,6 +29124,28 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="groupscanresultpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="groupscanresultpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. Default is DIRECT. |
##### `Group.securityPolicies`
List of security policies configured for the namespace.
{{< details >}}
**Introduced** in GitLab 18.1.
**Status**: Experiment.
{{< /details >}}
Returns [`SecurityPolicyTypeConnection`](#securitypolicytypeconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#pagination-arguments):
`before: String`, `after: String`, `first: Int`, and `last: Int`.
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="groupsecuritypoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="groupsecuritypoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
##### `Group.securityPolicyProjectSuggestions`
Security policy project suggestions.
@ -33529,6 +33590,28 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="namespacescanresultpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="namespacescanresultpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. Default is DIRECT. |
##### `Namespace.securityPolicies`
List of security policies configured for the namespace.
{{< details >}}
**Introduced** in GitLab 18.1.
**Status**: Experiment.
{{< /details >}}
Returns [`SecurityPolicyTypeConnection`](#securitypolicytypeconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#pagination-arguments):
`before: String`, `after: String`, `first: Int`, and `last: Int`.
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="namespacesecuritypoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="namespacesecuritypoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
##### `Namespace.vulnerabilityManagementPolicies`
Vulnerability Management Policies of the project.
@ -34766,6 +34849,18 @@ Represents the pipeline execution policy.
| <a id="pipelineexecutionpolicywarnings"></a>`warnings` | [`[String!]!`](#string) | Warnings associated with the policy. |
| <a id="pipelineexecutionpolicyyaml"></a>`yaml` | [`String!`](#string) | YAML definition of the policy. |
### `PipelineExecutionPolicyAttributesType`
Represents policy fields related to the pipeline execution policy.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="pipelineexecutionpolicyattributestypepolicyblobfilepath"></a>`policyBlobFilePath` | [`String!`](#string) | Path to the policy file in the project. |
| <a id="pipelineexecutionpolicyattributestypesource"></a>`source` | [`SecurityPolicySource!`](#securitypolicysource) | Source of the policy. Its fields depend on the source type. |
| <a id="pipelineexecutionpolicyattributestypewarnings"></a>`warnings` | [`[String!]!`](#string) | Warnings associated with the policy. |
### `PipelineExecutionSchedulePolicy`
Represents the pipeline execution schedule policy.
@ -34785,6 +34880,18 @@ Represents the pipeline execution schedule policy.
| <a id="pipelineexecutionschedulepolicywarnings"></a>`warnings` | [`[String!]!`](#string) | Warnings associated with the policy. |
| <a id="pipelineexecutionschedulepolicyyaml"></a>`yaml` | [`String!`](#string) | YAML definition of the policy. |
### `PipelineExecutionScheduledPolicyAttributesType`
Represents policy fields related to the pipeline execution scheduled policy.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="pipelineexecutionscheduledpolicyattributestypepolicyblobfilepath"></a>`policyBlobFilePath` | [`String!`](#string) | Path to the policy file in the project. |
| <a id="pipelineexecutionscheduledpolicyattributestypesource"></a>`source` | [`SecurityPolicySource!`](#securitypolicysource) | Source of the policy. Its fields depend on the source type. |
| <a id="pipelineexecutionscheduledpolicyattributestypewarnings"></a>`warnings` | [`[String!]!`](#string) | Warnings associated with the policy. |
### `PipelineManualVariable`
CI/CD variables added to a manual pipeline.
@ -37056,6 +37163,28 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="projectsecurityexclusionsscanner"></a>`scanner` | [`ExclusionScannerEnum`](#exclusionscannerenum) | Filter entries by scanner. |
| <a id="projectsecurityexclusionstype"></a>`type` | [`ExclusionTypeEnum`](#exclusiontypeenum) | Filter entries by exclusion type. |
##### `Project.securityPolicies`
All security policies of the project.
{{< details >}}
**Introduced** in GitLab 18.1.
**Status**: Experiment.
{{< /details >}}
Returns [`SecurityPolicyTypeConnection`](#securitypolicytypeconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#pagination-arguments):
`before: String`, `after: String`, `first: Int`, and `last: Int`.
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="projectsecuritypoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="projectsecuritypoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
##### `Project.securityPolicyProjectLinkedGroups`
Groups linked to the project, when used as Security Policy Project.
@ -38721,6 +38850,17 @@ Represents the scan execution policy.
| <a id="scanexecutionpolicyupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the policy YAML was last updated. |
| <a id="scanexecutionpolicyyaml"></a>`yaml` | [`String!`](#string) | YAML definition of the policy. |
### `ScanExecutionPolicyAttributesType`
Represents policy fields related to the scan execution policy.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="scanexecutionpolicyattributestypedeprecatedproperties"></a>`deprecatedProperties` {{< icon name="warning-solid" >}} | [`[String!]`](#string) | **Introduced** in GitLab 17.3. **Status**: Experiment. All deprecated properties in the policy. |
| <a id="scanexecutionpolicyattributestypesource"></a>`source` | [`SecurityPolicySource!`](#securitypolicysource) | Source of the policy. Its fields depend on the source type. |
### `ScanResultPolicy`
Represents the scan result policy.
@ -38809,6 +38949,24 @@ Representation of a secrets permission.
| <a id="secretpermissionprincipal"></a>`principal` | [`Principal!`](#principal) | Who is provided access to. For eg: User/Role/MemberRole/Group. |
| <a id="secretpermissionproject"></a>`project` | [`Project!`](#project) | Project the secret permission belong to. |
### `SecurityPolicyType`
Represents the security policy.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="securitypolicytypedescription"></a>`description` | [`String!`](#string) | Description of the policy. |
| <a id="securitypolicytypeeditpath"></a>`editPath` | [`String!`](#string) | URL of policy edit page. |
| <a id="securitypolicytypeenabled"></a>`enabled` | [`Boolean!`](#boolean) | Indicates whether the policy is enabled. |
| <a id="securitypolicytypename"></a>`name` | [`String!`](#string) | Name of the policy. |
| <a id="securitypolicytypepolicyattributes"></a>`policyAttributes` | [`PolicyAttributesUnion!`](#policyattributesunion) | Attributes specific to the policy type. |
| <a id="securitypolicytypepolicyscope"></a>`policyScope` | [`PolicyScope`](#policyscope) | Scope of the policy. |
| <a id="securitypolicytypetype"></a>`type` | [`String`](#string) | Description of the policy type. |
| <a id="securitypolicytypeupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the policy YAML was last updated. |
| <a id="securitypolicytypeyaml"></a>`yaml` | [`String!`](#string) | YAML definition of the policy. |
### `SecurityPolicyValidationError`
Security policy validation error.
@ -41219,6 +41377,16 @@ Represents the vulnerability management policy.
| <a id="vulnerabilitymanagementpolicyupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the policy YAML was last updated. |
| <a id="vulnerabilitymanagementpolicyyaml"></a>`yaml` | [`String!`](#string) | YAML definition of the policy. |
### `VulnerabilityManagementPolicyAttributesType`
Represents policy fields related to the vulnerability management policy.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="vulnerabilitymanagementpolicyattributestypesource"></a>`source` | [`SecurityPolicySource!`](#securitypolicysource) | Source of the policy. Its fields depend on the source type. |
### `VulnerabilityNamespaceStatisticType`
Counts for each vulnerability severity in the group and its subgroups.
@ -47742,6 +47910,18 @@ One of:
- [`PypiMetadata`](#pypimetadata)
- [`TerraformModuleMetadata`](#terraformmodulemetadata)
#### `PolicyAttributesUnion`
Represents specific policy types. Its fields depend on the policy type.
One of:
- [`ApprovalPolicyAttributesType`](#approvalpolicyattributestype)
- [`PipelineExecutionPolicyAttributesType`](#pipelineexecutionpolicyattributestype)
- [`PipelineExecutionScheduledPolicyAttributesType`](#pipelineexecutionscheduledpolicyattributestype)
- [`ScanExecutionPolicyAttributesType`](#scanexecutionpolicyattributestype)
- [`VulnerabilityManagementPolicyAttributesType`](#vulnerabilitymanagementpolicyattributestype)
#### `Registrable`
One of:
@ -48336,6 +48516,7 @@ Implementations:
- [`PipelineExecutionSchedulePolicy`](#pipelineexecutionschedulepolicy)
- [`ScanExecutionPolicy`](#scanexecutionpolicy)
- [`ScanResultPolicy`](#scanresultpolicy)
- [`SecurityPolicyType`](#securitypolicytype)
- [`VulnerabilityManagementPolicy`](#vulnerabilitymanagementpolicy)
##### Fields

View File

@ -168,3 +168,16 @@ To resolve this issue:
For more information about the relations and batches that failed to export,
use the export status API endpoints for [projects](../../../api/project_relations_export.md#export-status)
and [groups](../../../api/group_relations_export.md#export-status) on the source instance.
## Error: `duplicate key value violates unique constraint`
You might get the following error when importing records:
```plaintext
PG::UniqueViolation: ERROR: duplicate key value violates unique constraint
```
This error occurs when a Sidekiq worker processing the import
restarts due to high CPU or memory usage during import.
To configure workers for imports, see
[Sidekiq configuration](../../project/import/_index.md#sidekiq-configuration).

View File

@ -46,7 +46,7 @@ To view the organizations you have access to:
{{< alert type="note" >}}
In [Cells 1.0](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/cells/iterations/cells-1.0/) organizations can be only private.
Support for only private organizations is proposed for [cells 1.0](https://gitlab.com/groups/gitlab-org/-/epics/12383).
{{< /alert >}}
@ -80,7 +80,7 @@ In [Cells 1.0](https://handbook.gitlab.com/handbook/engineering/architecture/des
{{< alert type="note" >}}
In [Cells 1.0](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/cells/iterations/cells-1.0/) organizations can be only private.
Support for only private organizations is proposed for [cells 1.0](https://gitlab.com/groups/gitlab-org/-/epics/12383).
{{< /alert >}}
@ -92,8 +92,8 @@ In [Cells 1.0](https://handbook.gitlab.com/handbook/engineering/architecture/des
{{< alert type="note" >}}
Switching between organizations is not supported in [Cells 1.0](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/cells/iterations/cells-1.0/),
but is supported in [Cells 1.5](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/cells/iterations/cells-1.5/).
Switching between organizations is not supported in cells 1.0.
Support for switching organizations is proposed for [cells 1.5](https://gitlab.com/groups/gitlab-org/-/epics/12505).
{{< /alert >}}
@ -152,7 +152,3 @@ The Organization description field supports a limited subset of [GitLab Flavored
- [Emphasis](../markdown.md#emphasis)
- [Links](../markdown.md#links)
- [Superscripts / Subscripts](../markdown.md#superscripts-and-subscripts)
## Related topics
- [Organization design document](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/organization/)

View File

@ -153,7 +153,7 @@ To create a project that uses SHA-256 hashing:
### Why SHA-256?
By default, Git uses the SHA-1 [hashing algorithm](https://handbook.gitlab.com/handbook/security/cryptographic-standard/#algorithmic-standards)
By default, Git uses the SHA-1 hashing algorithm
to generate a 40-character
ID for objects such as commits, blobs, trees, and tags. The SHA-1 algorithm was proven to be insecure when
[Google was able to produce a hash collision](https://security.googleblog.com/2017/02/announcing-first-sha1-collision.html).

View File

@ -14,15 +14,6 @@ module Authn
Gitlab::CurrentSettings.current_application_settings.instance_token_prefix
end
def self.default_instance_prefix(prefix)
return prefix unless Feature.enabled?(:custom_prefix_for_all_token_types, :instance)
return prefix unless prefix.starts_with?(instance_prefix)
# remove the configured instance prefix and add the default:
"#{ApplicationSetting.defaults[:instance_token_prefix]}#{prefix.delete_prefix(instance_prefix)}"
end
end
end
end

View File

@ -5,7 +5,7 @@ module Authn
class DeployToken
def self.prefix?(plaintext)
deploy_token_prefixes = [::DeployToken.prefix_for_deploy_token,
Authn::TokenField::PrefixHelper.default_instance_prefix(::DeployToken.prefix_for_deploy_token)]
::DeployToken::DEPLOY_TOKEN_PREFIX].uniq
plaintext.start_with?(*deploy_token_prefixes)
end

View File

@ -4,9 +4,10 @@ module Authn
module Tokens
class FeatureFlagsClientToken
def self.prefix?(plaintext)
current_prefix = ::Operations::FeatureFlagsClient.prefix_for_feature_flags_client_token
default_prefix = Authn::TokenField::PrefixHelper.default_instance_prefix(current_prefix)
plaintext.start_with?(current_prefix, default_prefix)
feature_flags_client_token_prefixes = [::Operations::FeatureFlagsClient.prefix_for_feature_flags_client_token,
::Operations::FeatureFlagsClient::FEATURE_FLAGS_CLIENT_TOKEN_PREFIX].uniq
plaintext.start_with?(*feature_flags_client_token_prefixes)
end
attr_reader :revocable, :source

View File

@ -4,8 +4,10 @@ module Authn
module Tokens
class FeedToken
def self.prefix?(plaintext)
plaintext.start_with?(::User.prefix_for_feed_token,
Authn::TokenField::PrefixHelper.default_instance_prefix(::User.prefix_for_feed_token))
feed_token_prefixes = [::User.prefix_for_feed_token,
::User::FEED_TOKEN_PREFIX].uniq
plaintext.start_with?(*feed_token_prefixes)
end
attr_reader :revocable, :source

View File

@ -20,7 +20,7 @@ module Gitlab
end
models_with_attr_encrypted_attributes.each do |model|
models_with_attributes[model] += model.attr_encrypted_attributes.keys
models_with_attributes[model] += model.attr_encrypted_encrypted_attributes.keys
end
models_with_encrypted_tokens.each do |model|
@ -87,7 +87,7 @@ module Gitlab
end
def models_with_attr_encrypted_attributes
all_models.select { |d| d.attr_encrypted_attributes.present? }
all_models.select { |d| d.attr_encrypted_encrypted_attributes.present? }
end
def models_with_encrypted_tokens

View File

@ -169,9 +169,9 @@ module Gitlab
end
def remove_encrypted_attributes!
return unless relation_class.respond_to?(:attr_encrypted_attributes) && relation_class.attr_encrypted_attributes.any?
return unless relation_class.respond_to?(:attr_encrypted_encrypted_attributes) && relation_class.attr_encrypted_encrypted_attributes.any?
relation_class.attr_encrypted_attributes.each_key do |key|
relation_class.attr_encrypted_encrypted_attributes.each_key do |key|
@relation_hash[key.to_s] = nil
end
end

View File

@ -67,7 +67,7 @@ module Gitlab
attr_reader :old_key, :new_key
def otp_secret_settings
@otp_secret_settings ||= User.attr_encrypted_attributes[:otp_secret]
@otp_secret_settings ||= User.attr_encrypted_encrypted_attributes[:otp_secret]
end
def reencrypt(user, old_key, new_key)

View File

@ -1191,6 +1191,9 @@ msgid_plural "%{no_of_days} days"
msgstr[0] ""
msgstr[1] ""
msgid "%{noteable_type} is not supported"
msgstr ""
msgid "%{numOfSteps} steps across %{numOfFiles} files"
msgstr ""
@ -66855,7 +66858,7 @@ msgstr ""
msgid "View all"
msgstr ""
msgid "View all (one more item)"
msgid "View all (%d more item)"
msgid_plural "View all (%d more items)"
msgstr[0] ""
msgstr[1] ""

View File

@ -322,7 +322,7 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set
let(:application_settings) { ApplicationSetting.current }
context 'with valid prefix' do
let(:prefix) { 'instance-prefix-' }
let(:prefix) { 'instanceprefix' }
it 'updates instance_token_prefix setting' do
put :update, params: { application_setting: { instance_token_prefix: prefix } }
@ -339,6 +339,7 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set
this-prefix-is-longer-than-20-characters
@
:
-
]
end

View File

@ -319,9 +319,10 @@ RSpec.describe SentNotificationsController, feature_category: :shared do
context 'when unsubscribing from design' do
let(:design) do
# reload necessary as namespace_id is set in a DB trigger
create(:design, issue: issue) do |design|
design.subscriptions.create!(user: user, project: project, subscribed: true)
end
end.reload
end
let(:sent_notification) do

View File

@ -20,7 +20,7 @@ describe('AccessTokens', () => {
const accessTokenCreate = '/api/v4/groups/1/service_accounts/:id/personal_access_tokens/';
const accessTokenRevoke = '/api/v4/groups/2/service_accounts/:id/personal_access_tokens/';
const accessTokenRotate = '/api/v4/groups/3/service_accounts/:id/personal_access_tokens/';
const accessTokenShow = '/api/v4/personal_access_tokens';
const accessTokenShow = '/api/v4/groups/4/service_accounts/:id/personal_access_token';
const id = 235;
const createComponent = () => {
@ -56,7 +56,7 @@ describe('AccessTokens', () => {
urlCreate: '/api/v4/groups/1/service_accounts/:id/personal_access_tokens/',
urlRevoke: '/api/v4/groups/2/service_accounts/:id/personal_access_tokens/',
urlRotate: '/api/v4/groups/3/service_accounts/:id/personal_access_tokens/',
urlShow: '/api/v4/personal_access_tokens',
urlShow: '/api/v4/groups/4/service_accounts/:id/personal_access_token',
});
expect(store.fetchTokens).toHaveBeenCalledTimes(1);
});

View File

@ -64,7 +64,7 @@ describe('useAccessTokens store', () => {
const urlCreate = '/api/v4/groups/1/service_accounts/:id/personal_access_tokens';
const urlRevoke = '/api/v4/groups/2/service_accounts/:id/personal_access_tokens';
const urlRotate = '/api/v4/groups/3/service_accounts/:id/personal_access_tokens';
const urlShow = '/api/v4/groups/4/service_accounts/:id/personal_access_token';
const urlShow = '/api/v4/personal_access_tokens?user_id=:id';
const headers = {
'X-Page': 1,
@ -173,7 +173,6 @@ describe('useAccessTokens store', () => {
page: 1,
sort: 'expires_asc',
search: 'my token',
user_id: 235,
},
}),
);
@ -194,12 +193,11 @@ describe('useAccessTokens store', () => {
expect(mockAxios.history.get).toHaveLength(1);
expect(mockAxios.history.get[0]).toEqual(
expect.objectContaining({
url: '/api/v4/groups/4/service_accounts/235/personal_access_token',
url: '/api/v4/personal_access_tokens?user_id=235',
params: {
page: 1,
sort: 'expires_asc',
search: 'dummy',
user_id: 235,
},
}),
);
@ -286,13 +284,12 @@ describe('useAccessTokens store', () => {
expect(mockAxios.history.get).toHaveLength(1);
expect(mockAxios.history.get[0]).toEqual(
expect.objectContaining({
url: '/api/v4/groups/4/service_accounts/235/personal_access_token',
url: '/api/v4/personal_access_tokens?user_id=235',
params: {
page: 1,
sort: 'expires_asc',
state: 'inactive',
search: 'my token',
user_id: 235,
},
}),
);
@ -405,7 +402,6 @@ describe('useAccessTokens store', () => {
page: 1,
sort: 'expires_asc',
search: 'my token',
user_id: 235,
},
}),
);
@ -510,7 +506,6 @@ describe('useAccessTokens store', () => {
page: 1,
sort: 'expires_asc',
search: 'my token',
user_id: 235,
},
}),
);

View File

@ -11,7 +11,7 @@ import WorkItemNoteReplying from '~/work_items/components/notes/work_item_note_r
import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue';
import toggleWorkItemNoteResolveDiscussion from '~/work_items/graphql/notes/toggle_work_item_note_resolve_discussion.mutation.graphql';
import {
mockWorkItemCommentNote,
mockWorkItemDiscussion,
mockToggleResolveDiscussionResponse,
mockWorkItemNotesResponseWithComments,
} from 'jest/work_items/mock_data';
@ -40,7 +40,7 @@ describe('Work Item Discussion', () => {
.mockResolvedValue(mockToggleResolveDiscussionResponse);
const createComponent = ({
discussion = [mockWorkItemCommentNote],
discussion = mockWorkItemDiscussion,
workItemId = mockWorkItemId,
workItemType = 'Task',
isExpandedOnLoad = true,
@ -85,7 +85,7 @@ describe('Work Item Discussion', () => {
describe('when hideFullscreenMarkdownButton is true', () => {
beforeEach(() => {
createComponent({
discussion: mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes,
discussion: mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0],
hideFullscreenMarkdownButton: true,
});
});
@ -100,18 +100,17 @@ describe('Work Item Discussion', () => {
describe('When the main comments has threads', () => {
beforeEach(() => {
createComponent({
discussion: mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes,
discussion: mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0],
});
});
it('should render timeline-entry-item with required data attributes', () => {
const expectedDiscussion =
mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes[0];
const expectedDiscussion = mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0];
expect(findTimelineEntryItem().attributes()).toEqual({
class: expect.any(String),
'data-note-id': expectedDiscussion.id.split('/').pop(),
'data-discussion-id': expectedDiscussion.discussion.id,
'data-note-id': expectedDiscussion.notes.nodes[0].id.split('/').pop(),
'data-discussion-id': expectedDiscussion.id,
});
});
@ -154,7 +153,7 @@ describe('Work Item Discussion', () => {
describe('When replying to any comment', () => {
beforeEach(async () => {
createComponent({
discussion: mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes,
discussion: mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0],
});
const mainComment = findThreadAtIndex(0);
@ -187,7 +186,7 @@ describe('Work Item Discussion', () => {
createComponent();
findThreadAtIndex(0).vm.$emit('deleteNote');
expect(wrapper.emitted('deleteNote')).toEqual([[mockWorkItemCommentNote]]);
expect(wrapper.emitted('deleteNote')).toEqual([[mockWorkItemDiscussion.notes.nodes[0]]]);
});
it('emits `error` event when child note emits an `error`', () => {
@ -204,28 +203,20 @@ describe('Work Item Discussion', () => {
window.gon.current_user_id = 'gid://gitlab/User/1';
window.gon.current_user_fullname = 'Administrator';
});
const resolvedDiscussionList =
mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes.slice();
resolvedDiscussionList.forEach((note) => {
return {
...note,
discussion: {
id: note.discussion.id,
resolved: true,
resolvable: true,
resolvedBy: {
id: 'gid://gitlab/User/1',
name: 'Administrator',
__typename: 'UserCore',
},
__typename: 'Discussion',
},
};
});
const resolvedDiscussion = {
...mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0],
resolved: true,
resolvable: true,
resolvedBy: {
id: 'gid://gitlab/User/1',
name: 'Administrator',
__typename: 'UserCore',
},
};
it('Resolved discussion is not expanded on default', () => {
createComponent({
discussion: resolvedDiscussionList,
discussion: resolvedDiscussion,
isExpandedOnLoad: false,
});
@ -234,7 +225,7 @@ describe('Work Item Discussion', () => {
it('should pass `isDiscussionResolvable` prop as true when user has resolveNote permission', () => {
createComponent({
discussion: resolvedDiscussionList,
discussion: resolvedDiscussion,
isExpandedOnLoad: false,
});
@ -242,23 +233,25 @@ describe('Work Item Discussion', () => {
});
it('should pass `isDiscussionResolvable` prop as false when user does not have resolveNote permission', () => {
const resolvedDiscussionListWithoutPermissions = resolvedDiscussionList.map((note) => {
return {
...note,
userPermissions: {
adminNote: true,
awardEmoji: true,
readNote: true,
createNote: true,
resolveNote: false,
repositionNote: true,
__typename: 'NotePermissions',
},
};
});
const resolvedDiscussionWithoutPermissions = { ...resolvedDiscussion };
resolvedDiscussionWithoutPermissions.notes.nodes = resolvedDiscussion.notes.nodes.map(
(note) => {
return {
...note,
userPermissions: {
adminNote: true,
awardEmoji: true,
createNote: true,
resolveNote: false,
__typename: 'NotePermissions',
},
};
},
);
createComponent({
discussion: resolvedDiscussionListWithoutPermissions,
discussion: resolvedDiscussionWithoutPermissions,
isExpandedOnLoad: false,
});
@ -267,8 +260,7 @@ describe('Work Item Discussion', () => {
it('toggles resolved status when toggle icon is clicked from note header', async () => {
createComponent({
discussion:
mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes.slice(),
discussion: mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0],
isExpandedOnLoad: true,
});
@ -287,14 +279,13 @@ describe('Work Item Discussion', () => {
});
describe('quote-reply event', () => {
const mockDiscussions =
mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes;
const mockDiscussion = mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0];
let mockAppendTextSpy;
beforeEach(async () => {
window.gon.current_user_id = 1;
createComponent({
discussion: mockDiscussions,
discussion: mockDiscussion,
});
mockAppendTextSpy = jest
@ -304,7 +295,7 @@ describe('Work Item Discussion', () => {
event: {
preventDefault: jest.fn(),
},
discussionId: mockDiscussions[0].discussion.id,
discussionId: mockDiscussion.id,
text: 'quoted text',
});
await nextTick();

View File

@ -84,8 +84,8 @@ describe('WorkItemNotes component', () => {
const findNotesLoading = () => wrapper.findComponent(WorkItemNotesLoading);
const findActivityHeader = () => wrapper.findComponent(WorkItemNotesActivityHeader);
const findSystemNoteAtIndex = (index) => findAllSystemNotes().at(index);
const findAllWorkItemCommentNotes = () => wrapper.findAllComponents(WorkItemDiscussion);
const findWorkItemCommentNoteAtIndex = (index) => findAllWorkItemCommentNotes().at(index);
const findAllWorkItemDiscussions = () => wrapper.findAllComponents(WorkItemDiscussion);
const findWorkItemDiscussionAtIndex = (index) => findAllWorkItemDiscussions().at(index);
const findDeleteNoteModal = () => wrapper.findComponent(GlModal);
const findWorkItemAddNote = () => wrapper.findComponent(WorkItemAddNote);
@ -276,8 +276,8 @@ describe('WorkItemNotes component', () => {
await waitForPromises();
expect(findWorkItemCommentNoteAtIndex(0).props('discussion')).toEqual(
mockWorkItemNoteResponse.data.note.discussion.notes.nodes,
expect(findWorkItemDiscussionAtIndex(0).props('discussion')).toEqual(
mockWorkItemNoteResponse.data.note.discussion,
);
});
});
@ -380,15 +380,15 @@ describe('WorkItemNotes component', () => {
it('should have work item notes', () => {
expect(workItemNotesWithCommentsQueryHandler).toHaveBeenCalled();
expect(findAllWorkItemCommentNotes()).toHaveLength(mockDiscussions.length);
expect(findAllWorkItemDiscussions()).toHaveLength(mockDiscussions.length);
});
it('should pass all the correct props to work item comment note', () => {
const commentIndex = 0;
const firstCommentNote = findWorkItemCommentNoteAtIndex(commentIndex);
const firstDiscussion = findWorkItemDiscussionAtIndex(commentIndex);
expect(firstCommentNote.props()).toMatchObject({
discussion: mockDiscussions[commentIndex].notes.nodes,
expect(firstDiscussion.props()).toMatchObject({
discussion: mockDiscussions[commentIndex],
autocompleteDataSources: autocompleteDataSources({
fullPath: 'test-path',
iid: mockWorkItemIid,
@ -407,7 +407,7 @@ describe('WorkItemNotes component', () => {
});
await waitForPromises();
findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', { id: '1', isLastNote: false });
findWorkItemDiscussionAtIndex(0).vm.$emit('deleteNote', { id: '1', isLastNote: false });
expect(showModal).toHaveBeenCalled();
});
@ -422,7 +422,7 @@ describe('WorkItemNotes component', () => {
it('sends the mutation with correct variables', () => {
const noteId = 'some-test-id';
findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', { id: noteId });
findWorkItemDiscussionAtIndex(0).vm.$emit('deleteNote', { id: noteId });
findDeleteNoteModal().vm.$emit('primary');
expect(deleteWorkItemNoteMutationSuccessHandler).toHaveBeenCalledWith({
@ -433,22 +433,22 @@ describe('WorkItemNotes component', () => {
});
it('successfully removes the note from the discussion', async () => {
expect(findWorkItemCommentNoteAtIndex(0).props('discussion')).toHaveLength(2);
expect(findWorkItemDiscussionAtIndex(0).props('discussion').notes.nodes).toHaveLength(2);
findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', {
findWorkItemDiscussionAtIndex(0).vm.$emit('deleteNote', {
id: mockDiscussions[0].notes.nodes[0].id,
});
findDeleteNoteModal().vm.$emit('primary');
await waitForPromises();
expect(findWorkItemCommentNoteAtIndex(0).props('discussion')).toHaveLength(1);
expect(findWorkItemDiscussionAtIndex(0).props('discussion').notes.nodes).toHaveLength(1);
});
it('successfully removes the discussion from work item if discussion only had one note', async () => {
const secondDiscussion = findWorkItemCommentNoteAtIndex(1);
const secondDiscussion = findWorkItemDiscussionAtIndex(1);
expect(findAllWorkItemCommentNotes()).toHaveLength(2);
expect(secondDiscussion.props('discussion')).toHaveLength(1);
expect(findAllWorkItemDiscussions()).toHaveLength(2);
expect(secondDiscussion.props('discussion').notes.nodes).toHaveLength(1);
secondDiscussion.vm.$emit('deleteNote', {
id: mockDiscussions[1].notes.nodes[0].id,
@ -457,7 +457,7 @@ describe('WorkItemNotes component', () => {
findDeleteNoteModal().vm.$emit('primary');
await waitForPromises();
expect(findAllWorkItemCommentNotes()).toHaveLength(1);
expect(findAllWorkItemDiscussions()).toHaveLength(1);
});
});
@ -468,7 +468,7 @@ describe('WorkItemNotes component', () => {
});
await waitForPromises();
findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', {
findWorkItemDiscussionAtIndex(0).vm.$emit('deleteNote', {
id: mockDiscussions[0].notes.nodes[0].id,
});
findDeleteNoteModal().vm.$emit('primary');
@ -514,7 +514,7 @@ describe('WorkItemNotes component', () => {
});
await waitForPromises();
expect(findWorkItemCommentNoteAtIndex(0).props('isWorkItemConfidential')).toBe(true);
expect(findWorkItemDiscussionAtIndex(0).props('isWorkItemConfidential')).toBe(true);
});
describe('when project context', () => {
@ -532,7 +532,7 @@ describe('WorkItemNotes component', () => {
defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
});
await waitForPromises();
expect(findAllWorkItemCommentNotes().at(0).props('isExpandedOnLoad')).toBe(true);
expect(findAllWorkItemDiscussions().at(0).props('isExpandedOnLoad')).toBe(true);
});
it('should be collapsed when the discussion is resolved', async () => {
@ -543,7 +543,7 @@ describe('WorkItemNotes component', () => {
});
await waitForPromises();
expect(findAllWorkItemCommentNotes().at(0).props('isExpandedOnLoad')).toBe(false);
expect(findAllWorkItemDiscussions().at(0).props('isExpandedOnLoad')).toBe(false);
});
it('should be expanded when the notes are resolved but the target note hash has note id', async () => {
@ -558,7 +558,7 @@ describe('WorkItemNotes component', () => {
await waitForPromises();
await nextTick();
expect(findAllWorkItemCommentNotes().at(0).props('isExpandedOnLoad')).toBe(true);
expect(findAllWorkItemDiscussions().at(0).props('isExpandedOnLoad')).toBe(true);
});
});
@ -614,7 +614,7 @@ describe('WorkItemNotes component', () => {
expect(findWorkItemAddNote().props('hideFullscreenMarkdownButton')).toBe(true);
});
it('passes prop to work-item-discussion', () => {
expect(findAllWorkItemCommentNotes().at(0).props('hideFullscreenMarkdownButton')).toBe(true);
expect(findAllWorkItemDiscussions().at(0).props('hideFullscreenMarkdownButton')).toBe(true);
});
});
@ -717,10 +717,6 @@ describe('WorkItemNotes component', () => {
expect(gfmEventHub.$emit).toHaveBeenCalledWith('edit-note', {
note: {
...mockLastNote,
discussion: {
...mockLastNote.discussion,
resolvable: false,
},
},
});
});

View File

@ -4036,6 +4036,9 @@ export const mockWorkItemNotesResponse = {
nodes: [
{
id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
resolved: false,
resolvable: false,
resolvedBy: null,
notes: {
nodes: [
{
@ -4054,13 +4057,6 @@ export const mockWorkItemNotesResponse = {
maxAccessLevelOfAuthor: 'Owner',
authorIsContributor: false,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723561234',
resolved: false,
resolvable: false,
resolvedBy: null,
__typename: 'Discussion',
},
userPermissions: {
adminNote: false,
awardEmoji: true,
@ -4091,6 +4087,9 @@ export const mockWorkItemNotesResponse = {
},
{
id: 'gid://gitlab/Discussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
resolved: false,
resolvable: false,
resolvedBy: null,
notes: {
nodes: [
{
@ -4109,9 +4108,6 @@ export const mockWorkItemNotesResponse = {
maxAccessLevelOfAuthor: 'Owner',
authorIsContributor: false,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723565678',
},
userPermissions: {
adminNote: false,
awardEmoji: true,
@ -4142,6 +4138,9 @@ export const mockWorkItemNotesResponse = {
},
{
id: 'gid://gitlab/Discussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
resolved: false,
resolvable: false,
resolvedBy: null,
notes: {
nodes: [
{
@ -4159,13 +4158,6 @@ export const mockWorkItemNotesResponse = {
maxAccessLevelOfAuthor: 'Owner',
authorIsContributor: false,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
resolved: false,
resolvable: false,
resolvedBy: null,
__typename: 'Discussion',
},
userPermissions: {
adminNote: false,
awardEmoji: true,
@ -4234,6 +4226,9 @@ export const mockWorkItemNotesByIidResponse = {
nodes: [
{
id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
resolved: false,
resolvable: false,
resolvedBy: null,
notes: {
nodes: [
{
@ -4252,13 +4247,6 @@ export const mockWorkItemNotesByIidResponse = {
maxAccessLevelOfAuthor: null,
authorIsContributor: false,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723561234',
resolved: false,
resolvable: false,
resolvedBy: null,
__typename: 'Discussion',
},
userPermissions: {
adminNote: false,
awardEmoji: true,
@ -4292,6 +4280,9 @@ export const mockWorkItemNotesByIidResponse = {
},
{
id: 'gid://gitlab/Discussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
resolved: false,
resolvable: false,
resolvedBy: null,
notes: {
nodes: [
{
@ -4310,13 +4301,6 @@ export const mockWorkItemNotesByIidResponse = {
maxAccessLevelOfAuthor: null,
authorIsContributor: false,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723568765',
resolved: false,
resolvable: false,
resolvedBy: null,
__typename: 'Discussion',
},
userPermissions: {
adminNote: false,
awardEmoji: true,
@ -4350,6 +4334,9 @@ export const mockWorkItemNotesByIidResponse = {
},
{
id: 'gid://gitlab/Discussion/addbc177f7664699a135130ab05ffb78c57e4db3',
resolved: false,
resolvable: false,
resolvedBy: null,
notes: {
nodes: [
{
@ -4368,13 +4355,6 @@ export const mockWorkItemNotesByIidResponse = {
maxAccessLevelOfAuthor: null,
authorIsContributor: false,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876',
resolved: false,
resolvable: false,
resolvedBy: null,
__typename: 'Discussion',
},
userPermissions: {
adminNote: false,
awardEmoji: true,
@ -4445,6 +4425,9 @@ export const mockMoreWorkItemNotesResponse = {
nodes: [
{
id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
resolved: false,
resolvable: false,
resolvedBy: null,
notes: {
nodes: [
{
@ -4463,13 +4446,6 @@ export const mockMoreWorkItemNotesResponse = {
maxAccessLevelOfAuthor: 'Owner',
authorIsContributor: false,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da1112356a59e',
resolved: false,
resolvable: false,
resolvedBy: null,
__typename: 'Discussion',
},
userPermissions: {
adminNote: false,
awardEmoji: true,
@ -4503,6 +4479,9 @@ export const mockMoreWorkItemNotesResponse = {
},
{
id: 'gid://gitlab/Discussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
resolved: false,
resolvable: false,
resolvedBy: null,
notes: {
nodes: [
{
@ -4521,13 +4500,6 @@ export const mockMoreWorkItemNotesResponse = {
maxAccessLevelOfAuthor: 'Owner',
authorIsContributor: false,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da1272356a59e',
resolved: false,
resolvable: false,
resolvedBy: null,
__typename: 'Discussion',
},
userPermissions: {
adminNote: false,
awardEmoji: true,
@ -4561,6 +4533,9 @@ export const mockMoreWorkItemNotesResponse = {
},
{
id: 'gid://gitlab/Discussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
resolved: false,
resolvable: false,
resolvedBy: null,
notes: {
nodes: [
{
@ -4578,13 +4553,6 @@ export const mockMoreWorkItemNotesResponse = {
maxAccessLevelOfAuthor: 'Owner',
authorIsContributor: false,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876',
resolved: false,
resolvable: false,
resolvedBy: null,
__typename: 'Discussion',
},
userPermissions: {
adminNote: false,
awardEmoji: true,
@ -4640,6 +4608,9 @@ export const createWorkItemNoteResponse = ({
id: 'gid://gitlab/Note/569',
discussion: {
id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
resolved: false,
resolvable: true,
resolvedBy: null,
notes: {
nodes: [
{
@ -4657,13 +4628,6 @@ export const createWorkItemNoteResponse = ({
maxAccessLevelOfAuthor: 'Owner',
authorIsContributor: false,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
resolved: false,
resolvable: true,
resolvedBy: null,
__typename: 'Discussion',
},
author: {
id: 'gid://gitlab/User/1',
avatarUrl:
@ -4720,13 +4684,6 @@ export const mockWorkItemCommentNote = {
maxAccessLevelOfAuthor: 'Owner',
authorIsContributor: false,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876',
resolved: false,
resolvable: true,
resolvedBy: null,
__typename: 'Discussion',
},
userPermissions: {
adminNote: false,
awardEmoji: true,
@ -4759,6 +4716,16 @@ export const mockWorkItemCommentByMaintainer = {
maxAccessLevelOfAuthor: 'Maintainer',
};
export const mockWorkItemDiscussion = {
id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
resolved: false,
resolvable: true,
resolvedBy: null,
notes: {
nodes: [mockWorkItemCommentNote],
},
};
export const mockWorkItemNotesResponseWithComments = (resolved = false) => {
return {
data: {
@ -4786,6 +4753,9 @@ export const mockWorkItemNotesResponseWithComments = (resolved = false) => {
nodes: [
{
id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
resolved,
resolvable: true,
resolvedBy: null,
notes: {
nodes: [
{
@ -4803,13 +4773,6 @@ export const mockWorkItemNotesResponseWithComments = (resolved = false) => {
maxAccessLevelOfAuthor: 'Owner',
authorIsContributor: false,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
resolved,
resolvable: true,
resolvedBy: null,
__typename: 'Discussion',
},
author: {
id: 'gid://gitlab/User/1',
avatarUrl:
@ -4848,13 +4811,6 @@ export const mockWorkItemNotesResponseWithComments = (resolved = false) => {
maxAccessLevelOfAuthor: 'Owner',
authorIsContributor: false,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
resolved,
resolvable: true,
resolvedBy: null,
__typename: 'Discussion',
},
author: {
id: 'gid://gitlab/User/1',
avatarUrl:
@ -4885,6 +4841,9 @@ export const mockWorkItemNotesResponseWithComments = (resolved = false) => {
},
{
id: 'gid://gitlab/Discussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
resolved,
resolvable: true,
resolvedBy: null,
notes: {
nodes: [
{
@ -4902,13 +4861,6 @@ export const mockWorkItemNotesResponseWithComments = (resolved = false) => {
maxAccessLevelOfAuthor: 'Owner',
authorIsContributor: false,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
resolved,
resolvable: true,
resolvedBy: null,
__typename: 'Discussion',
},
userPermissions: {
adminNote: false,
awardEmoji: true,
@ -4966,6 +4918,9 @@ export const workItemNotesCreateSubscriptionResponse = {
imported: false,
discussion: {
id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
resolved: false,
resolvable: false,
resolvedBy: null,
notes: {
nodes: [
{
@ -4983,13 +4938,6 @@ export const workItemNotesCreateSubscriptionResponse = {
maxAccessLevelOfAuthor: 'Owner',
authorIsContributor: false,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
resolved: false,
resolvable: false,
resolvedBy: null,
__typename: 'Discussion',
},
userPermissions: {
adminNote: false,
awardEmoji: true,
@ -5065,13 +5013,6 @@ export const workItemNotesUpdateSubscriptionResponse = {
maxAccessLevelOfAuthor: 'Owner',
authorIsContributor: false,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
resolved: false,
resolvable: false,
resolvedBy: null,
__typename: 'Discussion',
},
userPermissions: {
adminNote: false,
awardEmoji: true,
@ -5126,13 +5067,6 @@ export const workItemSystemNoteWithMetadata = {
maxAccessLevelOfAuthor: 'Owner',
authorIsContributor: false,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/7d4a46ea0525e2eeed451f7b718b0ebe73205374',
resolved: false,
resolvable: false,
resolvedBy: null,
__typename: 'Discussion',
},
author: {
id: 'gid://gitlab/User/1',
avatarUrl:
@ -5194,6 +5128,9 @@ export const workItemNotesWithSystemNotesWithChangedDescription = {
nodes: [
{
id: 'gid://gitlab/Discussion/aa72f4c2f3eef66afa6d79a805178801ce4bd89f',
resolved: false,
resolvable: false,
resolvedBy: null,
notes: {
nodes: [
{
@ -5212,13 +5149,6 @@ export const workItemNotesWithSystemNotesWithChangedDescription = {
maxAccessLevelOfAuthor: 'Owner',
authorIsContributor: false,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/aa72f4c2f3eef66afa6d79a805178801ce4bd89f',
resolved: false,
resolvable: false,
resolvedBy: null,
__typename: 'Discussion',
},
author: {
id: 'gid://gitlab/User/1',
avatarUrl:
@ -5262,6 +5192,9 @@ export const workItemNotesWithSystemNotesWithChangedDescription = {
},
{
id: 'gid://gitlab/Discussion/a7d3cf7bd72f7a98f802845f538af65cb11a02cc',
resolved: false,
resolvable: false,
resolvedBy: null,
notes: {
nodes: [
{
@ -5280,13 +5213,6 @@ export const workItemNotesWithSystemNotesWithChangedDescription = {
maxAccessLevelOfAuthor: 'Owner',
authorIsContributor: false,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/a7d3cf7bd72f7a98f802845f538af65cb11a02cc',
resolved: false,
resolvable: false,
resolvedBy: null,
__typename: 'Discussion',
},
author: {
id: 'gid://gitlab/User/1',
avatarUrl:
@ -5330,6 +5256,9 @@ export const workItemNotesWithSystemNotesWithChangedDescription = {
},
{
id: 'gid://gitlab/Discussion/391eed1ee0a258cc966a51dde900424f3b51b95d',
resolved: false,
resolvable: false,
resolvedBy: null,
notes: {
nodes: [
{
@ -5348,13 +5277,6 @@ export const workItemNotesWithSystemNotesWithChangedDescription = {
maxAccessLevelOfAuthor: 'Owner',
authorIsContributor: false,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/391eed1ee0a258cc966a51dde900424f3b51b95d',
resolved: false,
resolvable: false,
resolvedBy: null,
__typename: 'Discussion',
},
author: {
id: 'gid://gitlab/User/1',
avatarUrl:
@ -6131,6 +6053,13 @@ export const mockToggleResolveDiscussionResponse = {
discussionToggleResolve: {
discussion: {
id: 'gid://gitlab/Discussion/c4be5bec43a737e0966dbc4c040b1517e7febfa9',
resolved: true,
resolvable: true,
resolvedBy: {
id: 'gid://gitlab/User/1',
name: 'Administrator',
__typename: 'UserCore',
},
notes: {
nodes: [
{
@ -6147,17 +6076,6 @@ export const mockToggleResolveDiscussionResponse = {
maxAccessLevelOfAuthor: 'Owner',
lastEditedBy: null,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/c4be5bec43a737e0966dbc4c040b1517e7febfa9',
resolved: true,
resolvable: true,
resolvedBy: {
id: 'gid://gitlab/User/1',
name: 'Administrator',
__typename: 'UserCore',
},
__typename: 'Discussion',
},
author: {
id: 'gid://gitlab/User/1',
avatarUrl:
@ -6196,17 +6114,6 @@ export const mockToggleResolveDiscussionResponse = {
maxAccessLevelOfAuthor: 'Owner',
lastEditedBy: null,
externalAuthor: null,
discussion: {
id: 'gid://gitlab/Discussion/c4be5bec43a737e0966dbc4c040b1517e7febfa9',
resolved: true,
resolvable: true,
resolvedBy: {
id: 'gid://gitlab/User/1',
name: 'Administrator',
__typename: 'UserCore',
},
__typename: 'Discussion',
},
author: {
id: 'gid://gitlab/User/1',
avatarUrl:

View File

@ -82,7 +82,7 @@ RSpec.describe AccessTokensHelper, feature_category: :system_access do
create: '/-/user_settings/personal_access_tokens',
revoke: '/api/v4/personal_access_tokens',
rotate: '/api/v4/personal_access_tokens',
show: '/api/v4/personal_access_tokens'
show: '/api/v4/personal_access_tokens?user_id=:id'
}
}))
end

View File

@ -161,7 +161,7 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
'new_project_path' => '/projects/new',
'association_counts' => stubbed_results,
'organization_groups_projects_sort' => 'name_asc',
'organization_groups_projects_display' => 'projects'
'organization_groups_projects_display' => 'groups'
}
)
end
@ -203,9 +203,9 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
'new_group_path' => '/-/organizations/default/groups/new',
'new_project_path' => '/projects/new',
'organization_groups_projects_sort' => 'name_asc',
'organization_groups_projects_display' => 'projects',
'organization_groups_projects_display' => 'groups',
'user_preference_sort' => 'name_asc',
'user_preference_display' => 'projects'
'user_preference_display' => 'groups'
}
)
end

View File

@ -54,7 +54,7 @@ RSpec.describe Authn::AgnosticTokenIdentifier, feature_category: :system_access
end
context 'with custom instance prefix' do
let_it_be(:instance_prefix) { 'instance-prefix-' }
let_it_be(:instance_prefix) { 'instanceprefix' }
before do
stub_application_setting(instance_token_prefix: instance_prefix)

View File

@ -9,13 +9,13 @@ RSpec.describe Authn::TokenField::PrefixHelper, feature_category: :system_access
subject(:prepend_instance_prefix) { described_class.prepend_instance_prefix(prefix) }
context 'with application config default value' do
it 'prepends the instance wide token prefix' do
expect(prepend_instance_prefix).to eq("gl#{prefix}")
it 'returns the prefix without prepending the instance prefix' do
expect(prepend_instance_prefix).to eq(prefix)
end
end
context 'with application config set to custom value' do
let(:instance_prefix) { 'instance-prefix-' }
let(:instance_prefix) { 'instanceprefix' }
before do
stub_application_setting(instance_token_prefix: instance_prefix)
@ -36,56 +36,4 @@ RSpec.describe Authn::TokenField::PrefixHelper, feature_category: :system_access
end
end
end
describe '.default_instance_prefix' do
let(:prefix) { 'glprefix' }
subject(:default_instance_prefix) { described_class.default_instance_prefix(prefix) }
context 'with application config default value' do
it 'returns the default prefix' do
expect(default_instance_prefix).to eq(prefix)
end
end
context 'with application config set to custom value' do
let(:instance_prefix) { 'instance-prefix-' }
before do
stub_application_setting(instance_token_prefix: instance_prefix)
end
context 'for the default prefix' do
it 'still returns the default prefix' do
expect(default_instance_prefix).to eq(prefix)
end
end
context 'for the current instance prefix' do
let(:prefix) { "#{instance_prefix}token-prefix" }
it 'changes the instance prefix to the default prefix' do
expect(default_instance_prefix).to eq('gltoken-prefix')
end
end
context 'for a non-matching prefix' do
let(:prefix) { 'different-token-prefix' }
it 'keeps the prefix unchanged' do
expect(default_instance_prefix).to eq(prefix)
end
end
end
context 'with feature flag custom_prefix_for_all_token_types disabled' do
before do
stub_feature_flags(custom_prefix_for_all_token_types: false)
end
it 'keeps the prefix unchanged' do
expect(default_instance_prefix).to eq(prefix)
end
end
end
end

View File

@ -64,7 +64,7 @@ RSpec.describe Authn::Tokens::DeployToken, feature_category: :system_access do
end
context 'with custom instance prefix' do
let_it_be(:instance_prefix) { 'instance-prefix-' }
let_it_be(:instance_prefix) { 'instanceprefix' }
let(:valid_revocable) { create(:deploy_token) }
let(:plaintext) { valid_revocable.token }

View File

@ -14,7 +14,7 @@ RSpec.describe Authn::Tokens::FeedToken, feature_category: :system_access do
it_behaves_like 'finding the valid revocable'
context 'with different instance prefix' do
let(:instance_prefix) { 'instance-prefix-' }
let(:instance_prefix) { 'instanceprefix' }
before do
stub_application_setting(instance_token_prefix: instance_prefix)

View File

@ -10,10 +10,10 @@ RSpec.describe Gitlab::Email::Handler::UnsubscribeHandler do
stub_config_setting(host: 'localhost')
end
let_it_be(:project) { create(:project, :public, :repository) }
let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "#{mail_key}#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX}") }
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
let(:noteable) { create(:issue, project: project) }
let(:user) { create(:user) }
let(:noteable) { create(:issue, project: project) }
let!(:sent_notification) { SentNotification.record(noteable, user.id, mail_key) }

View File

@ -18,7 +18,7 @@ RSpec.describe 'Import/Export attribute configuration', feature_category: :impor
it 'has no new columns' do
relation_names_for(:project).each do |relation_name|
relation_class = relation_class_for_name(relation_name)
relation_attributes = relation_class.new.attributes.keys - relation_class.attr_encrypted_attributes.keys.map(&:to_s)
relation_attributes = relation_class.new.attributes.keys - relation_class.attr_encrypted_encrypted_attributes.keys.map(&:to_s)
current_attributes = parsed_attributes(relation_name, relation_attributes)
safe_attributes = safe_model_attributes[relation_class.to_s].dup || []

View File

@ -24,7 +24,7 @@ RSpec.describe 'Import/Export Project configuration', feature_category: :importe
context "where relation #{params[:relation_path]}" do
it 'does not have prohibited keys' do
relation_class = relation_class_for_name(relation_name)
relation_attributes = relation_class.new.attributes.keys - relation_class.attr_encrypted_attributes.keys.map(&:to_s)
relation_attributes = relation_class.new.attributes.keys - relation_class.attr_encrypted_encrypted_attributes.keys.map(&:to_s)
current_attributes = parsed_attributes(relation_name, relation_attributes)
prohibited_keys = current_attributes.select do |attribute|
prohibited_key?(attribute) || !relation_class.attribute_method?(attribute)

View File

@ -679,6 +679,8 @@ RSpec.describe Notify, feature_category: :code_review_workflow do
end
shared_examples 'a discussion note email' do |model|
let(:noteable_url_args) { {} }
it_behaves_like 'it should have Gmail Actions links'
# Two tests with flakiness:1 are coming from this test:
@ -702,7 +704,12 @@ RSpec.describe Notify, feature_category: :code_review_workflow do
issuable_url = "project_wiki_url" if note.for_wiki_page?
anchor = "note_#{note.id}"
is_expected.to have_body_text "started a new <a href=\"#{public_send(issuable_url, project, note.noteable, anchor: anchor)}\">discussion</a>"
is_expected.to have_body_text(
format(
"started a new <a href=\"%{url}\">discussion</a>",
url: public_send(issuable_url, project, note.noteable, anchor: anchor, **noteable_url_args)
)
)
end
context 'when a comment on an existing discussion' do
@ -826,12 +833,14 @@ RSpec.describe Notify, feature_category: :code_review_workflow do
before do
allow(note).to receive(:noteable).and_return(wiki_page_meta)
allow(wiki_page_meta).to receive(:id).and_return(wiki_page_meta.canonical_slug)
end
subject { described_class.note_wiki_page_email(recipient.id, note.id) }
it_behaves_like 'a discussion note email', :discussion_note_on_wiki_page
it_behaves_like 'a discussion note email', :discussion_note_on_wiki_page do
let(:noteable_url_args) { { id: wiki_page_meta.canonical_slug } }
end
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { wiki_page_meta }
end
@ -842,7 +851,7 @@ RSpec.describe Notify, feature_category: :code_review_workflow do
it_behaves_like 'appearance header and footer not enabled'
it 'has the correct subject' do
is_expected.to have_subject('Re: a-known-name | Page 1 (slug)')
is_expected.to have_subject("Re: a-known-name | #{wiki_page_meta.title} (slug)")
end
it 'contains a link to the wiki page note' do

View File

@ -1,63 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe MigrateAnonymousSearchesFlagToApplicationSettings, feature_category: :global_search do
let!(:application_setting) { table(:application_settings).create! }
describe '#down' do
let(:migration) { described_class.new }
context 'when search settings is already set' do
it 'removes the global search settings' do
migration.up
expected_search = application_setting.reload.search
expected_search.delete('anonymous_searches_allowed')
expect { migration.down }.to change { application_setting.reload.search }.to(expected_search)
end
end
end
describe '#up' do
context 'when ff is enabled' do
it 'migrates search from the feature flags in the application_settings successfully' do
search_settings = application_setting.reload.search
expected_settings = {
'anonymous_searches_allowed' => true
}
expected_search = search_settings.merge(expected_settings)
# Use a manually created migration instance with stubbed method
migration = described_class.new
allow(migration).to receive(:feature_flag_enabled?).with('allow_anonymous_searches').and_return(true)
# Run the migration manually instead of using migrate!
expect { migration.up }.to change {
application_setting.reload.search
}.to eq(expected_search)
end
end
context 'when the feature flag is disabled' do
it 'migrates the feature flag to the application_settings successfully' do
search_settings = application_setting.reload.search
expected_settings = {
'anonymous_searches_allowed' => false
}
expected_search = search_settings.merge(expected_settings)
# Use a manually created migration instance with stubbed method
migration = described_class.new
allow(migration).to receive(:feature_flag_enabled?).with('allow_anonymous_searches').and_return(false)
# Run the migration manually instead of using migrate!
expect { migration.up }.to change {
application_setting.reload.search
}.to eq(expected_search)
end
end
end
end

View File

@ -54,7 +54,7 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
project_api_limit: 400,
project_invited_groups_api_limit: 60,
projects_api_limit: 2000,
instance_token_prefix: 'gl',
instance_token_prefix: '',
use_clickhouse_for_analytics: false,
user_contributed_projects_api_limit: 100,
user_projects_api_limit: 300,

View File

@ -1,64 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'GitLab monkey-patches to AttrEncrypted', feature_category: :shared do
# See https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/23306
describe '#attribute_instance_methods_as_symbols_available?' do
let(:klass) do
Class.new(ActiveRecord::Base) do
# We need some sort of table to work on
self.table_name = 'projects'
attr_encrypted :foo
end
end
it 'returns false' do
expect(klass.__send__(:attribute_instance_methods_as_symbols_available?)).to be_falsy
end
it 'does not define virtual attributes' do
instance = klass.new
aggregate_failures do
%w[
encrypted_foo encrypted_foo=
encrypted_foo_iv encrypted_foo_iv=
encrypted_foo_salt encrypted_foo_salt=
].each do |method_name|
expect(instance).not_to respond_to(method_name)
end
end
end
it 'calls attr_changed? method with kwargs' do
obj = klass.new
expect(obj.foo_changed?).to eq(false)
end
end
describe '#attr_encrypted_attributes' do
let(:class_with_attr_encrypted) do
Class.new(ActiveRecord::Base) do
self.table_name = 'projects'
attr_accessor :encrypted_foo
attr_accessor :encrypted_foo_iv
attr_encrypted :foo, key: 'This is a key that is 256 bits!!'
end
end
it 'does not share state with other instances' do
instance = class_with_attr_encrypted.new
instance.foo = 'bar'
another_instance = class_with_attr_encrypted.new
expect(instance.attr_encrypted_attributes[:foo][:operation]).to eq(:encrypting)
expect(another_instance.attr_encrypted_attributes[:foo][:operation]).to be_nil
end
end
end

View File

@ -172,9 +172,19 @@ RSpec.describe BulkInsertSafe, feature_category: :database do
bulk_insert_item_class.bulk_insert!(items)
attribute_names = bulk_insert_item_class.attribute_names - %w[id created_at updated_at]
expect(bulk_insert_item_class.last(items.size).pluck(*attribute_names)).to eq(
items.pluck(*attribute_names))
encrypted_attribute_names = bulk_insert_item_class.attr_encrypted_encrypted_attributes.keys.map(&:to_s)
attribute_names = bulk_insert_item_class.attribute_names - %w[id created_at updated_at] - encrypted_attribute_names
decrypted_values = bulk_insert_item_class.last(items.size).map do |record|
encrypted_attribute_names.index_with { |attr| record.public_send(attr) }
end
expected_values = items.map do |record|
encrypted_attribute_names.index_with { |attr| record.public_send(attr) }
end
expect(bulk_insert_item_class.last(items.size).pluck(*attribute_names)).to eq(items.pluck(*attribute_names))
expect(decrypted_values).to eq(expected_values)
end
it 'rolls back the transaction when any item is invalid' do

View File

@ -24,7 +24,7 @@ RSpec.describe Gitlab::EncryptedAttribute, feature_category: :shared do
describe key_method do
context 'when encrypting' do
before do
record.attr_encrypted_attributes[:token][:operation] = :encrypting
record.attr_encrypted_encrypted_attributes[:token][:operation] = :encrypting
end
it 'returns the encryption key secret' do
@ -35,7 +35,7 @@ RSpec.describe Gitlab::EncryptedAttribute, feature_category: :shared do
context 'when decrypting' do
before do
record.attr_encrypted_attributes[:token][:operation] = :decrypting
record.attr_encrypted_encrypted_attributes[:token][:operation] = :decrypting
end
it 'returns the encryption key secret' do

View File

@ -46,8 +46,8 @@ RSpec.describe Gitlab::SensitiveSerializableHash, feature_category: :shared do
context "for #{klass.name}\##{attribute_name}" do
let(:attributes) { [attribute_name, "encrypted_#{attribute_name}", "encrypted_#{attribute_name}_iv"] }
it 'has a attr_encrypted_attributes field' do
expect(klass.attr_encrypted_attributes).to include(attribute_name.to_sym)
it 'has a attr_encrypted_encrypted_attributes field' do
expect(klass.attr_encrypted_encrypted_attributes).to include(attribute_name.to_sym)
end
it 'does not include the attribute in serializable_hash', :aggregate_failures do

View File

@ -1448,7 +1448,7 @@ RSpec.describe Integration, feature_category: :integrations do
options = record.send(:evaluated_attr_encrypted_options_for, :properties)
.merge(iv: hash['encrypted_properties_iv'])
decrypted = described_class.attr_decrypt(
decrypted = described_class.attr_encrypted_decrypt(
:properties,
hash['encrypted_properties'],
options

View File

@ -6,7 +6,7 @@ RSpec.describe Integrations::IssueTrackerData, feature_category: :integrations d
it_behaves_like Integrations::BaseDataFields
describe 'encrypted attributes' do
subject { described_class.attr_encrypted_attributes.keys }
subject { described_class.attr_encrypted_encrypted_attributes.keys }
it { is_expected.to contain_exactly(:issues_url, :new_issue_url, :project_url) }
end

View File

@ -12,7 +12,7 @@ RSpec.describe Integrations::JiraTrackerData, feature_category: :integrations do
end
describe 'encrypted attributes' do
subject { described_class.attr_encrypted_attributes.keys }
subject { described_class.attr_encrypted_encrypted_attributes.keys }
it { is_expected.to contain_exactly(:api_url, :password, :url, :username) }
end

View File

@ -12,7 +12,7 @@ RSpec.describe Integrations::ZentaoTrackerData, feature_category: :integrations
end
describe 'encrypted attributes' do
subject { described_class.attr_encrypted_attributes.keys }
subject { described_class.attr_encrypted_encrypted_attributes.keys }
it { is_expected.to contain_exactly(:url, :api_url, :zentao_product_xid, :api_token) }
end

View File

@ -31,14 +31,14 @@ RSpec.describe Operations::FeatureFlagsClient do
end
context 'with custom instance prefix' do
let_it_be(:instance_prefix) { 'instance-prefix-' }
let_it_be(:instance_prefix) { 'instanceprefix' }
before do
stub_application_setting(instance_token_prefix: instance_prefix)
end
it 'starts with instance prefix' do
expect(subject.token).to match(/instance-prefix-ffct-[A-Za-z0-9_-]{20}/)
expect(subject.token).to match(/instanceprefixglffct-[A-Za-z0-9_-]{20}/)
end
context 'with feature flag custom_prefix_for_all_token_types disabled' do

View File

@ -4,7 +4,8 @@ require 'spec_helper'
RSpec.describe SentNotification, :request_store, feature_category: :shared do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
describe 'validation' do
describe 'note validity' do
@ -46,6 +47,63 @@ RSpec.describe SentNotification, :request_store, feature_category: :shared do
it { is_expected.to belong_to(:issue_email_participant) }
end
describe 'callbacks' do
describe '#ensure_sharding_key' do
let(:additional_args) { {} }
subject(:notification_namespace_id) do
record = described_class.new(noteable: noteable, **additional_args)
record.valid?
record.namespace_id
end
context 'when noteable is a DesignManagement::Design' do
let(:noteable) { create(:design, issue: create(:issue, project: project)).reload }
# Using project.namespace_id here instead of project.project_namespace_id as that's the value the trigger
# sets for design_management_designs records in the DB. That shouls also be a valid sharding key.
it { is_expected.to eq(project.namespace_id) }
end
context 'when noteable is a Issue' do
let(:noteable) { create(:issue, project: project) }
it { is_expected.to eq(noteable.namespace_id) }
end
context 'when noteable is a MergeRequest' do
let(:noteable) { create(:merge_request, source_project: project) }
it { is_expected.to eq(project.project_namespace_id) }
end
context 'when noteable is a ProjectSnippet' do
let(:noteable) { create(:project_snippet, project: project) }
it { is_expected.to eq(project.project_namespace_id) }
end
context 'when noteable is a Commit' do
let(:commit) { create(:commit, project: project) }
let(:noteable) { nil }
let(:additional_args) { { project: project, noteable_type: commit.class.name, commit_id: commit.id } }
it { is_expected.to eq(project.project_namespace_id) }
end
context 'when noteable type is not supported' do
let(:noteable) { create(:personal_snippet) }
it 'raises an error' do
expect do
notification_namespace_id
end.to raise_error(SentNotification::INVALID_NOTEABLE)
end
end
end
end
shared_examples 'a successful sent notification' do
it 'creates a new SentNotification' do
expect { subject }.to change { described_class.count }.by(1)
@ -141,10 +199,6 @@ RSpec.describe SentNotification, :request_store, feature_category: :shared do
let(:noteable) { project.commit }
end
it_behaves_like 'a non-unsubscribable notification', 'personal snippet' do
let(:noteable) { create(:personal_snippet) }
end
it_behaves_like 'a non-unsubscribable notification', 'project snippet' do
let(:noteable) { create(:project_snippet, project: project) }
end
@ -180,10 +234,6 @@ RSpec.describe SentNotification, :request_store, feature_category: :shared do
let(:noteable) { project.commit }
end
it_behaves_like 'a non-commit notification', 'personal snippet' do
let(:noteable) { create(:personal_snippet) }
end
it_behaves_like 'a non-commit notification', 'project snippet' do
let(:noteable) { create(:project_snippet, project: project) }
end
@ -219,10 +269,6 @@ RSpec.describe SentNotification, :request_store, feature_category: :shared do
let(:noteable) { project.commit }
end
it_behaves_like 'a snippet notification', 'personal snippet' do
let(:noteable) { create(:personal_snippet) }
end
it_behaves_like 'a snippet notification', 'project snippet' do
let(:noteable) { create(:project_snippet, project: project) }
end

View File

@ -88,10 +88,10 @@ RSpec.describe UserPreference, feature_category: :user_profile do
end
describe 'organization_groups_projects_display' do
it 'is set to 0 by default' do
it 'is set to 1 by default' do
pref = described_class.new
expect(pref.organization_groups_projects_display).to eq('projects')
expect(pref.organization_groups_projects_display).to eq('groups')
end
it { is_expected.to define_enum_for(:organization_groups_projects_display).with_values(projects: 0, groups: 1) }

View File

@ -2523,7 +2523,7 @@ RSpec.describe User, feature_category: :user_profile do
end
context 'with instance prefix configured' do
let(:instance_prefix) { 'instance-prefix-' }
let(:instance_prefix) { 'instanceprefix' }
before do
stub_application_setting(instance_token_prefix: instance_prefix)
@ -2532,7 +2532,7 @@ RSpec.describe User, feature_category: :user_profile do
it 'returns feed token with instance prefix' do
user = create(:user)
expect(user.feed_token).to start_with("#{instance_prefix}ft-")
expect(user.feed_token).to start_with("#{instance_prefix}glft-")
end
end

View File

@ -1733,8 +1733,8 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
context 'wiki page note', :deliver_mails_inline do
let_it_be(:project) { create(:project, :public, :repository) }
let(:wiki_page_meta) { create(:wiki_page_meta, :for_wiki_page, container: project) }
let(:note) { create(:note, noteable: wiki_page_meta, project: project) }
let_it_be(:wiki_page_meta) { create(:wiki_page_meta, :for_wiki_page, container: project) }
let_it_be(:note) { create(:note, noteable: wiki_page_meta, project: project) }
before_all do
build_team(project)

View File

@ -66,6 +66,7 @@
- Security::PipelineExecutionPoliciesFinder
- Security::PipelineExecutionSchedulePoliciesFinder
- Security::VulnerabilityManagementPoliciesFinder
- Security::AllPoliciesFinder
- Security::SecurityPoliciesFinder
- Security::SecurityProjectGroupFinder
- SentryIssueFinder

View File

@ -79,7 +79,7 @@ end
RSpec.shared_examples 'contains instance prefix when enabled' do
context 'with default instance prefix' do
let_it_be(:instance_prefix) { 'instance-prefix-' }
let_it_be(:instance_prefix) { 'instanceprefix' }
before do
stub_application_setting(instance_token_prefix: instance_prefix)
@ -91,7 +91,7 @@ RSpec.shared_examples 'contains instance prefix when enabled' do
end
context 'with custom instance prefix' do
let_it_be(:instance_prefix) { 'instance-prefix-' }
let_it_be(:instance_prefix) { 'instanceprefix' }
before do
stub_application_setting(instance_token_prefix: instance_prefix)

View File

@ -3,7 +3,7 @@
RSpec.shared_examples 'encrypted attribute' do |attr, key_method|
describe "encrypted attribute :#{attr}" do
it 'uses correct key method' do
expect(record.attr_encrypted_attributes[attr][:key]).to eq(key_method)
expect(record.attr_encrypted_encrypted_attributes[attr][:key]).to eq(key_method)
end
end
end

View File

@ -332,7 +332,7 @@ RSpec.shared_examples 'a webhook' do |factory:|
end
describe 'encrypted attributes' do
subject { described_class.attr_encrypted_attributes.keys }
subject { described_class.attr_encrypted_encrypted_attributes.keys }
it { is_expected.to contain_exactly(:token, :url, :url_variables, :custom_headers) }
end