Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6a7ae91bd0
commit
ebb15c08a9
50
CHANGELOG.md
50
CHANGELOG.md
|
|
@ -2,6 +2,23 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 17.11.1 (2025-04-22)
|
||||
|
||||
### Fixed (1 change)
|
||||
|
||||
- [Fix string conversion for CI Inputs](https://gitlab.com/gitlab-org/security/gitlab/-/commit/aceb71126fb8ea5be6259a2156c6255bbaa1f3de)
|
||||
|
||||
### Changed (1 change)
|
||||
|
||||
- [Put allow_composite_identities_to_run_pipelines behind ff](https://gitlab.com/gitlab-org/security/gitlab/-/commit/2287e37df7c9ed82aa54643759e00bbf30a788c8)
|
||||
|
||||
### Security (4 changes)
|
||||
|
||||
- [Add SecureHeaders middleware with the Nel header](https://gitlab.com/gitlab-org/security/gitlab/-/commit/5a586de4d56429eabe0fb6ebc524894925759d2e) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4934))
|
||||
- [Restrict forwarded headers in Maven dependency proxy](https://gitlab.com/gitlab-org/security/gitlab/-/commit/80244b98dd92312510f4a9276b5adfcbaba8e68a) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4929))
|
||||
- [Security unauthorized access to reading branch names](https://gitlab.com/gitlab-org/security/gitlab/-/commit/9f9724584d109181e764f79a3b61667520d2212f) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4933))
|
||||
- [Simplify detecting paragraphs for quick actions](https://gitlab.com/gitlab-org/security/gitlab/-/commit/78466ef2cd3ddee5fbf0db67056a5bccf7c59907) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4925))
|
||||
|
||||
## 17.11.0 (2025-04-16)
|
||||
|
||||
### Added (211 changes)
|
||||
|
|
@ -767,6 +784,25 @@ entry.
|
|||
- [Remove feature flag allow_merge_request_pipelines_from_fork](https://gitlab.com/gitlab-org/gitlab/-/commit/b62f9187a57cc5ba66ce26889516cc55a425181a) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/182862))
|
||||
- [Finalize migration BackfillNewAuditEventTables](https://gitlab.com/gitlab-org/gitlab/-/commit/1bc0f07ffd3af5b9fab8a0ea0b1af5f2759d25db) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181881))
|
||||
|
||||
## 17.10.5 (2025-04-22)
|
||||
|
||||
### Fixed (3 changes)
|
||||
|
||||
- [Fix 500 in Todo API when wiki page todo exists](https://gitlab.com/gitlab-org/security/gitlab/-/commit/71215f0615fad3167fb96b521b9628e11ea30a5d)
|
||||
- [Clear session cookie when browser is closed](https://gitlab.com/gitlab-org/security/gitlab/-/commit/7c77ca404d9be7166d8ef991013394483b3f0371)
|
||||
- [Fix workspaces reconciliation to send inventory config map correctly](https://gitlab.com/gitlab-org/security/gitlab/-/commit/aba508e925aea81c4d47555254e6a657edc94863) **GitLab Enterprise Edition**
|
||||
|
||||
### Security (4 changes)
|
||||
|
||||
- [Add SecureHeaders middleware with the Nel header](https://gitlab.com/gitlab-org/security/gitlab/-/commit/0e180be62768513438f86ea99f0a4a305cca46b6) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4919))
|
||||
- [Restrict forwarded headers in Maven dependency proxy](https://gitlab.com/gitlab-org/security/gitlab/-/commit/faa100503f89d08e51549e4f35f362c9945dbb6f) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4930))
|
||||
- [Security unauthorized access to reading branch names](https://gitlab.com/gitlab-org/security/gitlab/-/commit/dc2f917499f58ed9ccff23158b39528b62b71c2f) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4895))
|
||||
- [Simplify detecting paragraphs for quick actions](https://gitlab.com/gitlab-org/security/gitlab/-/commit/507e465f21b5be5297eda7a67f0ba75994df88d8) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4926))
|
||||
|
||||
### Other (1 change)
|
||||
|
||||
- [No-op FinalizeBackfillCiRunnerMachinesPartitionedTable migration](https://gitlab.com/gitlab-org/security/gitlab/-/commit/119891459658f48120bcef02b1b66e3e78c78865)
|
||||
|
||||
## 17.10.4 (2025-04-09)
|
||||
|
||||
### Fixed (2 changes)
|
||||
|
|
@ -1608,6 +1644,20 @@ No changes.
|
|||
- [Quarantine a flaky test](https://gitlab.com/gitlab-org/gitlab/-/commit/998d8028213da6bf0c3c1c08301797c8b3395c28) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/180234))
|
||||
- [Quarantine a flaky test](https://gitlab.com/gitlab-org/gitlab/-/commit/8ae69a3765cfb7561db95e43faa30cc60fac6444) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/177662))
|
||||
|
||||
## 17.9.7 (2025-04-22)
|
||||
|
||||
### Security (4 changes)
|
||||
|
||||
- [Add SecureHeaders middleware with the Nel header](https://gitlab.com/gitlab-org/security/gitlab/-/commit/5e9112ba2d6fac340e1dbfc4b0330bdcff2f82bc) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4920))
|
||||
- [Restrict forwarded headers in Maven dependency proxy](https://gitlab.com/gitlab-org/security/gitlab/-/commit/24d73f3e778471c4e0153af174f2d8b44e106108) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4931))
|
||||
- [Security unauthorized access to reading branch names](https://gitlab.com/gitlab-org/security/gitlab/-/commit/cb09987371857b97ec3cc5774b9f214c9db7c8a6) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4923))
|
||||
- [Simplify detecting paragraphs for quick actions](https://gitlab.com/gitlab-org/security/gitlab/-/commit/29d3f746b02137aaff4364473ce62f701d208e27) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4927))
|
||||
|
||||
### Other (2 changes)
|
||||
|
||||
- [Clean up BackfillCiRunnerMachinesPartitionedTable migration](https://gitlab.com/gitlab-org/security/gitlab/-/commit/76d351431e07b3a695356c7b78ac16d23b180302)
|
||||
- [No-op FinalizeBackfillCiRunnerMachinesPartitionedTable migration](https://gitlab.com/gitlab-org/security/gitlab/-/commit/fb16e2ebcc6188b3f9bb0bc30e7e2b709484f3b1)
|
||||
|
||||
## 17.9.6 (2025-04-09)
|
||||
|
||||
### Security (6 changes)
|
||||
|
|
|
|||
|
|
@ -117,11 +117,12 @@ const watchFluxResource = async ({
|
|||
};
|
||||
|
||||
try {
|
||||
const config = new Configuration(variables.configuration);
|
||||
await subscribeToSocket({
|
||||
watchId,
|
||||
watchParams,
|
||||
configuration: variables.configuration,
|
||||
cacheParams,
|
||||
config,
|
||||
});
|
||||
} catch {
|
||||
await watchFunction();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
import { WebSocketWatchManager } from '@gitlab/cluster-client';
|
||||
|
||||
let watchManagerInstance = null;
|
||||
|
||||
export function resetWatchManager() {
|
||||
watchManagerInstance = null;
|
||||
}
|
||||
|
||||
export function getWatchManager(configuration) {
|
||||
if (!watchManagerInstance) {
|
||||
if (!configuration) {
|
||||
throw new Error('WebSocketWatchManager not initialized. Provide configuration first.');
|
||||
}
|
||||
watchManagerInstance = new WebSocketWatchManager(configuration);
|
||||
}
|
||||
|
||||
return watchManagerInstance;
|
||||
}
|
||||
|
|
@ -2,11 +2,11 @@ import {
|
|||
CoreV1Api,
|
||||
Configuration,
|
||||
WatchApi,
|
||||
webSocketWatchManager,
|
||||
EVENT_DATA,
|
||||
EVENT_TIMEOUT,
|
||||
EVENT_ERROR,
|
||||
} from '@gitlab/cluster-client';
|
||||
import { getWatchManager } from '~/environments/services/websocket_connection_service';
|
||||
import { connectionStatus } from '~/environments/graphql/resolvers/kubernetes/constants';
|
||||
import { updateConnectionStatus } from '~/environments/graphql/resolvers/kubernetes/k8s_connection_status';
|
||||
import { s__ } from '~/locale';
|
||||
|
|
@ -54,13 +54,14 @@ export const mapEventItem = ({
|
|||
type,
|
||||
}) => ({ lastTimestamp, eventTime, message, reason, source, type });
|
||||
|
||||
export const subscribeToSocket = async ({ watchId, watchParams, configuration, cacheParams }) => {
|
||||
export const subscribeToSocket = async ({ watchId, watchParams, cacheParams, config }) => {
|
||||
const { updateQueryCache, updateConnectionStatusFn } = cacheParams;
|
||||
|
||||
try {
|
||||
const watcher = await webSocketWatchManager.initConnection({
|
||||
const watcherConnection = getWatchManager(config);
|
||||
|
||||
const watcher = await watcherConnection.initConnection({
|
||||
message: { watchId, watchParams },
|
||||
configuration,
|
||||
});
|
||||
|
||||
const handleConnectionStatus = (status) => {
|
||||
|
|
@ -142,7 +143,7 @@ export const watchWorkloadItems = async ({
|
|||
};
|
||||
|
||||
try {
|
||||
await subscribeToSocket({ watchId, watchParams, configuration, cacheParams });
|
||||
await subscribeToSocket({ watchId, watchParams, cacheParams, config });
|
||||
} catch {
|
||||
await watchFunction();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@ import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
|||
import Tracking from '~/tracking';
|
||||
import { __ } from '~/locale';
|
||||
import { clearDraft } from '~/lib/utils/autosave';
|
||||
import { findWidget } from '~/issues/list/utils';
|
||||
import DiscussionReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
|
||||
import ResolveDiscussionButton from '~/notes/components/discussion_resolve_button.vue';
|
||||
import { updateCacheAfterCreatingNote } from '../../graphql/cache_utils';
|
||||
import createNoteMutation from '../../graphql/notes/create_work_item_note.mutation.graphql';
|
||||
import workItemNotesByIidQuery from '../../graphql/notes/work_item_notes_by_iid.query.graphql';
|
||||
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
|
||||
import { TRACKING_CATEGORY_SHOW, WIDGET_TYPE_EMAIL_PARTICIPANTS, i18n } from '../../constants';
|
||||
import { TRACKING_CATEGORY_SHOW, i18n } from '../../constants';
|
||||
import { findEmailParticipantsWidget } from '../../utils';
|
||||
import WorkItemNoteSignedOut from './work_item_note_signed_out.vue';
|
||||
import WorkItemCommentLocked from './work_item_comment_locked.vue';
|
||||
import WorkItemCommentForm from './work_item_comment_form.vue';
|
||||
|
|
@ -223,7 +223,7 @@ export default {
|
|||
return this.isDiscussionResolved ? __('Unresolve thread') : __('Resolve thread');
|
||||
},
|
||||
hasEmailParticipantsWidget() {
|
||||
return Boolean(findWidget(WIDGET_TYPE_EMAIL_PARTICIPANTS, this.workItem));
|
||||
return Boolean(findEmailParticipantsWidget(this.workItem));
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
|
|
|||
|
|
@ -3,14 +3,8 @@ import { GlButton, GlFormCheckbox, GlTooltipDirective } from '@gitlab/ui';
|
|||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { s__, __ } from '~/locale';
|
||||
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
|
||||
import {
|
||||
STATE_OPEN,
|
||||
WORK_ITEM_TYPE_NAME_TASK,
|
||||
WIDGET_TYPE_EMAIL_PARTICIPANTS,
|
||||
i18n,
|
||||
} from '~/work_items/constants';
|
||||
import { STATE_OPEN, WORK_ITEM_TYPE_NAME_TASK, i18n } from '~/work_items/constants';
|
||||
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
|
||||
import { findWidget } from '~/issues/list/utils';
|
||||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
import glAbilitiesMixin from '~/vue_shared/mixins/gl_abilities_mixin';
|
||||
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
|
||||
|
|
@ -19,6 +13,7 @@ import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.
|
|||
import CommentFieldLayout from '~/notes/components/comment_field_layout.vue';
|
||||
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
|
||||
import workItemEmailParticipantsByIidQuery from '../../graphql/notes/work_item_email_participants_by_iid.query.graphql';
|
||||
import { findEmailParticipantsWidget } from '../../utils';
|
||||
|
||||
const DOCS_WORK_ITEM_LOCKED_TASKS_PATH = helpPagePath('user/tasks.html', {
|
||||
anchor: 'lock-discussion',
|
||||
|
|
@ -269,8 +264,7 @@ export default {
|
|||
},
|
||||
update(data) {
|
||||
return (
|
||||
findWidget(WIDGET_TYPE_EMAIL_PARTICIPANTS, data?.workspace?.workItem)?.emailParticipants
|
||||
?.nodes || []
|
||||
findEmailParticipantsWidget(data?.workspace?.workItem)?.emailParticipants?.nodes || []
|
||||
);
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@
|
|||
import { GlTooltipDirective, GlAvatarsInline, GlAvatar, GlAvatarLink } from '@gitlab/ui';
|
||||
import ItemMilestone from '~/issuable/components/issue_milestone.vue';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import { WIDGET_TYPE_MILESTONE, WIDGET_TYPE_ASSIGNEES } from '~/work_items/constants';
|
||||
import { findWidget } from '~/issues/list/utils';
|
||||
import { getDisplayReference } from '../../utils';
|
||||
import { findAssigneesWidget, findMilestoneWidget, getDisplayReference } from '../../utils';
|
||||
|
||||
export default {
|
||||
name: 'WorkItemRelationshipPopoverMetadata',
|
||||
|
|
@ -30,10 +28,10 @@ export default {
|
|||
assigneesDisplayLimit: 3,
|
||||
computed: {
|
||||
workItemAssignees() {
|
||||
return findWidget(WIDGET_TYPE_ASSIGNEES, this.workItem)?.assignees?.nodes || [];
|
||||
return findAssigneesWidget(this.workItem)?.assignees?.nodes || [];
|
||||
},
|
||||
workItemMilestone() {
|
||||
return findWidget(WIDGET_TYPE_MILESTONE, this.workItem)?.milestone;
|
||||
return findMilestoneWidget(this.workItem)?.milestone;
|
||||
},
|
||||
fullReference() {
|
||||
return getDisplayReference(this.workItemFullPath, this.workItem.reference);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import { GlLink, GlPopover, GlTooltipDirective, GlTruncateText } from '@gitlab/ui';
|
||||
import { difference, groupBy, xor } from 'lodash';
|
||||
import { findWidget } from '~/issues/list/utils';
|
||||
import { __, n__, s__ } from '~/locale';
|
||||
import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue';
|
||||
import Tracking from '~/tracking';
|
||||
|
|
@ -9,8 +8,8 @@ import getGroupContactsQuery from '~/crm/contacts/components/graphql/get_group_c
|
|||
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
|
||||
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
||||
import updateNewWorkItemMutation from '../graphql/update_new_work_item.mutation.graphql';
|
||||
import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_CRM_CONTACTS } from '../constants';
|
||||
import { newWorkItemFullPath, newWorkItemId } from '../utils';
|
||||
import { i18n, TRACKING_CATEGORY_SHOW } from '../constants';
|
||||
import { findCrmContactsWidget, newWorkItemFullPath, newWorkItemId } from '../utils';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
|
|
@ -86,7 +85,7 @@ export default {
|
|||
return this.groupByOrganization(this.selectedItems, false);
|
||||
},
|
||||
workItemCrmContacts() {
|
||||
return findWidget(WIDGET_TYPE_CRM_CONTACTS, this.workItem);
|
||||
return findCrmContactsWidget(this.workItem);
|
||||
},
|
||||
workItemFullPath() {
|
||||
return this.createFlow
|
||||
|
|
|
|||
|
|
@ -1,20 +1,12 @@
|
|||
<script>
|
||||
import { GlIcon, GlAlert, GlTooltipDirective } from '@gitlab/ui';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
|
||||
import { s__, __ } from '~/locale';
|
||||
import { findWidget } from '~/issues/list/utils';
|
||||
|
||||
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
|
||||
import workItemDevelopmentQuery from '~/work_items/graphql/work_item_development.query.graphql';
|
||||
import workItemDevelopmentUpdatedSubscription from '~/work_items/graphql/work_item_development.subscription.graphql';
|
||||
import {
|
||||
sprintfWorkItem,
|
||||
WIDGET_TYPE_DEVELOPMENT,
|
||||
STATE_OPEN,
|
||||
DEVELOPMENT_ITEMS_ANCHOR,
|
||||
} from '~/work_items/constants';
|
||||
|
||||
import { sprintfWorkItem, STATE_OPEN, DEVELOPMENT_ITEMS_ANCHOR } from '~/work_items/constants';
|
||||
import { findDevelopmentWidget } from '~/work_items/utils';
|
||||
import CrudComponent from '~/vue_shared/components/crud_component.vue';
|
||||
import WorkItemActionsSplitButton from '~/work_items/components/work_item_links/work_item_actions_split_button.vue';
|
||||
import WorkItemDevelopmentRelationshipList from './work_item_development_relationship_list.vue';
|
||||
|
|
@ -204,7 +196,7 @@ export default {
|
|||
};
|
||||
},
|
||||
update(data) {
|
||||
return findWidget(WIDGET_TYPE_DEVELOPMENT, data?.workItem) || {};
|
||||
return findDevelopmentWidget(data?.workItem) || {};
|
||||
},
|
||||
skip() {
|
||||
return !this.workItemIid;
|
||||
|
|
|
|||
|
|
@ -2,13 +2,8 @@
|
|||
import { GlIcon, GlTooltip, GlPopover } from '@gitlab/ui';
|
||||
import { s__, __ } from '~/locale';
|
||||
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
|
||||
import { findWidget } from '~/issues/list/utils';
|
||||
import {
|
||||
i18n,
|
||||
WIDGET_TYPE_WEIGHT,
|
||||
WORK_ITEM_TYPE_NAME_EPIC,
|
||||
WIDGET_TYPE_HEALTH_STATUS,
|
||||
} from '../../constants';
|
||||
import { i18n, WORK_ITEM_TYPE_NAME_EPIC } from '../../constants';
|
||||
import { findHealthStatusWidget, findWeightWidget } from '../../utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -72,10 +67,10 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
workItemWeight() {
|
||||
return findWidget(WIDGET_TYPE_WEIGHT, this.workItem);
|
||||
return findWeightWidget(this.workItem);
|
||||
},
|
||||
workItemHealthStatus() {
|
||||
return findWidget(WIDGET_TYPE_HEALTH_STATUS, this.workItem);
|
||||
return findHealthStatusWidget(this.workItem);
|
||||
},
|
||||
shouldRolledUpWeightBeVisible() {
|
||||
return this.showRolledUpWeight && this.rolledUpWeight !== null;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { GlAlert } from '@gitlab/ui';
|
|||
import { sprintf, s__ } from '~/locale';
|
||||
import { createAlert } from '~/alert';
|
||||
import CrudComponent from '~/vue_shared/components/crud_component.vue';
|
||||
import { findWidget } from '~/issues/list/utils';
|
||||
import { getParameterByName } from '~/lib/utils/url_utility';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import {
|
||||
|
|
@ -192,7 +191,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
workItemHierarchy() {
|
||||
return findWidget(WIDGET_TYPE_HIERARCHY, this.workItem);
|
||||
return findHierarchyWidget(this.workItem);
|
||||
},
|
||||
rolledUpCountsByType() {
|
||||
return this.workItemHierarchy?.rolledUpCountsByType || [];
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
|
|||
import { getBaseURL } from '~/lib/utils/url_utility';
|
||||
import { convertEachWordToTitleCase } from '~/lib/utils/text_utility';
|
||||
import { getDraft, clearDraft } from '~/lib/utils/autosave';
|
||||
import { findWidget } from '~/issues/list/utils';
|
||||
import {
|
||||
newWorkItemOptimisticUserPermissions,
|
||||
WIDGET_TYPE_ASSIGNEES,
|
||||
|
|
@ -32,6 +31,7 @@ import {
|
|||
} from 'ee_else_ce/work_items/constants';
|
||||
import {
|
||||
findCurrentUserTodosWidget,
|
||||
findDescriptionWidget,
|
||||
findHierarchyWidget,
|
||||
findHierarchyWidgetChildren,
|
||||
findNotesWidget,
|
||||
|
|
@ -350,8 +350,7 @@ export const setNewWorkItemCache = async (
|
|||
const draftData = JSON.parse(getDraft(autosaveKey));
|
||||
|
||||
const draftTitle = draftData?.workspace?.workItem?.title || '';
|
||||
const draftDescriptionWidget =
|
||||
findWidget(WIDGET_TYPE_DESCRIPTION, draftData?.workspace?.workItem) || {};
|
||||
const draftDescriptionWidget = findDescriptionWidget(draftData?.workspace?.workItem) || {};
|
||||
const draftDescription = draftDescriptionWidget?.description || null;
|
||||
|
||||
widgets.push({
|
||||
|
|
|
|||
|
|
@ -5,7 +5,12 @@ import { findWidget } from '~/issues/list/utils';
|
|||
import { newDate, toISODateFormat } from '~/lib/utils/datetime_utility';
|
||||
import { updateDraft } from '~/lib/utils/autosave';
|
||||
import { getParameterByName } from '~/lib/utils/url_utility';
|
||||
import { getNewWorkItemAutoSaveKey, newWorkItemFullPath } from '../utils';
|
||||
import {
|
||||
findCustomFieldsWidget,
|
||||
findStartAndDueDateWidget,
|
||||
getNewWorkItemAutoSaveKey,
|
||||
newWorkItemFullPath,
|
||||
} from '../utils';
|
||||
import {
|
||||
WIDGET_TYPE_ASSIGNEES,
|
||||
WIDGET_TYPE_COLOR,
|
||||
|
|
@ -15,7 +20,6 @@ import {
|
|||
WIDGET_TYPE_CRM_CONTACTS,
|
||||
WIDGET_TYPE_ITERATION,
|
||||
WIDGET_TYPE_WEIGHT,
|
||||
WIDGET_TYPE_START_AND_DUE_DATE,
|
||||
NEW_WORK_ITEM_IID,
|
||||
WIDGET_TYPE_MILESTONE,
|
||||
WIDGET_TYPE_HIERARCHY,
|
||||
|
|
@ -44,7 +48,7 @@ const updateDatesWidget = (draftData, dates) => {
|
|||
const dueDate = dates.dueDate ? toISODateFormat(newDate(dates.dueDate)) : null;
|
||||
const startDate = dates.startDate ? toISODateFormat(newDate(dates.startDate)) : null;
|
||||
|
||||
const widget = findWidget(WIDGET_TYPE_START_AND_DUE_DATE, draftData.workspace.workItem);
|
||||
const widget = findStartAndDueDateWidget(draftData.workspace.workItem);
|
||||
Object.assign(widget, {
|
||||
dueDate,
|
||||
startDate,
|
||||
|
|
@ -57,7 +61,7 @@ const updateDatesWidget = (draftData, dates) => {
|
|||
const updateCustomFieldsWidget = (sourceData, draftData, customField) => {
|
||||
if (!customField) return;
|
||||
|
||||
const widget = findWidget(WIDGET_TYPE_CUSTOM_FIELDS, draftData.workspace.workItem);
|
||||
const widget = findCustomFieldsWidget(draftData.workspace.workItem);
|
||||
|
||||
if (!widget) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,37 +1,42 @@
|
|||
import { escapeRegExp } from 'lodash';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { queryToObject, joinPaths } from '~/lib/utils/url_utility';
|
||||
import { joinPaths, queryToObject } from '~/lib/utils/url_utility';
|
||||
import AccessorUtilities from '~/lib/utils/accessor';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
|
||||
|
||||
import {
|
||||
DEFAULT_PAGE_SIZE_CHILD_ITEMS,
|
||||
ISSUABLE_EPIC,
|
||||
NAME_TO_ENUM_MAP,
|
||||
NEW_WORK_ITEM_GID,
|
||||
NEW_WORK_ITEM_IID,
|
||||
STATE_CLOSED,
|
||||
WIDGET_TYPE_ASSIGNEES,
|
||||
WIDGET_TYPE_AWARD_EMOJI,
|
||||
WIDGET_TYPE_COLOR,
|
||||
WIDGET_TYPE_CRM_CONTACTS,
|
||||
WIDGET_TYPE_CURRENT_USER_TODOS,
|
||||
WIDGET_TYPE_CUSTOM_FIELDS,
|
||||
WIDGET_TYPE_DESCRIPTION,
|
||||
WIDGET_TYPE_DESIGNS,
|
||||
WIDGET_TYPE_DEVELOPMENT,
|
||||
WIDGET_TYPE_EMAIL_PARTICIPANTS,
|
||||
WIDGET_TYPE_ERROR_TRACKING,
|
||||
WIDGET_TYPE_HEALTH_STATUS,
|
||||
WIDGET_TYPE_HIERARCHY,
|
||||
WIDGET_TYPE_ITERATION,
|
||||
WIDGET_TYPE_LABELS,
|
||||
WIDGET_TYPE_LINKED_ITEMS,
|
||||
WIDGET_TYPE_MILESTONE,
|
||||
WIDGET_TYPE_NOTES,
|
||||
WIDGET_TYPE_START_AND_DUE_DATE,
|
||||
WIDGET_TYPE_STATUS,
|
||||
WIDGET_TYPE_TIME_TRACKING,
|
||||
WIDGET_TYPE_VULNERABILITIES,
|
||||
WIDGET_TYPE_WEIGHT,
|
||||
ISSUABLE_EPIC,
|
||||
WORK_ITEMS_TYPE_MAP,
|
||||
WORK_ITEM_TYPE_ENUM_EPIC,
|
||||
WORK_ITEM_TYPE_ROUTE_WORK_ITEM,
|
||||
NEW_WORK_ITEM_GID,
|
||||
DEFAULT_PAGE_SIZE_CHILD_ITEMS,
|
||||
STATE_CLOSED,
|
||||
NAME_TO_ENUM_MAP,
|
||||
WIDGET_TYPE_DEVELOPMENT,
|
||||
WORK_ITEMS_TYPE_MAP,
|
||||
} from './constants';
|
||||
|
||||
export const isAssigneesWidget = (widget) => widget.type === WIDGET_TYPE_ASSIGNEES;
|
||||
|
|
@ -40,23 +45,35 @@ export const isMilestoneWidget = (widget) => widget.type === WIDGET_TYPE_MILESTO
|
|||
|
||||
export const isNotesWidget = (widget) => widget.type === WIDGET_TYPE_NOTES;
|
||||
|
||||
export const findAssigneesWidget = (workItem) =>
|
||||
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_ASSIGNEES);
|
||||
|
||||
export const findAwardEmojiWidget = (workItem) =>
|
||||
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_AWARD_EMOJI);
|
||||
|
||||
export const findColorWidget = (workItem) =>
|
||||
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_COLOR);
|
||||
|
||||
export const findCrmContactsWidget = (workItem) =>
|
||||
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_CRM_CONTACTS);
|
||||
|
||||
export const findCurrentUserTodosWidget = (workItem) =>
|
||||
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_CURRENT_USER_TODOS);
|
||||
|
||||
export const findCustomFieldsWidget = (workItem) =>
|
||||
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_CUSTOM_FIELDS);
|
||||
|
||||
export const findDescriptionWidget = (workItem) =>
|
||||
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESCRIPTION);
|
||||
|
||||
export const findDesignsWidget = (workItem) =>
|
||||
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESIGNS);
|
||||
|
||||
export const findDevelopmentWidget = (workItem) =>
|
||||
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DEVELOPMENT);
|
||||
|
||||
export const findDesignsWidget = (workItem) =>
|
||||
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESIGNS);
|
||||
export const findEmailParticipantsWidget = (workItem) =>
|
||||
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_EMAIL_PARTICIPANTS);
|
||||
|
||||
export const findErrorTrackingWidget = (workItem) =>
|
||||
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_ERROR_TRACKING);
|
||||
|
|
@ -67,6 +84,9 @@ export const findHealthStatusWidget = (workItem) =>
|
|||
export const findHierarchyWidget = (workItem) =>
|
||||
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY);
|
||||
|
||||
export const findIterationWidget = (workItem) =>
|
||||
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_ITERATION);
|
||||
|
||||
export const findLabelsWidget = (workItem) =>
|
||||
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
|
||||
|
||||
|
|
@ -82,9 +102,15 @@ export const findNotesWidget = (workItem) =>
|
|||
export const findStartAndDueDateWidget = (workItem) =>
|
||||
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_START_AND_DUE_DATE);
|
||||
|
||||
export const findStatusWidget = (workItem) =>
|
||||
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_STATUS);
|
||||
|
||||
export const findTimeTrackingWidget = (workItem) =>
|
||||
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_TIME_TRACKING);
|
||||
|
||||
export const findVulnerabilitiesWidget = (workItem) =>
|
||||
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_VULNERABILITIES);
|
||||
|
||||
export const findWeightWidget = (workItem) =>
|
||||
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_WEIGHT);
|
||||
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ module Groups
|
|||
|
||||
crm_enabled = params.delete(:crm_enabled)
|
||||
crm_enabled = true if crm_enabled.nil?
|
||||
crm_source_group_id = params.delete(:crm_source_group_id)
|
||||
crm_source_group_id = params.delete(:crm_source_group_id).presence
|
||||
return if group.crm_enabled? == crm_enabled && group.crm_settings&.source_group_id == crm_source_group_id
|
||||
|
||||
if group.crm_settings&.source_group_id != crm_source_group_id && group.has_issues_with_contacts?
|
||||
|
|
@ -154,7 +154,7 @@ module Groups
|
|||
|
||||
crm_settings = group.crm_settings || group.build_crm_settings
|
||||
crm_settings.enabled = crm_enabled
|
||||
crm_settings.source_group_id = crm_source_group_id.presence
|
||||
crm_settings.source_group_id = crm_source_group_id
|
||||
crm_settings.save
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ module Projects
|
|||
attr_reader :current_user, :params
|
||||
|
||||
def user_permitted?
|
||||
Ability.allowed?(current_user, :admin_project, project)
|
||||
current_user.can?(:import_projects, project.namespace) || current_user.can?(:admin_project, project)
|
||||
end
|
||||
|
||||
def relation_valid?
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ module Projects
|
|||
relation_factory: Gitlab::ImportExport::Project::RelationFactory,
|
||||
reader: Gitlab::ImportExport::Reader.new(shared: project.import_export_shared),
|
||||
importable: project,
|
||||
importable_attributes: relation_reader.consume_attributes('project'),
|
||||
importable_attributes: {},
|
||||
importable_path: 'project',
|
||||
skip_on_duplicate_iid: true
|
||||
)
|
||||
|
|
@ -113,12 +113,15 @@ module Projects
|
|||
Gitlab::ImportExport::MembersMapper.new(
|
||||
exported_members: project_members,
|
||||
user: current_user,
|
||||
importable: project
|
||||
importable: project,
|
||||
default_member: false
|
||||
)
|
||||
end
|
||||
|
||||
def perform_post_import_tasks
|
||||
project.reset_counters_and_iids
|
||||
# Issue iids are scoped on :namespace so that scope must be flushed as well
|
||||
InternalId.flush_records!(namespace: project.project_namespace)
|
||||
end
|
||||
|
||||
def log_failure(exception)
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ module Gitlab
|
|||
require_dependency Rails.root.join('lib/gitlab/middleware/handle_malformed_strings')
|
||||
require_dependency Rails.root.join('lib/gitlab/middleware/path_traversal_check')
|
||||
require_dependency Rails.root.join('lib/gitlab/middleware/rack_multipart_tempfile_factory')
|
||||
require_dependency Rails.root.join('lib/gitlab/middleware/secure_headers')
|
||||
require_dependency Rails.root.join('lib/gitlab/runtime')
|
||||
require_dependency Rails.root.join('lib/gitlab/patch/database_config')
|
||||
require_dependency Rails.root.join('lib/gitlab/patch/redis_cache_store')
|
||||
|
|
@ -444,6 +445,8 @@ module Gitlab
|
|||
|
||||
config.middleware.insert_before Rack::Runtime, ::Gitlab::Middleware::CompressedJson
|
||||
|
||||
config.middleware.insert_after ActionDispatch::Cookies, ::Gitlab::Middleware::SecureHeaders
|
||||
|
||||
# Allow access to GitLab API from other domains
|
||||
config.middleware.insert_before Warden::Manager, Rack::Cors do
|
||||
headers_to_expose = %w[Link X-Total X-Total-Pages X-Per-Page X-Page X-Next-Page X-Prev-Page X-Gitlab-Blob-Id X-Gitlab-Commit-Id X-Gitlab-Content-Sha256 X-Gitlab-Encoding X-Gitlab-File-Name X-Gitlab-File-Path X-Gitlab-Last-Commit-Id X-Gitlab-Ref X-Gitlab-Size X-Request-Id ETag]
|
||||
|
|
|
|||
|
|
@ -37,6 +37,12 @@ en:
|
|||
grafana_enabled: "Grafana integration enabled"
|
||||
service_desk_setting:
|
||||
project_key: "Project name suffix"
|
||||
system_access/group_microsoft_application:
|
||||
tenant_xid: "Tenant ID"
|
||||
client_xid: "Client ID"
|
||||
encrypted_client_secret: "Client secret"
|
||||
login_endpoint: "Login API endpoint"
|
||||
graph_endpoint: "Graph API endpoint"
|
||||
system_access/microsoft_application:
|
||||
tenant_xid: "Tenant ID"
|
||||
client_xid: "Client ID"
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ Settings = GitlabSettings.load(file, Rails.env) do
|
|||
# generate a hash of the password:
|
||||
# https://github.com/attr-encrypted/encryptor/blob/c3a62c4a9e74686dd95e0548f9dc2a361fdc95d1/lib/encryptor.rb#L77
|
||||
def db_key_base_keys
|
||||
Array(Gitlab::Application.credentials.db_key_base).tap do |keys|
|
||||
@db_key_base_keys ||= Array(Gitlab::Application.credentials.db_key_base).tap do |keys|
|
||||
raise(MultipleDbKeyBaseError, "Defining multiple `db_key_base` keys isn't supported yet.") if keys.size > 1
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@ feature_category: system_access
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/177531
|
||||
milestone: '17.9'
|
||||
queued_migration_version: 20250127052138
|
||||
finalized_by: # version of the migration that finalized this BBM
|
||||
finalized_by: 20250416005514
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DropNotNullConstraintFromProjectFingerprint < Gitlab::Database::Migration[2.2]
|
||||
disable_ddl_transaction!
|
||||
milestone '18.0'
|
||||
def up
|
||||
change_column_null :vulnerability_occurrences, :project_fingerprint, true
|
||||
change_column_null :vulnerability_feedback, :project_fingerprint, true
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FinalizeSplitMicrosoftApplicationsTable < Gitlab::Database::Migration[2.2]
|
||||
milestone '18.0'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
MIGRATION = "SplitMicrosoftApplicationsTable"
|
||||
|
||||
def up
|
||||
ensure_batched_background_migration_is_finished(
|
||||
job_class_name: MIGRATION,
|
||||
table_name: :system_access_microsoft_applications,
|
||||
column_name: :id,
|
||||
job_arguments: [],
|
||||
finalize: true
|
||||
)
|
||||
end
|
||||
|
||||
def down; end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
380c2a8f4619d082324e81b281becd510b89391458c4dd5a6e5767a47d50e1e8
|
||||
|
|
@ -0,0 +1 @@
|
|||
633ac4a40eee2fc8d4bdfeca223e8308cfe68d6fea52f7b84abdbfd1975efdbc
|
||||
|
|
@ -25026,7 +25026,7 @@ CREATE TABLE vulnerability_feedback (
|
|||
project_id bigint NOT NULL,
|
||||
author_id bigint NOT NULL,
|
||||
issue_id bigint,
|
||||
project_fingerprint character varying(40) NOT NULL,
|
||||
project_fingerprint character varying(40),
|
||||
merge_request_id bigint,
|
||||
comment_author_id bigint,
|
||||
comment text,
|
||||
|
|
@ -25333,7 +25333,7 @@ CREATE TABLE vulnerability_occurrences (
|
|||
project_id bigint NOT NULL,
|
||||
scanner_id bigint NOT NULL,
|
||||
primary_identifier_id bigint NOT NULL,
|
||||
project_fingerprint bytea NOT NULL,
|
||||
project_fingerprint bytea,
|
||||
location_fingerprint bytea NOT NULL,
|
||||
name character varying NOT NULL,
|
||||
metadata_version character varying NOT NULL,
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ GET /projects/:id/feature_flags_user_lists
|
|||
| `search` | string | no | Return user lists matching the search criteria. |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists"
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
@ -87,10 +88,11 @@ POST /projects/:id/feature_flags_user_lists
|
|||
| `user_xids` | string | yes | A comma-separated list of external user IDs. |
|
||||
|
||||
```shell
|
||||
curl "https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists" \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--header "Content-type: application/json" \
|
||||
--data @- << EOF
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--header "Content-type: application/json" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists" \
|
||||
--data @- << EOF
|
||||
{
|
||||
"name": "my_user_list",
|
||||
"user_xids": "user1,user2,user3"
|
||||
|
|
@ -126,7 +128,8 @@ GET /projects/:id/feature_flags_user_lists/:iid
|
|||
| `iid` | integer/string | yes | The internal ID of the project's feature flag user list. |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists/1"
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists/1"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
@ -159,11 +162,11 @@ PUT /projects/:id/feature_flags_user_lists/:iid
|
|||
| `user_xids` | string | no | A comma-separated list of external user IDs. |
|
||||
|
||||
```shell
|
||||
curl "https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists/1" \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--header "Content-type: application/json" \
|
||||
--request PUT \
|
||||
--data @- << EOF
|
||||
curl --request PUT \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--header "Content-type: application/json" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists/1" \
|
||||
--data @- << EOF
|
||||
{
|
||||
"user_xids": "user2,user3,user4"
|
||||
}
|
||||
|
|
@ -198,5 +201,7 @@ DELETE /projects/:id/feature_flags_user_lists/:iid
|
|||
| `iid` | integer/string | yes | The internal ID of the project's feature flag user list |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" --request DELETE "https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists/1"
|
||||
curl --request DELETE \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists/1"
|
||||
```
|
||||
|
|
|
|||
|
|
@ -20974,14 +20974,28 @@ Requires ClickHouse. Premium and Ultimate with GitLab Duo Pro and Enterprise onl
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="aimetricscodecontributorscount"></a>`codeContributorsCount` | [`Int`](#int) | Number of code contributors. |
|
||||
| <a id="aimetricscodesuggestionsacceptedcount"></a>`codeSuggestionsAcceptedCount` | [`Int`](#int) | Total count of code suggestions accepted by code contributors. |
|
||||
| <a id="aimetricscodesuggestionscontributorscount"></a>`codeSuggestionsContributorsCount` | [`Int`](#int) | Number of code contributors who used GitLab Duo Code Suggestions features. |
|
||||
| <a id="aimetricscodesuggestionsshowncount"></a>`codeSuggestionsShownCount` | [`Int`](#int) | Total count of code suggestions shown to code contributors. |
|
||||
| <a id="aimetricscodesuggestionsacceptedcount"></a>`codeSuggestionsAcceptedCount` {{< icon name="warning-solid" >}} | [`Int`](#int) | **Deprecated** in GitLab 18.0. moved to codeSuggestions field. |
|
||||
| <a id="aimetricscodesuggestionscontributorscount"></a>`codeSuggestionsContributorsCount` {{< icon name="warning-solid" >}} | [`Int`](#int) | **Deprecated** in GitLab 18.0. moved to codeSuggestions field. |
|
||||
| <a id="aimetricscodesuggestionsshowncount"></a>`codeSuggestionsShownCount` {{< icon name="warning-solid" >}} | [`Int`](#int) | **Deprecated** in GitLab 18.0. moved to codeSuggestions field. |
|
||||
| <a id="aimetricsduoassigneduserscount"></a>`duoAssignedUsersCount` | [`Int`](#int) | Total assigned Duo Pro and Enterprise seats. Ignores time period filter. Returns current data. |
|
||||
| <a id="aimetricsduochatcontributorscount"></a>`duoChatContributorsCount` | [`Int`](#int) | Number of contributors who used GitLab Duo Chat features. |
|
||||
| <a id="aimetricsduousedcount"></a>`duoUsedCount` | [`Int`](#int) | Number of contributors who used any GitLab Duo feature. |
|
||||
| <a id="aimetricsrootcauseanalysisuserscount"></a>`rootCauseAnalysisUsersCount` | [`Int`](#int) | Number of users using troubleshoot within a failed pipeline. |
|
||||
|
||||
#### Fields with arguments
|
||||
|
||||
##### `AiMetrics.codeSuggestions`
|
||||
|
||||
Code suggestions metrics.
|
||||
|
||||
Returns [`codeSuggestionMetrics`](#codesuggestionmetrics).
|
||||
|
||||
###### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="aimetricscodesuggestionslanguages"></a>`languages` | [`[String!]`](#string) | Filter code suggestion metrics by one or more languages. |
|
||||
|
||||
### `AiSelfHostedModel`
|
||||
|
||||
Self-hosted LLM servers.
|
||||
|
|
@ -34384,7 +34398,6 @@ Represents vulnerability finding of a security report on the pipeline.
|
|||
| <a id="pipelinesecurityreportfindinglocation"></a>`location` | [`VulnerabilityLocation`](#vulnerabilitylocation) | Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability. |
|
||||
| <a id="pipelinesecurityreportfindingmergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request that fixes the vulnerability. |
|
||||
| <a id="pipelinesecurityreportfindingproject"></a>`project` | [`Project`](#project) | Project on which the vulnerability finding was found. |
|
||||
| <a id="pipelinesecurityreportfindingprojectfingerprint"></a>`projectFingerprint` {{< icon name="warning-solid" >}} | [`String`](#string) | **Deprecated** in GitLab 16.1. Use uuid instead. |
|
||||
| <a id="pipelinesecurityreportfindingremediations"></a>`remediations` | [`[VulnerabilityRemediationType!]`](#vulnerabilityremediationtype) | Remediations of the security report finding. |
|
||||
| <a id="pipelinesecurityreportfindingreporttype"></a>`reportType` | [`VulnerabilityReportType`](#vulnerabilityreporttype) | Type of the security report that found the vulnerability finding. |
|
||||
| <a id="pipelinesecurityreportfindingscanner"></a>`scanner` | [`VulnerabilityScanner`](#vulnerabilityscanner) | Scanner metadata for the vulnerability. |
|
||||
|
|
@ -41829,6 +41842,21 @@ X.509 signature for a signed commit.
|
|||
| <a id="x509signatureverificationstatus"></a>`verificationStatus` | [`VerificationStatus`](#verificationstatus) | Indicates verification status of the associated key or certificate. |
|
||||
| <a id="x509signaturex509certificate"></a>`x509Certificate` | [`X509Certificate`](#x509certificate) | Certificate used for the signature. |
|
||||
|
||||
### `codeSuggestionMetrics`
|
||||
|
||||
Requires ClickHouse. Premium and Ultimate with GitLab Duo Pro and Enterprise only.
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="codesuggestionmetricsacceptedcount"></a>`acceptedCount` | [`Int`](#int) | Total count of code suggestions accepted. |
|
||||
| <a id="codesuggestionmetricsacceptedlinesofcode"></a>`acceptedLinesOfCode` | [`Int`](#int) | Sum of lines of code from code suggestions accepted. |
|
||||
| <a id="codesuggestionmetricscontributorscount"></a>`contributorsCount` | [`Int`](#int) | Number of code contributors who used GitLab Duo Code Suggestions features. |
|
||||
| <a id="codesuggestionmetricslanguages"></a>`languages` | [`[String!]`](#string) | List of languages with at least one suggestion shown or accepted. |
|
||||
| <a id="codesuggestionmetricsshowncount"></a>`shownCount` | [`Int`](#int) | Total count of code suggestions shown. |
|
||||
| <a id="codesuggestionmetricsshownlinesofcode"></a>`shownLinesOfCode` | [`Int`](#int) | Sum of lines of code from code suggestions shown. |
|
||||
|
||||
## Enumeration types
|
||||
|
||||
Also called _Enums_, enumeration types are a special kind of scalar that
|
||||
|
|
|
|||
|
|
@ -83,7 +83,6 @@ Example response:
|
|||
"metadata_version": "2.0",
|
||||
"name": "Regular Expression Denial of Service in debug",
|
||||
"primary_identifier_id": 135,
|
||||
"project_fingerprint": "05e7cc9978ca495cf739a9f707ed34811e41c615",
|
||||
"project_id": 24,
|
||||
"raw_metadata": "{\"category\":\"dependency_scanning\",\"name\":\"Regular Expression Denial of Service\",\"message\":\"Regular Expression Denial of Service in debug\",\"description\":\"The debug module is vulnerable to regular expression denial of service when untrusted user input is passed into the `o` formatter. It takes around 50k characters to block for 2 seconds making this a low severity issue.\",\"cve\":\"yarn.lock:debug:gemnasium:37283ed4-0380-40d7-ada7-2d994afcc62a\",\"severity\":\"Unknown\",\"solution\":\"Upgrade to latest versions.\",\"scanner\":{\"id\":\"gemnasium\",\"name\":\"Gemnasium\"},\"location\":{\"file\":\"yarn.lock\",\"dependency\":{\"package\":{\"name\":\"debug\"},\"version\":\"1.0.5\"}},\"identifiers\":[{\"type\":\"gemnasium\",\"name\":\"Gemnasium-37283ed4-0380-40d7-ada7-2d994afcc62a\",\"value\":\"37283ed4-0380-40d7-ada7-2d994afcc62a\",\"url\":\"https://deps.sec.gitlab.com/packages/npm/debug/versions/1.0.5/advisories\"}],\"links\":[{\"url\":\"https://nodesecurity.io/advisories/534\"},{\"url\":\"https://github.com/visionmedia/debug/issues/501\"},{\"url\":\"https://github.com/visionmedia/debug/pull/504\"}],\"remediations\":[null]}",
|
||||
"report_type": "dependency_scanning",
|
||||
|
|
@ -166,7 +165,6 @@ Example response:
|
|||
"metadata_version": "2.0",
|
||||
"name": "Regular Expression Denial of Service in debug",
|
||||
"primary_identifier_id": 135,
|
||||
"project_fingerprint": "05e7cc9978ca495cf739a9f707ed34811e41c615",
|
||||
"project_id": 24,
|
||||
"raw_metadata": "{\"category\":\"dependency_scanning\",\"name\":\"Regular Expression Denial of Service\",\"message\":\"Regular Expression Denial of Service in debug\",\"description\":\"The debug module is vulnerable to regular expression denial of service when untrusted user input is passed into the `o` formatter. It takes around 50k characters to block for 2 seconds making this a low severity issue.\",\"cve\":\"yarn.lock:debug:gemnasium:37283ed4-0380-40d7-ada7-2d994afcc62a\",\"severity\":\"Unknown\",\"solution\":\"Upgrade to latest versions.\",\"scanner\":{\"id\":\"gemnasium\",\"name\":\"Gemnasium\"},\"location\":{\"file\":\"yarn.lock\",\"dependency\":{\"package\":{\"name\":\"debug\"},\"version\":\"1.0.5\"}},\"identifiers\":[{\"type\":\"gemnasium\",\"name\":\"Gemnasium-37283ed4-0380-40d7-ada7-2d994afcc62a\",\"value\":\"37283ed4-0380-40d7-ada7-2d994afcc62a\",\"url\":\"https://deps.sec.gitlab.com/packages/npm/debug/versions/1.0.5/advisories\"}],\"links\":[{\"url\":\"https://nodesecurity.io/advisories/534\"},{\"url\":\"https://github.com/visionmedia/debug/issues/501\"},{\"url\":\"https://github.com/visionmedia/debug/pull/504\"}],\"remediations\":[null]}",
|
||||
"report_type": "dependency_scanning",
|
||||
|
|
|
|||
|
|
@ -92,7 +92,6 @@ Example response:
|
|||
"url": "https://brakemanscanner.org/docs/warning_types/command_injection/"
|
||||
}
|
||||
],
|
||||
"project_fingerprint": "ac218b1770af030cfeef967752ab803c55afb36d",
|
||||
"uuid": "ad5e3be3-a193-55f5-a200-bc12865fb09c",
|
||||
"create_jira_issue_url": null,
|
||||
"false_positive": true,
|
||||
|
|
|
|||
|
|
@ -5346,7 +5346,7 @@ In this example, GitLab launches two containers for the job:
|
|||
|
||||
- A Ruby container that runs the `script` commands.
|
||||
- A PostgreSQL container. The `script` commands in the Ruby container can connect to
|
||||
the PostgreSQL database at the `db-postgrest` hostname.
|
||||
the PostgreSQL database at the `db-postgres` hostname.
|
||||
|
||||
**Related topics**:
|
||||
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ positives.
|
|||
| `ADDITIONAL_CA_CERT_BUNDLE` | `""` | Bundle of CA certs that you want to trust. See [Using a custom SSL CA certificate authority](#using-a-custom-ssl-ca-certificate-authority) for more details. |
|
||||
| `CI_APPLICATION_REPOSITORY` | `$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG` | Docker repository URL for the image to be scanned. |
|
||||
| `CI_APPLICATION_TAG` | `$CI_COMMIT_SHA` | Docker repository tag for the image to be scanned. |
|
||||
| `CS_ANALYZER_IMAGE` | `registry.gitlab.com/security-products/container-scanning:7` | Docker image of the analyzer. Do not use the `:latest` tag with analyzer images provided by GitLab. |
|
||||
| `CS_ANALYZER_IMAGE` | `registry.gitlab.com/security-products/container-scanning:8` | Docker image of the analyzer. Do not use the `:latest` tag with analyzer images provided by GitLab. |
|
||||
| `CS_DEFAULT_BRANCH_IMAGE` | `""` | The name of the `CS_IMAGE` on the default branch. See [Setting the default branch image](#setting-the-default-branch-image) for more details. |
|
||||
| `CS_DISABLE_DEPENDENCY_LIST` | `"false"` | {{< icon name="warning" >}} **[Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/439782)** in GitLab 17.0. |
|
||||
| `CS_DISABLE_LANGUAGE_VULNERABILITY_SCAN` | `"true"` | Disable scanning for language-specific packages installed in the scanned image. |
|
||||
|
|
@ -548,8 +548,8 @@ For container scanning, import the following images from `registry.gitlab.com` i
|
|||
[local Docker container registry](../../packages/container_registry/_index.md):
|
||||
|
||||
```plaintext
|
||||
registry.gitlab.com/security-products/container-scanning:7
|
||||
registry.gitlab.com/security-products/container-scanning/trivy:7
|
||||
registry.gitlab.com/security-products/container-scanning:8
|
||||
registry.gitlab.com/security-products/container-scanning/trivy:8
|
||||
```
|
||||
|
||||
The process for importing Docker images into a local offline Docker registry depends on
|
||||
|
|
@ -589,7 +589,7 @@ following `.gitlab-ci.yml` example as a template.
|
|||
|
||||
```yaml
|
||||
variables:
|
||||
SOURCE_IMAGE: registry.gitlab.com/security-products/container-scanning:7
|
||||
SOURCE_IMAGE: registry.gitlab.com/security-products/container-scanning:8
|
||||
TARGET_IMAGE: $CI_REGISTRY/namespace/container-scanning
|
||||
|
||||
image: docker:latest
|
||||
|
|
|
|||
|
|
@ -1169,12 +1169,12 @@ To use dependency scanning with all [supported languages and frameworks](#suppor
|
|||
your [local Docker container registry](../../packages/container_registry/_index.md):
|
||||
|
||||
```plaintext
|
||||
registry.gitlab.com/security-products/gemnasium:5
|
||||
registry.gitlab.com/security-products/gemnasium:5-fips
|
||||
registry.gitlab.com/security-products/gemnasium-maven:5
|
||||
registry.gitlab.com/security-products/gemnasium-maven:5-fips
|
||||
registry.gitlab.com/security-products/gemnasium-python:5
|
||||
registry.gitlab.com/security-products/gemnasium-python:5-fips
|
||||
registry.gitlab.com/security-products/gemnasium:6
|
||||
registry.gitlab.com/security-products/gemnasium:6-fips
|
||||
registry.gitlab.com/security-products/gemnasium-maven:6
|
||||
registry.gitlab.com/security-products/gemnasium-maven:6-fips
|
||||
registry.gitlab.com/security-products/gemnasium-python:6
|
||||
registry.gitlab.com/security-products/gemnasium-python:6-fips
|
||||
```
|
||||
|
||||
The process for importing Docker images into a local offline Docker registry depends on
|
||||
|
|
|
|||
|
|
@ -756,7 +756,7 @@ predefined project variable, not defined in the project's `.gitlab-ci.yml` file.
|
|||
|
||||
```yaml
|
||||
variables:
|
||||
CS_ANALYZER_IMAGE: "$CI_TEMPLATE_REGISTRY_HOST/security-products/container-scanning:7"
|
||||
CS_ANALYZER_IMAGE: "$CI_TEMPLATE_REGISTRY_HOST/security-products/container-scanning:8"
|
||||
CS_IMAGE: alpine:latest
|
||||
|
||||
policy::container-security:
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ module ActiveContext
|
|||
fields << Field::Keyword.new(name, index: true)
|
||||
end
|
||||
|
||||
def text(name)
|
||||
fields << Field::Text.new(name, index: false)
|
||||
end
|
||||
|
||||
def vector(name, dimensions:, index: true)
|
||||
fields << Field::Vector.new(name, dimensions: dimensions, index: index)
|
||||
end
|
||||
|
|
@ -32,6 +36,7 @@ module ActiveContext
|
|||
|
||||
class Bigint < Field; end
|
||||
class Keyword < Field; end
|
||||
class Text < Field; end
|
||||
class Vector < Field; end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ module ActiveContext
|
|||
{ type: 'long' }
|
||||
when Field::Keyword
|
||||
{ type: 'keyword' }
|
||||
when Field::Text
|
||||
{ type: 'text' }
|
||||
when Field::Vector
|
||||
vector_field_mapping(field)
|
||||
else
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ module ActiveContext
|
|||
# @raise [ArgumentError] If the query type is not supported
|
||||
def process(node)
|
||||
case node.type
|
||||
when :all then process_all
|
||||
when :filter then process_filter(node.value)
|
||||
when :prefix then process_prefix(node.value)
|
||||
when :or then process_or(node)
|
||||
|
|
@ -72,6 +73,10 @@ module ActiveContext
|
|||
|
||||
private
|
||||
|
||||
def process_all
|
||||
{ query: { match_all: {} } }
|
||||
end
|
||||
|
||||
# Processes filter conditions into term or terms queries
|
||||
#
|
||||
# @param conditions [Hash] The filter conditions where keys are fields and values are the terms
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ module ActiveContext
|
|||
when Field::Bigint
|
||||
# Bigint is 8 bytes
|
||||
fixed_columns << [field, 8]
|
||||
when Field::Keyword
|
||||
when Field::Keyword, Field::Text
|
||||
# Text fields are variable width
|
||||
variable_columns << field
|
||||
else
|
||||
|
|
@ -93,7 +93,7 @@ module ActiveContext
|
|||
table.column(field.name, "vector(#{field.options[:dimensions]})")
|
||||
when Field::Bigint
|
||||
table.bigint(field.name, **field.options.except(:index))
|
||||
when Field::Keyword
|
||||
when Field::Keyword, Field::Text
|
||||
table.text(field.name, **field.options.except(:index))
|
||||
else
|
||||
raise ArgumentError, "Unknown field type: #{field.class}"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ module ActiveContext
|
|||
# Processes a query node and returns the corresponding ActiveRecord relation
|
||||
def process(node)
|
||||
case node.type
|
||||
when :all then process_all
|
||||
when :filter then process_filter(node.value)
|
||||
when :prefix then process_prefix(node.value)
|
||||
when :and then process_and(node.children)
|
||||
|
|
@ -37,6 +38,10 @@ module ActiveContext
|
|||
|
||||
attr_reader :model, :base_relation
|
||||
|
||||
def process_all
|
||||
base_relation
|
||||
end
|
||||
|
||||
def process_filter(conditions)
|
||||
relation = base_relation
|
||||
conditions.each do |key, value|
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
# ActiveContext::Query.knn(target: "similarity", vector: [0.1, 0.2, 0.3], limit: 5)
|
||||
#
|
||||
# Supported Query Types:
|
||||
# - :all - Return all documents
|
||||
# - :filter - Exact match conditions
|
||||
# - :prefix - Prefix/starts-with conditions
|
||||
# - :limit - Restricts number of results
|
||||
|
|
@ -44,11 +45,15 @@
|
|||
|
||||
module ActiveContext
|
||||
class Query
|
||||
ALLOWED_TYPES = [:filter, :prefix, :limit, :knn, :and, :or].freeze
|
||||
ALLOWED_TYPES = [:all, :filter, :prefix, :limit, :knn, :and, :or].freeze
|
||||
SPACES_PER_INDENT = 2
|
||||
|
||||
class << self
|
||||
# Class methods to start the chain
|
||||
def all
|
||||
new(type: :all)
|
||||
end
|
||||
|
||||
def filter(**conditions)
|
||||
raise ArgumentError, "Filter cannot be empty" if conditions.empty?
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ RSpec.describe ActiveContext::Databases::Elasticsearch::Processor do
|
|||
|
||||
let(:simple_filter) { ActiveContext::Query.filter(status: 'active') }
|
||||
let(:simple_prefix) { ActiveContext::Query.prefix(name: 'test') }
|
||||
let(:simple_all) { ActiveContext::Query.all }
|
||||
let(:simple_knn) do
|
||||
ActiveContext::Query.knn(
|
||||
target: 'embedding',
|
||||
|
|
@ -379,5 +380,15 @@ RSpec.describe ActiveContext::Databases::Elasticsearch::Processor do
|
|||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with all queries' do
|
||||
it 'creates a match_all query' do
|
||||
result = processor.process(simple_all)
|
||||
|
||||
expect(result).to eq(
|
||||
query: { match_all: {} }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ RSpec.describe ActiveContext::Databases::Opensearch::Processor do
|
|||
|
||||
let(:simple_filter) { ActiveContext::Query.filter(status: 'active') }
|
||||
let(:simple_prefix) { ActiveContext::Query.prefix(name: 'test') }
|
||||
let(:simple_all) { ActiveContext::Query.all }
|
||||
let(:simple_knn) do
|
||||
ActiveContext::Query.knn(
|
||||
target: 'embedding',
|
||||
|
|
@ -369,5 +370,15 @@ RSpec.describe ActiveContext::Databases::Opensearch::Processor do
|
|||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with all queries' do
|
||||
it 'creates a match_all query' do
|
||||
result = processor.process(simple_all)
|
||||
|
||||
expect(result).to eq(
|
||||
query: { match_all: {} }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -182,4 +182,10 @@ RSpec.describe ActiveContext::Databases::Postgresql::Processor, feature_category
|
|||
"SELECT subq.* FROM (SELECT \"items\".* FROM \"items\" " \
|
||||
"ORDER BY \"embedding\" <=> '[0.1,0.2]' LIMIT 5) subq LIMIT 10"
|
||||
end
|
||||
|
||||
context 'with all queries' do
|
||||
it_behaves_like 'a SQL transformer',
|
||||
ActiveContext::Query.all,
|
||||
"SELECT \"items\".* FROM \"items\""
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,6 +2,15 @@
|
|||
|
||||
RSpec.describe ActiveContext::Query do
|
||||
describe 'class methods' do
|
||||
describe '.all' do
|
||||
it 'creates an all query' do
|
||||
query = described_class.all
|
||||
expect(query.type).to eq(:all)
|
||||
expect(query.value).to be_nil
|
||||
expect(query.children).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '.filter' do
|
||||
it 'creates a filter query with valid conditions' do
|
||||
query = described_class.filter(project_id: 1)
|
||||
|
|
@ -172,6 +181,18 @@ RSpec.describe ActiveContext::Query do
|
|||
end
|
||||
|
||||
describe '#inspect_ast' do
|
||||
it 'generates a readable AST representation for a simple all query' do
|
||||
query = described_class.all
|
||||
ast = query.inspect_ast
|
||||
expect(ast).to eq('all')
|
||||
end
|
||||
|
||||
it 'generates a readable AST representation for an all query with limit' do
|
||||
query = described_class.all.limit(10)
|
||||
ast = query.inspect_ast
|
||||
expect(ast).to eq("limit(10)\n all")
|
||||
end
|
||||
|
||||
it 'generates a readable AST representation for a simple filter query' do
|
||||
query = described_class.filter(project_id: 1)
|
||||
ast = query.inspect_ast
|
||||
|
|
@ -215,7 +236,7 @@ RSpec.describe ActiveContext::Query do
|
|||
it 'raises an error for invalid query type' do
|
||||
expect { described_class.new(type: :invalid) }.to raise_error(
|
||||
ArgumentError,
|
||||
/Invalid type: invalid\. Allowed types are: filter, prefix, limit, knn, and, or/
|
||||
/Invalid type: invalid\. Allowed types are: all, filter, prefix, limit, knn, and, or/
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -727,7 +727,8 @@ module API
|
|||
# @content_disposition controls the Content-Disposition response header. nil by default. Forced to attachment for object storage disabled mode.
|
||||
# @content_type controls the Content-Type response header. By default, it will rely on the 'application/octet-stream' value or the content type detected by carrierwave.
|
||||
# @extra_response_headers. Set additional response headers. Not used in the direct download supported case.
|
||||
def present_carrierwave_file!(file, supports_direct_download: true, content_disposition: nil, content_type: nil, extra_response_headers: {})
|
||||
# @extra_send_url_params. Additional parameters to send to workhorse send_url call. See Gitlab::Workhorse.send_url for more information
|
||||
def present_carrierwave_file!(file, supports_direct_download: true, content_disposition: nil, content_type: nil, extra_response_headers: {}, extra_send_url_params: {})
|
||||
return not_found! unless file&.exists?
|
||||
|
||||
if content_disposition
|
||||
|
|
@ -749,7 +750,7 @@ module API
|
|||
else
|
||||
response_headers = extra_response_headers.merge('Content-Type' => content_type, 'Content-Disposition' => response_disposition).compact_blank
|
||||
|
||||
header(*Gitlab::Workhorse.send_url(file.url, response_headers: response_headers))
|
||||
header(*Gitlab::Workhorse.send_url(file.url, response_headers: response_headers, **extra_send_url_params))
|
||||
status :ok
|
||||
body '' # to avoid an error from API::APIGuard::ResponseCoercerMiddleware
|
||||
end
|
||||
|
|
|
|||
|
|
@ -54,17 +54,18 @@ module API
|
|||
nil
|
||||
end
|
||||
|
||||
def download_package_file!(package_file)
|
||||
def download_package_file!(package_file, extra_send_url_params: {}, extra_response_headers: {})
|
||||
package_file.package.touch_last_downloaded_at
|
||||
file = package_file.file
|
||||
|
||||
extra_response_headers = { SHA1_CHECKSUM_HEADER => package_file.file_sha1 }
|
||||
extra_response_headers[MD5_CHECKSUM_HEADER] = package_file.file_md5 unless Gitlab::FIPS.enabled?
|
||||
headers = extra_response_headers.merge(SHA1_CHECKSUM_HEADER => package_file.file_sha1)
|
||||
headers[MD5_CHECKSUM_HEADER] = package_file.file_md5 unless Gitlab::FIPS.enabled?
|
||||
|
||||
present_carrierwave_file!(
|
||||
file,
|
||||
supports_direct_download: false, # we can't support direct download if we have custom response headers
|
||||
extra_response_headers: extra_response_headers
|
||||
extra_response_headers: headers,
|
||||
extra_send_url_params: extra_send_url_params
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,25 +2,23 @@
|
|||
|
||||
module Banzai
|
||||
module Filter
|
||||
# Filter which looks for possible paragraphs with quick action lines, and allows
|
||||
# another processor to do final determination. Paragraph source position
|
||||
# Filter which extracts top-level paragraph sourcepos, so
|
||||
# another processor can determine if it's a quick action. Paragraph source position
|
||||
# is returned in `result[:quick_action_paragraphs]`.
|
||||
class QuickActionFilter < HTML::Pipeline::Filter
|
||||
def call
|
||||
result[:quick_action_paragraphs] = []
|
||||
|
||||
doc.children.xpath('self::p').each do |node|
|
||||
# don't use `xpath` as it can take too long
|
||||
doc.children.each do |node|
|
||||
next unless node.name == 'p'
|
||||
next unless node.attributes['data-sourcepos']
|
||||
next unless %r{^/}.match?(node.content)
|
||||
|
||||
sourcepos = ::Banzai::Filter::MarkdownFilter.parse_sourcepos(node.attributes['data-sourcepos'].value)
|
||||
|
||||
node.children.xpath('self::text()').each do |text_node|
|
||||
next unless %r{^/}.match?(text_node.content)
|
||||
|
||||
result[:quick_action_paragraphs] <<
|
||||
{ start_line: sourcepos[:start][:row], end_line: sourcepos[:end][:row] }
|
||||
break
|
||||
end
|
||||
result[:quick_action_paragraphs] <<
|
||||
{ start_line: sourcepos[:start][:row], end_line: sourcepos[:end][:row] }
|
||||
end
|
||||
|
||||
doc
|
||||
|
|
|
|||
|
|
@ -18,7 +18,12 @@ module Banzai
|
|||
<p>Timeout while sanitizing links - rendering aborted. Please reduce the number of links if possible.</p>
|
||||
HTML
|
||||
|
||||
# We have seen documents with thousands of links
|
||||
MAX_LINK_ATTRIBUTES = 10000
|
||||
|
||||
def call
|
||||
raise Timeout::Error if doc.xpath(self.class::XPATH).count > MAX_LINK_ATTRIBUTES
|
||||
|
||||
doc.xpath(self.class::XPATH).each do |el|
|
||||
sanitize_unsafe_links({ node: el })
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ module Gitlab
|
|||
attr_reader :metadata_version
|
||||
attr_reader :name
|
||||
attr_reader :old_location
|
||||
attr_reader :project_fingerprint
|
||||
attr_reader :report_type
|
||||
attr_reader :scanner
|
||||
attr_reader :scan
|
||||
|
|
@ -55,8 +54,6 @@ module Gitlab
|
|||
@vulnerability_finding_signatures_enabled = vulnerability_finding_signatures_enabled
|
||||
@found_by_pipeline = found_by_pipeline
|
||||
@cvss = cvss
|
||||
|
||||
@project_fingerprint = generate_project_fingerprint
|
||||
end
|
||||
|
||||
def to_hash
|
||||
|
|
@ -69,7 +66,6 @@ module Gitlab
|
|||
evidence
|
||||
metadata_version
|
||||
name
|
||||
project_fingerprint
|
||||
raw_metadata
|
||||
report_type
|
||||
scanner
|
||||
|
|
@ -193,10 +189,6 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def generate_project_fingerprint
|
||||
Digest::SHA1.hexdigest(uuid.to_s)
|
||||
end
|
||||
|
||||
def location_fingerprints
|
||||
@location_fingerprints ||= signature_hexes << location&.fingerprint
|
||||
end
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
# List of available variables: https://docs.gitlab.com/ee/user/application_security/container_scanning/#available-variables
|
||||
|
||||
variables:
|
||||
CS_ANALYZER_IMAGE: "$CI_TEMPLATE_REGISTRY_HOST/security-products/container-scanning:7"
|
||||
CS_ANALYZER_IMAGE: "$CI_TEMPLATE_REGISTRY_HOST/security-products/container-scanning:8"
|
||||
CS_SCHEMA_MODEL: 15
|
||||
|
||||
container_scanning:
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ variables:
|
|||
# (SAST, Dependency Scanning, ...)
|
||||
AST_ENABLE_MR_PIPELINES: "true"
|
||||
#
|
||||
CS_ANALYZER_IMAGE: "$CI_TEMPLATE_REGISTRY_HOST/security-products/container-scanning:7"
|
||||
CS_ANALYZER_IMAGE: "$CI_TEMPLATE_REGISTRY_HOST/security-products/container-scanning:8"
|
||||
CS_SCHEMA_MODEL: 15
|
||||
|
||||
# Provide a base job for extensibility until delivery of https://gitlab.com/gitlab-org/gitlab/-/issues/215470
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ variables:
|
|||
#
|
||||
DS_EXCLUDED_ANALYZERS: ""
|
||||
DS_EXCLUDED_PATHS: "spec, test, tests, tmp"
|
||||
DS_MAJOR_VERSION: 5
|
||||
DS_MAJOR_VERSION: 6
|
||||
DS_SCHEMA_MODEL: 15
|
||||
|
||||
dependency_scanning:
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ variables:
|
|||
#
|
||||
DS_EXCLUDED_ANALYZERS: ""
|
||||
DS_EXCLUDED_PATHS: "spec, test, tests, tmp"
|
||||
DS_MAJOR_VERSION: 5
|
||||
DS_MAJOR_VERSION: 6
|
||||
DS_SCHEMA_MODEL: 15
|
||||
# Use this variable to enforce the new Dependency Scanning analyzer for all projects
|
||||
DS_ENFORCE_NEW_ANALYZER: 'false'
|
||||
|
|
|
|||
|
|
@ -183,21 +183,21 @@ kics:
|
|||
gemnasium:
|
||||
extends: .download_images
|
||||
variables:
|
||||
SECURE_BINARIES_ANALYZER_VERSION: "5"
|
||||
SECURE_BINARIES_ANALYZER_VERSION: "6"
|
||||
rules:
|
||||
- if: '$SECURE_BINARIES_DOWNLOAD_IMAGES == "true" && $SECURE_BINARIES_ANALYZERS =~ /\bgemnasium\b/'
|
||||
|
||||
gemnasium-maven:
|
||||
extends: .download_images
|
||||
variables:
|
||||
SECURE_BINARIES_ANALYZER_VERSION: "5"
|
||||
SECURE_BINARIES_ANALYZER_VERSION: "6"
|
||||
rules:
|
||||
- if: '$SECURE_BINARIES_DOWNLOAD_IMAGES == "true" && $SECURE_BINARIES_ANALYZERS =~ /\bgemnasium-maven\b/'
|
||||
|
||||
gemnasium-python:
|
||||
extends: .download_images
|
||||
variables:
|
||||
SECURE_BINARIES_ANALYZER_VERSION: "5"
|
||||
SECURE_BINARIES_ANALYZER_VERSION: "6"
|
||||
rules:
|
||||
- if: '$SECURE_BINARIES_DOWNLOAD_IMAGES == "true" && $SECURE_BINARIES_ANALYZERS =~ /\bgemnasium-python\b/'
|
||||
|
||||
|
|
|
|||
|
|
@ -3,14 +3,15 @@
|
|||
module Gitlab
|
||||
module ImportExport
|
||||
class MembersMapper
|
||||
def initialize(exported_members:, user:, importable:)
|
||||
def initialize(exported_members:, user:, importable:, default_member: true)
|
||||
@exported_members = user.admin? ? exported_members : []
|
||||
@user = user
|
||||
@importable = importable
|
||||
|
||||
# This needs to run first, as second call would be from #map
|
||||
# which means Project/Group members already exist.
|
||||
ensure_default_member!
|
||||
# Skip this when importing single relations to avoid destroying project members
|
||||
ensure_default_member! if default_member
|
||||
end
|
||||
|
||||
def map
|
||||
|
|
|
|||
|
|
@ -1318,7 +1318,6 @@ ee:
|
|||
- :report_type
|
||||
- :findings
|
||||
findings:
|
||||
- :project_fingerprint
|
||||
- :project_id
|
||||
- :location_fingerprint
|
||||
- :primary_identifier_fingerprint
|
||||
|
|
@ -1343,7 +1342,6 @@ ee:
|
|||
primary_identifier: *identifiers_definition
|
||||
vulnerability_finding:
|
||||
- :uuid
|
||||
- :project_fingerprint
|
||||
- :project_id
|
||||
- :location_fingerprint
|
||||
- :name
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Middleware
|
||||
class SecureHeaders
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
status, headers, body = @app.call(env)
|
||||
|
||||
# Remove NEL policy from the policy cache by setting max_age to 0.
|
||||
# https://w3c.github.io/network-error-logging/#the-max_age-member
|
||||
# https://w3c.github.io/network-error-logging/#example-2
|
||||
headers['NEL'] = '{"max_age": 0}'
|
||||
|
||||
[status, headers, body]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -187,7 +187,8 @@ module Gitlab
|
|||
timeouts: {},
|
||||
response_statuses: {},
|
||||
response_headers: {},
|
||||
allowed_uris: []
|
||||
allowed_uris: [],
|
||||
restrict_forwarded_response_headers: {}
|
||||
)
|
||||
params = {
|
||||
'URL' => url,
|
||||
|
|
@ -199,7 +200,7 @@ module Gitlab
|
|||
'Header' => headers.transform_values { |v| Array.wrap(v) },
|
||||
'ResponseHeaders' => response_headers.transform_values { |v| Array.wrap(v) },
|
||||
'Method' => method
|
||||
}.compact
|
||||
}.merge(restrict_forwarded_response_headers_params(restrict_forwarded_response_headers)).compact
|
||||
|
||||
if timeouts.present?
|
||||
params['DialTimeout'] = "#{timeouts[:open]}s" if timeouts[:open]
|
||||
|
|
@ -243,7 +244,8 @@ module Gitlab
|
|||
upload_config: {},
|
||||
response_headers: {},
|
||||
ssrf_filter: false,
|
||||
allowed_uris: [])
|
||||
allowed_uris: [],
|
||||
restrict_forwarded_response_headers: {})
|
||||
params = {
|
||||
'AllowLocalhost' => allow_localhost,
|
||||
'AllowedURIs' => allowed_uris,
|
||||
|
|
@ -257,9 +259,8 @@ module Gitlab
|
|||
'Headers' => (upload_config[:headers] || {}).transform_values { |v| Array.wrap(v) },
|
||||
'AuthorizedUploadResponse' => upload_config[:authorized_upload_response] || {}
|
||||
}.compact_blank!
|
||||
}
|
||||
}.merge(restrict_forwarded_response_headers_params(restrict_forwarded_response_headers))
|
||||
params.compact_blank!
|
||||
|
||||
[
|
||||
SEND_DATA_HEADER,
|
||||
"send-dependency:#{encode(params)}"
|
||||
|
|
@ -399,6 +400,16 @@ module Gitlab
|
|||
)
|
||||
}
|
||||
end
|
||||
|
||||
def restrict_forwarded_response_headers_params(params)
|
||||
params[:enabled] = false unless params.key?(:enabled)
|
||||
{
|
||||
'RestrictForwardedResponseHeaders' => {
|
||||
'Enabled' => params[:enabled],
|
||||
'AllowList' => params[:allow_list] || []
|
||||
}
|
||||
}.compact_blank!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@
|
|||
"@floating-ui/dom": "^1.2.9",
|
||||
"@gitlab/application-sdk-browser": "^0.3.4",
|
||||
"@gitlab/at.js": "1.5.7",
|
||||
"@gitlab/cluster-client": "^2.5.0",
|
||||
"@gitlab/cluster-client": "^3.0.0",
|
||||
"@gitlab/duo-ui": "^8.10.0",
|
||||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/fonts": "^1.3.0",
|
||||
|
|
@ -305,7 +305,7 @@
|
|||
"swagger-cli": "^4.0.4",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"timezone-mock": "^1.0.8",
|
||||
"vite": "^6.3.1",
|
||||
"vite": "^6.3.2",
|
||||
"vite-plugin-ruby": "^5.1.1",
|
||||
"vue-loader-vue3": "npm:vue-loader@17.4.2",
|
||||
"vue-test-utils-compat": "0.0.14",
|
||||
|
|
|
|||
|
|
@ -153,6 +153,21 @@ RSpec.describe Settings, feature_category: :system_access do
|
|||
allow(Gitlab::Application.credentials)
|
||||
.to receive(:db_key_base)
|
||||
.and_return(raw_keys)
|
||||
# Reset memoization
|
||||
described_class.instance_variable_set(:@db_key_base_keys, nil)
|
||||
end
|
||||
|
||||
describe 'memoization' do
|
||||
let(:raw_keys) { 'a' }
|
||||
|
||||
it 'memoizes the value' do
|
||||
db_key_base_keys = described_class.db_key_base_keys
|
||||
|
||||
expect(described_class.db_key_base_keys).to be(db_key_base_keys)
|
||||
|
||||
expect(Gitlab::Application.credentials)
|
||||
.to have_received(:db_key_base).once
|
||||
end
|
||||
end
|
||||
|
||||
context 'when db key base secret is a string' do
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{"project_id":5,"author_id":1,"title":"Regular expression with non-literal value","description":null,"severity":"medium","report_type":"sast","vulnerability_finding":{"severity":"medium","report_type":"sast","project_id":5,"project_fingerprint":"4ce7494840bb1882d5a9003b0f272f8e3e22c7a5","location_fingerprint":"4f7a2fffbb791c4cc8d1454db40b80f7fa9ed5be","name":"Regular expression with non-literal value","metadata_version":"15.1.4","raw_metadata":"{\"id\":\"b13b66b99eabefb8bc0d385b90cb952734e246ff3477a8ee563d6d04ef4bded4\",\"category\":\"sast\",\"name\":\"Regular expression with non-literal value\",\"description\":\"The `RegExp` constructor was called with a non-literal value. If an adversary were able to\\nsupply a malicious regex, they could cause a Regular Expression Denial of Service (ReDoS)\\nagainst the application. In Node applications, this could cause the entire application to no\\nlonger be responsive to other users' requests.\\n\\nTo remediate this issue, never allow user-supplied regular expressions. Instead, the regular \\nexpression should be hardcoded. If this is not possible, consider using an alternative regular\\nexpression engine such as [node-re2](https://www.npmjs.com/package/re2). RE2 is a safe alternative \\nthat does not support backtracking, which is what leads to ReDoS.\\n\\nExample using re2 which does not support backtracking (Note: it is still recommended to\\nnever use user-supplied input):\\n```\\n// Import the re2 module\\nconst RE2 = require('re2');\\n\\nfunction match(userSuppliedRegex, userInput) {\\n // Create a RE2 object with the user supplied regex, this is relatively safe\\n // due to RE2 not supporting backtracking which can be abused to cause long running\\n // queries\\n var re = new RE2(userSuppliedRegex);\\n // Execute the regular expression against some userInput\\n var result = re.exec(userInput);\\n // Work with the result\\n}\\n```\\n\\nFor more information on Regular Expression DoS see:\\n- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS\\n\",\"cve\":\"semgrep_id:eslint.detect-non-literal-regexp:515:515\",\"severity\":\"Medium\",\"scanner\":{\"id\":\"semgrep\",\"name\":\"Semgrep\"},\"location\":{\"file\":\"common/static/ace/ext-language_tools.js\",\"start_line\":515},\"identifiers\":[{\"type\":\"semgrep_id\",\"name\":\"eslint.detect-non-literal-regexp\",\"value\":\"eslint.detect-non-literal-regexp\",\"url\":\"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp\"},{\"type\":\"cwe\",\"name\":\"CWE-185\",\"value\":\"185\",\"url\":\"https://cwe.mitre.org/data/definitions/185.html\"},{\"type\":\"owasp\",\"name\":\"A03:2021 - Injection\",\"value\":\"A03:2021\"},{\"type\":\"owasp\",\"name\":\"A1:2017 - Injection\",\"value\":\"A1:2017\"},{\"type\":\"eslint_rule_id\",\"name\":\"ESLint rule ID/detect-non-literal-regexp\",\"value\":\"detect-non-literal-regexp\"}],\"tracking\":{\"type\":\"source\",\"items\":[{\"file\":\"common/static/ace/ext-language_tools.js\",\"line_start\":515,\"line_end\":515,\"signatures\":[{\"algorithm\":\"scope_offset\",\"value\":\"common/static/ace/ext-language_tools.js|func[0]:498\"}]}]}}","detection_method":"gitlab_security_report","uuid":"fa74cb01-2544-5d42-b9e8-0150119bf6cb","scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"},"primary_identifier":{"project_id":5,"fingerprint":"a751f35f1185de7ca5e6c0610c3bca21eb25ac9a","external_type":"semgrep_id","external_id":"eslint.detect-non-literal-regexp","name":"eslint.detect-non-literal-regexp","url":"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp"},"identifiers":[{"project_id":5,"fingerprint":"08de3511f2132da4d24f1b8b1d3ca14368a0259b","external_type":"owasp","external_id":"A1:2017","name":"A1:2017 - Injection","url":null},{"project_id":5,"fingerprint":"7153fe286fd77c7a6250aa9603b82d44ab1c31e4","external_type":"cwe","external_id":"185","name":"CWE-185","url":"https://cwe.mitre.org/data/definitions/185.html"},{"project_id":5,"fingerprint":"a15f44ab746431d58b21b4fc67d8c4d3fb160ca0","external_type":"eslint_rule_id","external_id":"detect-non-literal-regexp","name":"ESLint rule ID/detect-non-literal-regexp","url":null},{"project_id":5,"fingerprint":"a751f35f1185de7ca5e6c0610c3bca21eb25ac9a","external_type":"semgrep_id","external_id":"eslint.detect-non-literal-regexp","name":"eslint.detect-non-literal-regexp","url":"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp"},{"project_id":5,"fingerprint":"a8e828eea3aba35916401da9304619f0a218119b","external_type":"owasp","external_id":"A03:2021","name":"A03:2021 - Injection","url":null}],"initial_finding_pipeline":{"iid":438},"latest_finding_pipeline":{"iid":438}},"vulnerability_read":{"project_id":5,"scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"}}}
|
||||
{"project_id":5,"author_id":1,"title":"Incorrect regular expression","description":null,"severity":"medium","report_type":"sast","vulnerability_finding":{"severity":"medium","report_type":"sast","project_id":5,"project_fingerprint":"46e1dffeb673fa9e3de7343653b84dd9826e7312","location_fingerprint":"f866afbfc47ac0fae0da7c6df8e5ed35330e4384","name":"Incorrect regular expression","metadata_version":"15.1.4","raw_metadata":"{\"id\":\"0152dfdd49aa1b9636cd267c12d080250199f15f21f427d3bed1a07a002e011f\",\"category\":\"sast\",\"name\":\"Incorrect regular expression\",\"description\":\"Ensure that the regex used to compare with user supplied input is safe from regular expression denial of service.\\n\",\"cve\":\"semgrep_id:nodejs_scan.javascript-dos-rule-regex_dos:1050:1052\",\"severity\":\"Medium\",\"scanner\":{\"id\":\"semgrep\",\"name\":\"Semgrep\"},\"location\":{\"file\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.sortable.js\",\"start_line\":1050,\"end_line\":1052},\"identifiers\":[{\"type\":\"semgrep_id\",\"name\":\"nodejs_scan.javascript-dos-rule-regex_dos\",\"value\":\"nodejs_scan.javascript-dos-rule-regex_dos\"},{\"type\":\"cwe\",\"name\":\"CWE-185\",\"value\":\"185\",\"url\":\"https://cwe.mitre.org/data/definitions/185.html\"},{\"type\":\"owasp\",\"name\":\"A05:2021 - Security Misconfiguration\",\"value\":\"A05:2021\"},{\"type\":\"owasp\",\"name\":\"A6:2017 - Security Misconfiguration\",\"value\":\"A6:2017\"},{\"type\":\"njsscan_rule_type\",\"name\":\"NodeJS Scan ID javascript-dos-rule-regex_dos\",\"value\":\"Ensure that the regex used to compare with user supplied input is safe from regular expression denial of service.\"}],\"tracking\":{\"type\":\"source\",\"items\":[{\"file\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.sortable.js\",\"line_start\":1050,\"line_end\":1050,\"signatures\":[{\"algorithm\":\"scope_offset\",\"value\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.sortable.js|func($, undefined)[0]:1034\"}]}]}}","detection_method":"gitlab_security_report","uuid":"fa2589df-c1ad-5108-93f0-90237b17c1b1","scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"},"primary_identifier":{"project_id":5,"fingerprint":"ad9e1d2b073e1c296088e8fbedf8ed738d06f88a","external_type":"semgrep_id","external_id":"nodejs_scan.javascript-dos-rule-regex_dos","name":"nodejs_scan.javascript-dos-rule-regex_dos","url":null},"identifiers":[{"project_id":5,"fingerprint":"2bd02e525f0e78f8745e5a063ca1b5f396527a41","external_type":"owasp","external_id":"A6:2017","name":"A6:2017 - Security Misconfiguration","url":null},{"project_id":5,"fingerprint":"3f2c4e94cf8c0b53c44cb5b187963b753da9e882","external_type":"owasp","external_id":"A05:2021","name":"A05:2021 - Security Misconfiguration","url":null},{"project_id":5,"fingerprint":"518290ee3e47f4a5bba33213ca8a82e4c0d8697d","external_type":"njsscan_rule_type","external_id":"Ensure that the regex used to compare with user supplied input is safe from regular expression denial of service.","name":"NodeJS Scan ID javascript-dos-rule-regex_dos","url":null},{"project_id":5,"fingerprint":"7153fe286fd77c7a6250aa9603b82d44ab1c31e4","external_type":"cwe","external_id":"185","name":"CWE-185","url":"https://cwe.mitre.org/data/definitions/185.html"},{"project_id":5,"fingerprint":"ad9e1d2b073e1c296088e8fbedf8ed738d06f88a","external_type":"semgrep_id","external_id":"nodejs_scan.javascript-dos-rule-regex_dos","name":"nodejs_scan.javascript-dos-rule-regex_dos","url":null}],"initial_finding_pipeline":{"iid":438},"latest_finding_pipeline":{"iid":438}},"vulnerability_read":{"project_id":5,"scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"}}}
|
||||
{"project_id":5,"author_id":1,"title":"Regular expression with non-literal value","description":null,"severity":"medium","report_type":"sast","vulnerability_finding":{"severity":"medium","report_type":"sast","project_id":5,"project_fingerprint":"ea561c323d8e5e87040ad59ca2b926f2b005255c","location_fingerprint":"708aa3150b2b448e6894dd447689336d0ce63f19","name":"Regular expression with non-literal value","metadata_version":"15.1.4","raw_metadata":"{\"id\":\"f8c645cd515f94924c9a8fe73cc3e2bcf08b90ee9936462b6da57b6c28b52803\",\"category\":\"sast\",\"name\":\"Regular expression with non-literal value\",\"description\":\"The `RegExp` constructor was called with a non-literal value. If an adversary were able to\\nsupply a malicious regex, they could cause a Regular Expression Denial of Service (ReDoS)\\nagainst the application. In Node applications, this could cause the entire application to no\\nlonger be responsive to other users' requests.\\n\\nTo remediate this issue, never allow user-supplied regular expressions. Instead, the regular \\nexpression should be hardcoded. If this is not possible, consider using an alternative regular\\nexpression engine such as [node-re2](https://www.npmjs.com/package/re2). RE2 is a safe alternative \\nthat does not support backtracking, which is what leads to ReDoS.\\n\\nExample using re2 which does not support backtracking (Note: it is still recommended to\\nnever use user-supplied input):\\n```\\n// Import the re2 module\\nconst RE2 = require('re2');\\n\\nfunction match(userSuppliedRegex, userInput) {\\n // Create a RE2 object with the user supplied regex, this is relatively safe\\n // due to RE2 not supporting backtracking which can be abused to cause long running\\n // queries\\n var re = new RE2(userSuppliedRegex);\\n // Execute the regular expression against some userInput\\n var result = re.exec(userInput);\\n // Work with the result\\n}\\n```\\n\\nFor more information on Regular Expression DoS see:\\n- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS\\n\",\"cve\":\"semgrep_id:eslint.detect-non-literal-regexp:1108:1108\",\"severity\":\"Medium\",\"scanner\":{\"id\":\"semgrep\",\"name\":\"Semgrep\"},\"location\":{\"file\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.datepicker.js\",\"start_line\":1108},\"identifiers\":[{\"type\":\"semgrep_id\",\"name\":\"eslint.detect-non-literal-regexp\",\"value\":\"eslint.detect-non-literal-regexp\",\"url\":\"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp\"},{\"type\":\"cwe\",\"name\":\"CWE-185\",\"value\":\"185\",\"url\":\"https://cwe.mitre.org/data/definitions/185.html\"},{\"type\":\"owasp\",\"name\":\"A03:2021 - Injection\",\"value\":\"A03:2021\"},{\"type\":\"owasp\",\"name\":\"A1:2017 - Injection\",\"value\":\"A1:2017\"},{\"type\":\"eslint_rule_id\",\"name\":\"ESLint rule ID/detect-non-literal-regexp\",\"value\":\"detect-non-literal-regexp\"}],\"tracking\":{\"type\":\"source\",\"items\":[{\"file\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.datepicker.js\",\"line_start\":1108,\"line_end\":1108,\"signatures\":[{\"algorithm\":\"scope_offset\",\"value\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.datepicker.js|func($, undefined)[0]|getNumber[0]:4\"}]}]}}","detection_method":"gitlab_security_report","uuid":"f72b22e4-1e01-5c53-95a5-5e3a1e2f2b16","scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"},"primary_identifier":{"project_id":5,"fingerprint":"a751f35f1185de7ca5e6c0610c3bca21eb25ac9a","external_type":"semgrep_id","external_id":"eslint.detect-non-literal-regexp","name":"eslint.detect-non-literal-regexp","url":"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp"},"identifiers":[{"project_id":5,"fingerprint":"08de3511f2132da4d24f1b8b1d3ca14368a0259b","external_type":"owasp","external_id":"A1:2017","name":"A1:2017 - Injection","url":null},{"project_id":5,"fingerprint":"7153fe286fd77c7a6250aa9603b82d44ab1c31e4","external_type":"cwe","external_id":"185","name":"CWE-185","url":"https://cwe.mitre.org/data/definitions/185.html"},{"project_id":5,"fingerprint":"a15f44ab746431d58b21b4fc67d8c4d3fb160ca0","external_type":"eslint_rule_id","external_id":"detect-non-literal-regexp","name":"ESLint rule ID/detect-non-literal-regexp","url":null},{"project_id":5,"fingerprint":"a751f35f1185de7ca5e6c0610c3bca21eb25ac9a","external_type":"semgrep_id","external_id":"eslint.detect-non-literal-regexp","name":"eslint.detect-non-literal-regexp","url":"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp"},{"project_id":5,"fingerprint":"a8e828eea3aba35916401da9304619f0a218119b","external_type":"owasp","external_id":"A03:2021","name":"A03:2021 - Injection","url":null}],"initial_finding_pipeline":{"iid":438},"latest_finding_pipeline":{"iid":438}},"vulnerability_read":{"project_id":5,"scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"}}}
|
||||
{"project_id":5,"author_id":1,"title":"Regular expression with non-literal value","description":null,"severity":"medium","report_type":"sast","vulnerability_finding":{"severity":"medium","report_type":"sast","project_id":5,"location_fingerprint":"4f7a2fffbb791c4cc8d1454db40b80f7fa9ed5be","name":"Regular expression with non-literal value","metadata_version":"15.1.4","raw_metadata":"{\"id\":\"b13b66b99eabefb8bc0d385b90cb952734e246ff3477a8ee563d6d04ef4bded4\",\"category\":\"sast\",\"name\":\"Regular expression with non-literal value\",\"description\":\"The `RegExp` constructor was called with a non-literal value. If an adversary were able to\\nsupply a malicious regex, they could cause a Regular Expression Denial of Service (ReDoS)\\nagainst the application. In Node applications, this could cause the entire application to no\\nlonger be responsive to other users' requests.\\n\\nTo remediate this issue, never allow user-supplied regular expressions. Instead, the regular \\nexpression should be hardcoded. If this is not possible, consider using an alternative regular\\nexpression engine such as [node-re2](https://www.npmjs.com/package/re2). RE2 is a safe alternative \\nthat does not support backtracking, which is what leads to ReDoS.\\n\\nExample using re2 which does not support backtracking (Note: it is still recommended to\\nnever use user-supplied input):\\n```\\n// Import the re2 module\\nconst RE2 = require('re2');\\n\\nfunction match(userSuppliedRegex, userInput) {\\n // Create a RE2 object with the user supplied regex, this is relatively safe\\n // due to RE2 not supporting backtracking which can be abused to cause long running\\n // queries\\n var re = new RE2(userSuppliedRegex);\\n // Execute the regular expression against some userInput\\n var result = re.exec(userInput);\\n // Work with the result\\n}\\n```\\n\\nFor more information on Regular Expression DoS see:\\n- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS\\n\",\"cve\":\"semgrep_id:eslint.detect-non-literal-regexp:515:515\",\"severity\":\"Medium\",\"scanner\":{\"id\":\"semgrep\",\"name\":\"Semgrep\"},\"location\":{\"file\":\"common/static/ace/ext-language_tools.js\",\"start_line\":515},\"identifiers\":[{\"type\":\"semgrep_id\",\"name\":\"eslint.detect-non-literal-regexp\",\"value\":\"eslint.detect-non-literal-regexp\",\"url\":\"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp\"},{\"type\":\"cwe\",\"name\":\"CWE-185\",\"value\":\"185\",\"url\":\"https://cwe.mitre.org/data/definitions/185.html\"},{\"type\":\"owasp\",\"name\":\"A03:2021 - Injection\",\"value\":\"A03:2021\"},{\"type\":\"owasp\",\"name\":\"A1:2017 - Injection\",\"value\":\"A1:2017\"},{\"type\":\"eslint_rule_id\",\"name\":\"ESLint rule ID/detect-non-literal-regexp\",\"value\":\"detect-non-literal-regexp\"}],\"tracking\":{\"type\":\"source\",\"items\":[{\"file\":\"common/static/ace/ext-language_tools.js\",\"line_start\":515,\"line_end\":515,\"signatures\":[{\"algorithm\":\"scope_offset\",\"value\":\"common/static/ace/ext-language_tools.js|func[0]:498\"}]}]}}","detection_method":"gitlab_security_report","uuid":"fa74cb01-2544-5d42-b9e8-0150119bf6cb","scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"},"primary_identifier":{"project_id":5,"fingerprint":"a751f35f1185de7ca5e6c0610c3bca21eb25ac9a","external_type":"semgrep_id","external_id":"eslint.detect-non-literal-regexp","name":"eslint.detect-non-literal-regexp","url":"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp"},"identifiers":[{"project_id":5,"fingerprint":"08de3511f2132da4d24f1b8b1d3ca14368a0259b","external_type":"owasp","external_id":"A1:2017","name":"A1:2017 - Injection","url":null},{"project_id":5,"fingerprint":"7153fe286fd77c7a6250aa9603b82d44ab1c31e4","external_type":"cwe","external_id":"185","name":"CWE-185","url":"https://cwe.mitre.org/data/definitions/185.html"},{"project_id":5,"fingerprint":"a15f44ab746431d58b21b4fc67d8c4d3fb160ca0","external_type":"eslint_rule_id","external_id":"detect-non-literal-regexp","name":"ESLint rule ID/detect-non-literal-regexp","url":null},{"project_id":5,"fingerprint":"a751f35f1185de7ca5e6c0610c3bca21eb25ac9a","external_type":"semgrep_id","external_id":"eslint.detect-non-literal-regexp","name":"eslint.detect-non-literal-regexp","url":"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp"},{"project_id":5,"fingerprint":"a8e828eea3aba35916401da9304619f0a218119b","external_type":"owasp","external_id":"A03:2021","name":"A03:2021 - Injection","url":null}],"initial_finding_pipeline":{"iid":438},"latest_finding_pipeline":{"iid":438}},"vulnerability_read":{"project_id":5,"scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"}}}
|
||||
{"project_id":5,"author_id":1,"title":"Incorrect regular expression","description":null,"severity":"medium","report_type":"sast","vulnerability_finding":{"severity":"medium","report_type":"sast","project_id":5,"location_fingerprint":"f866afbfc47ac0fae0da7c6df8e5ed35330e4384","name":"Incorrect regular expression","metadata_version":"15.1.4","raw_metadata":"{\"id\":\"0152dfdd49aa1b9636cd267c12d080250199f15f21f427d3bed1a07a002e011f\",\"category\":\"sast\",\"name\":\"Incorrect regular expression\",\"description\":\"Ensure that the regex used to compare with user supplied input is safe from regular expression denial of service.\\n\",\"cve\":\"semgrep_id:nodejs_scan.javascript-dos-rule-regex_dos:1050:1052\",\"severity\":\"Medium\",\"scanner\":{\"id\":\"semgrep\",\"name\":\"Semgrep\"},\"location\":{\"file\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.sortable.js\",\"start_line\":1050,\"end_line\":1052},\"identifiers\":[{\"type\":\"semgrep_id\",\"name\":\"nodejs_scan.javascript-dos-rule-regex_dos\",\"value\":\"nodejs_scan.javascript-dos-rule-regex_dos\"},{\"type\":\"cwe\",\"name\":\"CWE-185\",\"value\":\"185\",\"url\":\"https://cwe.mitre.org/data/definitions/185.html\"},{\"type\":\"owasp\",\"name\":\"A05:2021 - Security Misconfiguration\",\"value\":\"A05:2021\"},{\"type\":\"owasp\",\"name\":\"A6:2017 - Security Misconfiguration\",\"value\":\"A6:2017\"},{\"type\":\"njsscan_rule_type\",\"name\":\"NodeJS Scan ID javascript-dos-rule-regex_dos\",\"value\":\"Ensure that the regex used to compare with user supplied input is safe from regular expression denial of service.\"}],\"tracking\":{\"type\":\"source\",\"items\":[{\"file\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.sortable.js\",\"line_start\":1050,\"line_end\":1050,\"signatures\":[{\"algorithm\":\"scope_offset\",\"value\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.sortable.js|func($, undefined)[0]:1034\"}]}]}}","detection_method":"gitlab_security_report","uuid":"fa2589df-c1ad-5108-93f0-90237b17c1b1","scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"},"primary_identifier":{"project_id":5,"fingerprint":"ad9e1d2b073e1c296088e8fbedf8ed738d06f88a","external_type":"semgrep_id","external_id":"nodejs_scan.javascript-dos-rule-regex_dos","name":"nodejs_scan.javascript-dos-rule-regex_dos","url":null},"identifiers":[{"project_id":5,"fingerprint":"2bd02e525f0e78f8745e5a063ca1b5f396527a41","external_type":"owasp","external_id":"A6:2017","name":"A6:2017 - Security Misconfiguration","url":null},{"project_id":5,"fingerprint":"3f2c4e94cf8c0b53c44cb5b187963b753da9e882","external_type":"owasp","external_id":"A05:2021","name":"A05:2021 - Security Misconfiguration","url":null},{"project_id":5,"fingerprint":"518290ee3e47f4a5bba33213ca8a82e4c0d8697d","external_type":"njsscan_rule_type","external_id":"Ensure that the regex used to compare with user supplied input is safe from regular expression denial of service.","name":"NodeJS Scan ID javascript-dos-rule-regex_dos","url":null},{"project_id":5,"fingerprint":"7153fe286fd77c7a6250aa9603b82d44ab1c31e4","external_type":"cwe","external_id":"185","name":"CWE-185","url":"https://cwe.mitre.org/data/definitions/185.html"},{"project_id":5,"fingerprint":"ad9e1d2b073e1c296088e8fbedf8ed738d06f88a","external_type":"semgrep_id","external_id":"nodejs_scan.javascript-dos-rule-regex_dos","name":"nodejs_scan.javascript-dos-rule-regex_dos","url":null}],"initial_finding_pipeline":{"iid":438},"latest_finding_pipeline":{"iid":438}},"vulnerability_read":{"project_id":5,"scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"}}}
|
||||
{"project_id":5,"author_id":1,"title":"Regular expression with non-literal value","description":null,"severity":"medium","report_type":"sast","vulnerability_finding":{"severity":"medium","report_type":"sast","project_id":5,"location_fingerprint":"708aa3150b2b448e6894dd447689336d0ce63f19","name":"Regular expression with non-literal value","metadata_version":"15.1.4","raw_metadata":"{\"id\":\"f8c645cd515f94924c9a8fe73cc3e2bcf08b90ee9936462b6da57b6c28b52803\",\"category\":\"sast\",\"name\":\"Regular expression with non-literal value\",\"description\":\"The `RegExp` constructor was called with a non-literal value. If an adversary were able to\\nsupply a malicious regex, they could cause a Regular Expression Denial of Service (ReDoS)\\nagainst the application. In Node applications, this could cause the entire application to no\\nlonger be responsive to other users' requests.\\n\\nTo remediate this issue, never allow user-supplied regular expressions. Instead, the regular \\nexpression should be hardcoded. If this is not possible, consider using an alternative regular\\nexpression engine such as [node-re2](https://www.npmjs.com/package/re2). RE2 is a safe alternative \\nthat does not support backtracking, which is what leads to ReDoS.\\n\\nExample using re2 which does not support backtracking (Note: it is still recommended to\\nnever use user-supplied input):\\n```\\n// Import the re2 module\\nconst RE2 = require('re2');\\n\\nfunction match(userSuppliedRegex, userInput) {\\n // Create a RE2 object with the user supplied regex, this is relatively safe\\n // due to RE2 not supporting backtracking which can be abused to cause long running\\n // queries\\n var re = new RE2(userSuppliedRegex);\\n // Execute the regular expression against some userInput\\n var result = re.exec(userInput);\\n // Work with the result\\n}\\n```\\n\\nFor more information on Regular Expression DoS see:\\n- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS\\n\",\"cve\":\"semgrep_id:eslint.detect-non-literal-regexp:1108:1108\",\"severity\":\"Medium\",\"scanner\":{\"id\":\"semgrep\",\"name\":\"Semgrep\"},\"location\":{\"file\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.datepicker.js\",\"start_line\":1108},\"identifiers\":[{\"type\":\"semgrep_id\",\"name\":\"eslint.detect-non-literal-regexp\",\"value\":\"eslint.detect-non-literal-regexp\",\"url\":\"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp\"},{\"type\":\"cwe\",\"name\":\"CWE-185\",\"value\":\"185\",\"url\":\"https://cwe.mitre.org/data/definitions/185.html\"},{\"type\":\"owasp\",\"name\":\"A03:2021 - Injection\",\"value\":\"A03:2021\"},{\"type\":\"owasp\",\"name\":\"A1:2017 - Injection\",\"value\":\"A1:2017\"},{\"type\":\"eslint_rule_id\",\"name\":\"ESLint rule ID/detect-non-literal-regexp\",\"value\":\"detect-non-literal-regexp\"}],\"tracking\":{\"type\":\"source\",\"items\":[{\"file\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.datepicker.js\",\"line_start\":1108,\"line_end\":1108,\"signatures\":[{\"algorithm\":\"scope_offset\",\"value\":\"themis/static/assets/plugins/jquery-ui/ui/jquery.ui.datepicker.js|func($, undefined)[0]|getNumber[0]:4\"}]}]}}","detection_method":"gitlab_security_report","uuid":"f72b22e4-1e01-5c53-95a5-5e3a1e2f2b16","scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"},"primary_identifier":{"project_id":5,"fingerprint":"a751f35f1185de7ca5e6c0610c3bca21eb25ac9a","external_type":"semgrep_id","external_id":"eslint.detect-non-literal-regexp","name":"eslint.detect-non-literal-regexp","url":"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp"},"identifiers":[{"project_id":5,"fingerprint":"08de3511f2132da4d24f1b8b1d3ca14368a0259b","external_type":"owasp","external_id":"A1:2017","name":"A1:2017 - Injection","url":null},{"project_id":5,"fingerprint":"7153fe286fd77c7a6250aa9603b82d44ab1c31e4","external_type":"cwe","external_id":"185","name":"CWE-185","url":"https://cwe.mitre.org/data/definitions/185.html"},{"project_id":5,"fingerprint":"a15f44ab746431d58b21b4fc67d8c4d3fb160ca0","external_type":"eslint_rule_id","external_id":"detect-non-literal-regexp","name":"ESLint rule ID/detect-non-literal-regexp","url":null},{"project_id":5,"fingerprint":"a751f35f1185de7ca5e6c0610c3bca21eb25ac9a","external_type":"semgrep_id","external_id":"eslint.detect-non-literal-regexp","name":"eslint.detect-non-literal-regexp","url":"https://semgrep.dev/r/gitlab.eslint.detect-non-literal-regexp"},{"project_id":5,"fingerprint":"a8e828eea3aba35916401da9304619f0a218119b","external_type":"owasp","external_id":"A03:2021","name":"A03:2021 - Injection","url":null}],"initial_finding_pipeline":{"iid":438},"latest_finding_pipeline":{"iid":438}},"vulnerability_read":{"project_id":5,"scanner":{"project_id":5,"external_id":"semgrep","name":"Semgrep","vendor":"GitLab"}}}
|
||||
|
|
|
|||
|
|
@ -137,7 +137,6 @@ describe('~/frontend/environments/graphql/resolvers', () => {
|
|||
);
|
||||
|
||||
expect(mockInitConnectionFn).toHaveBeenCalledWith({
|
||||
configuration,
|
||||
message: {
|
||||
watchId: `kustomizations-${resourceName}`,
|
||||
watchParams: {
|
||||
|
|
|
|||
|
|
@ -154,7 +154,6 @@ describe('~/frontend/environments/graphql/resolvers', () => {
|
|||
await mockResolvers.Query.k8sPods(null, { configuration, namespace }, { client });
|
||||
|
||||
expect(mockInitConnectionFn).toHaveBeenCalledWith({
|
||||
configuration,
|
||||
message: {
|
||||
watchId: `k8sPods-n-${namespace}`,
|
||||
watchParams: {
|
||||
|
|
@ -281,7 +280,6 @@ describe('~/frontend/environments/graphql/resolvers', () => {
|
|||
await mockResolvers.Query.k8sServices(null, { configuration, namespace }, { client });
|
||||
|
||||
expect(mockInitConnectionFn).toHaveBeenCalledWith({
|
||||
configuration,
|
||||
message: {
|
||||
watchId: `k8sServices-n-${namespace}`,
|
||||
watchParams: {
|
||||
|
|
@ -546,7 +544,6 @@ describe('~/frontend/environments/graphql/resolvers', () => {
|
|||
);
|
||||
|
||||
expect(mockInitConnectionFn).toHaveBeenCalledWith({
|
||||
configuration,
|
||||
message: {
|
||||
watchId: `events-io-${involvedObjectName}`,
|
||||
watchParams: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import { WebSocketWatchManager } from '@gitlab/cluster-client';
|
||||
import {
|
||||
getWatchManager,
|
||||
resetWatchManager,
|
||||
} from '~/environments/services/websocket_connection_service';
|
||||
|
||||
jest.mock('@gitlab/cluster-client', () => ({
|
||||
WebSocketWatchManager: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('getWatchManager', () => {
|
||||
beforeEach(() => {
|
||||
resetWatchManager();
|
||||
});
|
||||
|
||||
it('creates new WebSocketWatchManager instance when configuration is provided', () => {
|
||||
const mockConfig = { url: 'wss://example.com' };
|
||||
|
||||
const watchManager = getWatchManager(mockConfig);
|
||||
|
||||
expect(WebSocketWatchManager).toHaveBeenCalledWith(mockConfig);
|
||||
expect(watchManager).toBeDefined();
|
||||
});
|
||||
|
||||
it('returns existing instance when called multiple times', () => {
|
||||
const mockConfig = { url: 'wss://example.com' };
|
||||
|
||||
const firstInstance = getWatchManager(mockConfig);
|
||||
const secondInstance = getWatchManager();
|
||||
|
||||
expect(WebSocketWatchManager).toHaveBeenCalledTimes(1);
|
||||
expect(firstInstance).toBe(secondInstance);
|
||||
});
|
||||
|
||||
it('throws error when called without configuration and no existing instance', () => {
|
||||
expect(() => {
|
||||
getWatchManager();
|
||||
}).toThrow('WebSocketWatchManager not initialized. Provide configuration first.');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,10 @@
|
|||
import { handleClusterError } from '~/kubernetes_dashboard/graphql/helpers/resolver_helpers';
|
||||
import {
|
||||
handleClusterError,
|
||||
subscribeToSocket,
|
||||
} from '~/kubernetes_dashboard/graphql/helpers/resolver_helpers';
|
||||
import { getWatchManager } from '~/environments/services/websocket_connection_service';
|
||||
|
||||
jest.mock('~/environments/services/websocket_connection_service');
|
||||
|
||||
describe('handleClusterError', () => {
|
||||
describe('helper argument includes a response data', () => {
|
||||
|
|
@ -46,3 +52,57 @@ describe('handleClusterError', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribeToSocket', () => {
|
||||
const config = { apiUrl: 'test-url' };
|
||||
const watchParams = { resource: 'pods', namespace: 'default' };
|
||||
const watchId = 'test-watch-id';
|
||||
let mockWatcher;
|
||||
let mockCacheParams;
|
||||
|
||||
beforeEach(() => {
|
||||
mockWatcher = {
|
||||
on: jest.fn(),
|
||||
initConnection: jest.fn(),
|
||||
};
|
||||
|
||||
mockCacheParams = {
|
||||
updateQueryCache: jest.fn(),
|
||||
updateConnectionStatusFn: jest.fn(),
|
||||
};
|
||||
|
||||
getWatchManager.mockReturnValue(mockWatcher);
|
||||
mockWatcher.initConnection.mockResolvedValue(mockWatcher);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('initializes websocket connection with correct parameters', async () => {
|
||||
await subscribeToSocket({
|
||||
watchId,
|
||||
watchParams,
|
||||
cacheParams: mockCacheParams,
|
||||
config,
|
||||
});
|
||||
|
||||
expect(getWatchManager).toHaveBeenCalledWith(config);
|
||||
expect(mockWatcher.initConnection).toHaveBeenCalledWith({
|
||||
message: { watchId, watchParams },
|
||||
});
|
||||
});
|
||||
|
||||
it('handles connection initialization failure', async () => {
|
||||
mockWatcher.initConnection.mockRejectedValue(new Error('Connection failed'));
|
||||
|
||||
await expect(
|
||||
subscribeToSocket({
|
||||
watchId,
|
||||
watchParams,
|
||||
cacheParams: mockCacheParams,
|
||||
config,
|
||||
}),
|
||||
).rejects.toThrow('Failed to establish WebSocket connection');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1391,14 +1391,16 @@ RSpec.describe API::Helpers, feature_category: :shared do
|
|||
let(:content_type) { nil }
|
||||
let(:content_disposition) { nil }
|
||||
let(:extra_response_headers) { {} }
|
||||
let(:extra_send_url_params) { {} }
|
||||
|
||||
subject do
|
||||
helper.present_carrierwave_file!(
|
||||
artifact.file,
|
||||
supports_direct_download: supports_direct_download,
|
||||
content_disposition: content_disposition,
|
||||
content_type: content_type,
|
||||
extra_response_headers: extra_response_headers
|
||||
supports_direct_download:,
|
||||
content_disposition:,
|
||||
content_type:,
|
||||
extra_response_headers:,
|
||||
extra_send_url_params:
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -1514,6 +1516,27 @@ RSpec.describe API::Helpers, feature_category: :shared do
|
|||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'with extra_send_url_params set' do
|
||||
let(:extra_send_url_params) { { restrict_forwarded_response_headers: { enabled: true, allow_list: ['x-optional-header'] } } }
|
||||
|
||||
it 'sends a workhorse header with the response headers' do
|
||||
expect(helper).to receive(:status).with(:ok)
|
||||
expect(helper).to receive(:body).with('')
|
||||
expect(helper).to receive(:header) do |name, value|
|
||||
expect(name).to eq(Gitlab::Workhorse::SEND_DATA_HEADER)
|
||||
command, encoded_params = value.split(":")
|
||||
params = Gitlab::Json.parse(Base64.urlsafe_decode64(encoded_params))
|
||||
|
||||
expect(command).to eq('send-url')
|
||||
restrict_forwarded_response_headers_params = params['RestrictForwardedResponseHeaders']
|
||||
expect(restrict_forwarded_response_headers_params['Enabled']).to be_truthy
|
||||
expect(restrict_forwarded_response_headers_params['AllowList']).to contain_exactly('x-optional-header')
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@ RSpec.describe Banzai::Filter::QuickActionFilter, feature_category: :markdown do
|
|||
expect(result[:quick_action_paragraphs]).to match_array [{ start_line: 0, end_line: 1 }]
|
||||
end
|
||||
|
||||
it 'detects action in paragraph when it is on another line' do
|
||||
described_class.call(%(<p data-sourcepos="1:1-2:3">foo\n/quick</p>), {}, result)
|
||||
|
||||
expect(result[:quick_action_paragraphs]).to match_array [{ start_line: 0, end_line: 1 }]
|
||||
end
|
||||
|
||||
it 'does not detect action in paragraph if no sourcepos' do
|
||||
described_class.call('<p>/quick</p>', {}, result)
|
||||
|
||||
|
|
|
|||
|
|
@ -293,6 +293,22 @@ RSpec.describe Gitlab::ImportExport::MembersMapper do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when not using the default member' do
|
||||
let(:members_mapper) do
|
||||
described_class.new(
|
||||
exported_members: exported_members, user: user2, importable: importable, default_member: false
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
importable.add_members([user, user2], GroupMember::MAINTAINER)
|
||||
end
|
||||
|
||||
it 'does not destroy existing members on initialization' do
|
||||
expect { members_mapper }.not_to change { importable.reload.members.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when importer mapping fails' do
|
||||
let(:exception_message) { 'Something went wrong' }
|
||||
|
||||
|
|
|
|||
|
|
@ -1110,7 +1110,6 @@ Vulnerabilities::Finding:
|
|||
- severity
|
||||
- report_type
|
||||
- project_id
|
||||
- project_fingerprint
|
||||
- location_fingerprint
|
||||
- name
|
||||
- metadata_version
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Middleware::SecureHeaders, feature_category: :shared do
|
||||
let(:status_origin) { 200 }
|
||||
let(:body_origin) { ['Hello, World'] }
|
||||
let(:app) { ->(_env) { [status_origin, {}, body_origin] } }
|
||||
let(:env) { Rack::MockRequest.env_for('/', method: 'get') }
|
||||
|
||||
subject(:middleware) { described_class.new(app) }
|
||||
|
||||
it 'adds the expected header to the response and preserves the original response status and body' do
|
||||
status, headers, body = middleware.call(env)
|
||||
|
||||
expect(headers['NEL']).to eq('{"max_age": 0}')
|
||||
expect(status).to eq(status_origin)
|
||||
expect(body).to eq(body_origin)
|
||||
end
|
||||
end
|
||||
|
|
@ -548,7 +548,11 @@ RSpec.describe Gitlab::Workhorse, feature_category: :shared do
|
|||
'Header' => {},
|
||||
'ResponseHeaders' => {},
|
||||
'Body' => '',
|
||||
'Method' => 'GET'
|
||||
'Method' => 'GET',
|
||||
'RestrictForwardedResponseHeaders' => {
|
||||
'Enabled' => false,
|
||||
'AllowList' => []
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -649,6 +653,26 @@ RSpec.describe Gitlab::Workhorse, feature_category: :shared do
|
|||
expect(params).to eq(expected_params)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when `restrict_forwarded_repsonse_headers` is set' do
|
||||
let(:expected_params) do
|
||||
super().merge('RestrictForwardedResponseHeaders' => {
|
||||
'Enabled' => true,
|
||||
'AllowList' => ['x-optional-header']
|
||||
})
|
||||
end
|
||||
|
||||
it 'sets the header correctly' do
|
||||
key, command, params = decode_workhorse_header(described_class.send_url(
|
||||
url,
|
||||
restrict_forwarded_response_headers: { enabled: true, allow_list: ['x-optional-header'] }
|
||||
))
|
||||
|
||||
expect(key).to eq('Gitlab-Workhorse-Send-Data')
|
||||
expect(command).to eq('send-url')
|
||||
expect(params).to eq(expected_params)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.send_scaled_image' do
|
||||
|
|
@ -682,10 +706,12 @@ RSpec.describe Gitlab::Workhorse, feature_category: :shared do
|
|||
let(:ssrf_filter) { false }
|
||||
let(:allow_localhost) { true }
|
||||
let(:allowed_uris) { [] }
|
||||
let(:restrict_forwarded_response_headers) { {} }
|
||||
let(:expected_restrict_forwarded_response_headers) { { 'Enabled' => false, 'AllowList' => [] } }
|
||||
|
||||
subject do
|
||||
described_class.send_dependency(
|
||||
headers, url, upload_config: upload_config, ssrf_filter: ssrf_filter, allow_localhost: allow_localhost, allowed_uris: allowed_uris
|
||||
headers, url, upload_config:, ssrf_filter:, allow_localhost:, allowed_uris:, restrict_forwarded_response_headers:
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -698,6 +724,7 @@ RSpec.describe Gitlab::Workhorse, feature_category: :shared do
|
|||
'SSRFFilter' => ssrf_filter,
|
||||
'AllowedURIs' => allowed_uris.map(&:to_s),
|
||||
'Url' => url,
|
||||
'RestrictForwardedResponseHeaders' => expected_restrict_forwarded_response_headers,
|
||||
'UploadConfig' => {
|
||||
'Method' => upload_method,
|
||||
'Url' => upload_url,
|
||||
|
|
@ -758,6 +785,18 @@ RSpec.describe Gitlab::Workhorse, feature_category: :shared do
|
|||
|
||||
it_behaves_like 'setting the header correctly'
|
||||
end
|
||||
|
||||
context 'when `restrict_forwarded_response_headers` parameter is set' do
|
||||
let(:restrict_forwarded_response_headers) { { enabled: true, allow_list: ['x-optional-header'] } }
|
||||
let(:expected_restrict_forwarded_response_headers) do
|
||||
super().merge(
|
||||
'Enabled' => true,
|
||||
'AllowList' => ['x-optional-header']
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'setting the header correctly'
|
||||
end
|
||||
end
|
||||
|
||||
describe '.send_git_snapshot' do
|
||||
|
|
|
|||
|
|
@ -177,6 +177,20 @@ RSpec.describe Groups::UpdateService, feature_category: :groups_and_projects do
|
|||
expect(updated_group.crm_enabled?).to be_truthy
|
||||
expect(updated_group.crm_group).to eq(internal_group)
|
||||
end
|
||||
|
||||
context 'when crm_source_group_id blank and issues have contacts' do
|
||||
let(:params) { { crm_source_group_id: '' } }
|
||||
|
||||
before do
|
||||
allow(public_group).to receive(:has_issues_with_contacts?).and_return(true)
|
||||
end
|
||||
|
||||
it 'does not return an error' do
|
||||
described_class.new(public_group, user, params).execute
|
||||
|
||||
expect(public_group.errors).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with existing crm_settings' do
|
||||
|
|
|
|||
|
|
@ -13,6 +13,23 @@ RSpec.shared_examples 'returns failure response' do |expected_status, expected_m
|
|||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'returns success response' do
|
||||
it 'schedules a restore of the relation' do
|
||||
expect(Projects::ImportExport::RelationImportWorker).to receive(:perform_async)
|
||||
|
||||
import_service.execute
|
||||
end
|
||||
|
||||
it 'returns a service response' do
|
||||
response = import_service.execute
|
||||
|
||||
expect(response).to be_instance_of(ServiceResponse)
|
||||
expect(response).to be_success
|
||||
expect(response.http_status).to eq(:ok)
|
||||
expect(response.payload).to be_instance_of(Projects::ImportExport::RelationImportTracker)
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe ::Projects::ImportExport::RelationImportService, :aggregate_failures, feature_category: :importers do
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
|
|
@ -39,20 +56,7 @@ RSpec.describe ::Projects::ImportExport::RelationImportService, :aggregate_failu
|
|||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
it 'schedules a restore of the relation' do
|
||||
expect(Projects::ImportExport::RelationImportWorker).to receive(:perform_async)
|
||||
|
||||
import_service.execute
|
||||
end
|
||||
|
||||
it 'returns a service response' do
|
||||
response = import_service.execute
|
||||
|
||||
expect(response).to be_instance_of(ServiceResponse)
|
||||
expect(response).to be_success
|
||||
expect(response.http_status).to eq(:ok)
|
||||
expect(response.payload).to be_instance_of(Projects::ImportExport::RelationImportTracker)
|
||||
end
|
||||
include_examples 'returns success response'
|
||||
|
||||
context 'and the relation import tracker cannot be created' do
|
||||
before do
|
||||
|
|
@ -66,6 +70,12 @@ RSpec.describe ::Projects::ImportExport::RelationImportService, :aggregate_failu
|
|||
end
|
||||
end
|
||||
|
||||
context 'and the user is an admin bot' do
|
||||
let_it_be(:user) { Users::Internal.admin_bot }
|
||||
|
||||
include_examples 'returns success response'
|
||||
end
|
||||
|
||||
context 'and the user has developer access' do
|
||||
before_all do
|
||||
project.add_developer(user)
|
||||
|
|
@ -74,7 +84,7 @@ RSpec.describe ::Projects::ImportExport::RelationImportService, :aggregate_failu
|
|||
include_examples 'returns failure response', :forbidden, 'You are not authorized to perform this action'
|
||||
end
|
||||
|
||||
context 'and the has no access' do
|
||||
context 'and the user has no access' do
|
||||
include_examples 'returns failure response', :forbidden, 'You are not authorized to perform this action'
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ module MigrationHelpers
|
|||
uuid = SecureRandom.uuid
|
||||
|
||||
{
|
||||
project_fingerprint: SecureRandom.hex(20),
|
||||
location_fingerprint: Digest::SHA1.hexdigest(SecureRandom.hex(10)), # rubocop:disable Fips/SHA1
|
||||
uuid: uuid,
|
||||
name: "Vulnerability Finding #{uuid}",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ module MigrationHelpers
|
|||
def create_finding!(
|
||||
project_id:, scanner_id:, primary_identifier_id:, vulnerability_id: nil,
|
||||
name: "test", severity: 7, report_type: 0,
|
||||
project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
|
||||
location_fingerprint: 'test',
|
||||
metadata_version: 'test', raw_metadata: 'test', uuid: 'b1cee17e-3d7a-11ed-b878-0242ac120002')
|
||||
table(:vulnerability_occurrences).create!(
|
||||
vulnerability_id: vulnerability_id,
|
||||
|
|
@ -14,7 +14,6 @@ module MigrationHelpers
|
|||
name: name,
|
||||
severity: severity,
|
||||
report_type: report_type,
|
||||
project_fingerprint: project_fingerprint,
|
||||
scanner_id: scanner_id,
|
||||
primary_identifier_id: primary_identifier_id,
|
||||
location_fingerprint: location_fingerprint,
|
||||
|
|
|
|||
|
|
@ -179,6 +179,13 @@ RSpec.shared_examples 'sanitize link' do
|
|||
|
||||
expect(act.to_html).to eq exp
|
||||
end
|
||||
|
||||
it 'limits to MAX_LINK_ATTRIBUTES links' do
|
||||
exp = %q(<a href="foo/bar.md">foo/bar.md</a>) * (Banzai::Filter::SanitizeLinkFilter::MAX_LINK_ATTRIBUTES + 1)
|
||||
act = filter(exp)
|
||||
|
||||
expect(act.to_html).to eq Banzai::Filter::SanitizeLinkFilter::TIMEOUT_MARKDOWN_MESSAGE
|
||||
end
|
||||
end
|
||||
|
||||
# not meant to be exhaustive, but verify that the pipeline is doing sanitization
|
||||
|
|
|
|||
|
|
@ -34,10 +34,18 @@ RSpec.describe Projects::ImportExport::RelationImportWorker, feature_category: :
|
|||
end
|
||||
|
||||
it 'refreshes the project stats' do
|
||||
expect(worker).to receive(:perform_post_import_tasks)
|
||||
allow(worker).to receive(:project).and_return(tracker.project)
|
||||
expect(tracker.project).to receive(:reset_counters_and_iids)
|
||||
expect(InternalId).to receive(:flush_records!).with(namespace: tracker.project.project_namespace)
|
||||
|
||||
perform
|
||||
end
|
||||
|
||||
it 'does not change any project attributes' do
|
||||
tracker.project.update!(description: 'an updated description', approvals_before_merge: 2, visibility_level: 10)
|
||||
|
||||
expect { perform }.not_to change { tracker.project.reload.attributes }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the import fails' do
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@ internal/api/channel_settings.go:57:28: G402: TLS MinVersion too low. (gosec)
|
|||
internal/channel/channel.go:128:31: response body must be closed (bodyclose)
|
||||
internal/config/config.go:247:18: G204: Subprocess launched with variable (gosec)
|
||||
internal/config/config.go:339:8: G101: Potential hardcoded credentials (gosec)
|
||||
internal/dependencyproxy/dependencyproxy.go:121: Function 'Inject' is too long (61 > 60) (funlen)
|
||||
internal/dependencyproxy/dependencyproxy_test.go:514: internal/dependencyproxy/dependencyproxy_test.go:514: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "note that the timeout duration here is s..." (godox)
|
||||
internal/dependencyproxy/dependencyproxy_test.go:572: internal/dependencyproxy/dependencyproxy_test.go:572: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "note that the timeout duration here is s..." (godox)
|
||||
internal/git/archive.go:67: Function 'Inject' has too many statements (55 > 40) (funlen)
|
||||
internal/git/blob.go:21:5: exported: exported var SendBlob should have comment or be unexported (revive)
|
||||
internal/git/diff.go:1: 1-47 lines are duplicate of `internal/git/format-patch.go:1-48` (dupl)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
"gitlab.com/gitlab-org/labkit/log"
|
||||
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/forwardheaders"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper/fail"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/senddata"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/transport"
|
||||
|
|
@ -49,13 +50,14 @@ type Injector struct {
|
|||
}
|
||||
|
||||
type entryParams struct {
|
||||
URL string
|
||||
Headers http.Header
|
||||
ResponseHeaders http.Header
|
||||
UploadConfig uploadConfig
|
||||
SSRFFilter bool
|
||||
AllowLocalhost bool
|
||||
AllowedURIs []string
|
||||
URL string
|
||||
Headers http.Header
|
||||
ResponseHeaders http.Header
|
||||
UploadConfig uploadConfig
|
||||
SSRFFilter bool
|
||||
AllowLocalhost bool
|
||||
AllowedURIs []string
|
||||
RestrictForwardedResponseHeaders forwardheaders.Params
|
||||
}
|
||||
|
||||
type uploadConfig struct {
|
||||
|
|
@ -160,8 +162,7 @@ func (p *Injector) Inject(w http.ResponseWriter, r *http.Request, sendData strin
|
|||
}
|
||||
|
||||
forwardHeaders(dependencyResponse.Header, saveFileRequest)
|
||||
|
||||
p.forwardHeadersToResponse(w, dependencyResponse.Header, params.ResponseHeaders)
|
||||
params.RestrictForwardedResponseHeaders.ForwardResponseHeaders(w, dependencyResponse, []string{}, params.ResponseHeaders)
|
||||
|
||||
// workhorse hijack overwrites the Content-Type header, but we need this header value
|
||||
saveFileRequest.Header.Set("Workhorse-Proxy-Content-Type", dependencyResponse.Header.Get("Content-Type"))
|
||||
|
|
@ -234,17 +235,6 @@ func (p *Injector) newUploadRequest(ctx context.Context, params *entryParams, or
|
|||
return request, nil
|
||||
}
|
||||
|
||||
func (p *Injector) forwardHeadersToResponse(w http.ResponseWriter, headers ...http.Header) {
|
||||
for _, h := range headers {
|
||||
for key, values := range h {
|
||||
w.Header().Del(key)
|
||||
for _, v := range values {
|
||||
w.Header().Add(key, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Injector) unpackParams(sendData string) (*entryParams, error) {
|
||||
var params entryParams
|
||||
if err := p.Unpack(¶ms, sendData); err != nil {
|
||||
|
|
|
|||
|
|
@ -437,6 +437,64 @@ func mergeMap(from map[string]interface{}, into map[string]interface{}) map[stri
|
|||
return into
|
||||
}
|
||||
|
||||
func TestRestrictForwardedResponseHeaders(t *testing.T) {
|
||||
content := []byte("result")
|
||||
contentLength := strconv.Itoa(len(content))
|
||||
contentType := "multipart/x-mixed-replace"
|
||||
acceptedHeader := "X-Accepted-Header"
|
||||
notAcceptedHeader := "X-Not-Accepted-Header"
|
||||
originResourceServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Length", contentLength)
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Header().Set(acceptedHeader, "test")
|
||||
w.Header().Set(notAcceptedHeader, "test")
|
||||
w.Write(content)
|
||||
}))
|
||||
defer originResourceServer.Close()
|
||||
|
||||
uploadHandler := &fakeUploadHandler{
|
||||
handler: func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
},
|
||||
}
|
||||
|
||||
injector := NewInjector()
|
||||
injector.SetUploadHandler(uploadHandler)
|
||||
|
||||
entryParamsJSON := jsonEntryParams(t, &map[string]interface{}{
|
||||
"Token": "token",
|
||||
"Url": originResourceServer.URL + `/remote/file`,
|
||||
"ResponseHeaders": http.Header{"CustomHeader": {"test"}},
|
||||
"RestrictForwardedResponseHeaders": &map[string]interface{}{
|
||||
"Enabled": true,
|
||||
"AllowList": []string{acceptedHeader, "Content-Type", "Content-Length"},
|
||||
},
|
||||
})
|
||||
|
||||
response := makeRequest(injector, entryParamsJSON)
|
||||
|
||||
require.Equal(t, "/target/upload", uploadHandler.request.URL.Path)
|
||||
require.Equal(t, int64(6), uploadHandler.request.ContentLength)
|
||||
require.Equal(t, content, uploadHandler.body)
|
||||
require.Equal(t, http.StatusOK, response.Code)
|
||||
|
||||
expectedHeaders := http.Header{
|
||||
"Content-Length": []string{contentLength},
|
||||
"Content-Type": []string{"application/octet-stream"},
|
||||
acceptedHeader: []string{"test"},
|
||||
"Customheader": []string{"test"},
|
||||
}
|
||||
|
||||
require.Equal(t, expectedHeaders, response.Header())
|
||||
}
|
||||
|
||||
func jsonEntryParams(t *testing.T, params *map[string]interface{}) string {
|
||||
result, err := json.Marshal(params)
|
||||
require.NoError(t, err)
|
||||
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func TestIncorrectSendData(t *testing.T) {
|
||||
response := makeRequest(NewInjector(), "")
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
// Package forwardheaders implements utility functions for forwarding headers from
|
||||
// a response to a response writer
|
||||
//
|
||||
// This is meant to be used in sendurl and dependencyproxy packages.
|
||||
package forwardheaders
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
contentTypeHeader = "Content-Type"
|
||||
octetStreamMimeType = "application/octet-stream"
|
||||
)
|
||||
|
||||
// Params represents the configuration used by this package.
|
||||
type Params struct {
|
||||
Enabled bool
|
||||
AllowList []string
|
||||
}
|
||||
|
||||
// ForwardResponseHeaders will forward the headers from the passed upstream response to the response writer.
|
||||
// If a header is forwarded or not depends on a few rules:
|
||||
// * if a header is present in the preserveHeaderKeys slice, then it's not forwarded.
|
||||
// * if enabled _and_ a header is present in the allow list, then it's forward.
|
||||
// * if disabled, all headers (except those that are protected) are forwarded.
|
||||
func (p *Params) ForwardResponseHeaders(w http.ResponseWriter, upstreamResponse *http.Response, preserveHeaderKeys []string, extraHeaders http.Header) {
|
||||
if p.Enabled {
|
||||
replaceContentType(upstreamResponse)
|
||||
}
|
||||
|
||||
w.Header().Del("Content-Length")
|
||||
|
||||
canonicalProtectedKeys := []string{}
|
||||
canonicalAllowedKeys := []string{}
|
||||
|
||||
for _, header := range preserveHeaderKeys {
|
||||
canonicalProtectedKeys = append(canonicalProtectedKeys, http.CanonicalHeaderKey(header))
|
||||
}
|
||||
|
||||
for _, header := range p.AllowList {
|
||||
canonicalAllowedKeys = append(canonicalAllowedKeys, http.CanonicalHeaderKey(header))
|
||||
}
|
||||
|
||||
// forward headers according to the protected and allowed keys
|
||||
for key, value := range upstreamResponse.Header {
|
||||
if !slices.Contains(canonicalProtectedKeys, key) {
|
||||
if p.Enabled {
|
||||
if slices.Contains(canonicalAllowedKeys, key) {
|
||||
w.Header()[key] = value
|
||||
}
|
||||
} else {
|
||||
w.Header()[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set the extra headers
|
||||
for key, values := range extraHeaders {
|
||||
w.Header().Del(key)
|
||||
for _, value := range values {
|
||||
w.Header().Add(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replaceContentType(response *http.Response) {
|
||||
contentType := response.Header.Get(contentTypeHeader)
|
||||
|
||||
if strings.HasPrefix(contentType, "multipart") {
|
||||
response.Header.Set(contentTypeHeader, octetStreamMimeType)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package forwardheaders
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"maps"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var upstreamHeaders = http.Header{
|
||||
"X-Protected-Header1": []string{"protected1_from_upstream"},
|
||||
"X-Protected-Header2": []string{"protected2_from_upstream"},
|
||||
"X-Custom-Header1": []string{"custom1_from_upstream"},
|
||||
"X-Custom-Header2": []string{"custom2_from_upstream"},
|
||||
}
|
||||
|
||||
func TestForwardResponseHeaders(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
params Params
|
||||
protectedHeaders []string
|
||||
extraHeaders http.Header
|
||||
upstreamContentType string
|
||||
responseWriterHeaders http.Header
|
||||
expectedResponseWriterHeaders http.Header
|
||||
}{
|
||||
{
|
||||
desc: "with restricted headers enabled",
|
||||
params: Params{Enabled: true, AllowList: []string{"content-type", "x-custom-header1"}},
|
||||
protectedHeaders: []string{"x-protected-header1"},
|
||||
extraHeaders: http.Header{"X-Extra-Header": []string{"test"}},
|
||||
upstreamContentType: "multipart/x-mixed-replace",
|
||||
responseWriterHeaders: http.Header{"X-Protected-Header1": []string{"protected1_from_response_writer"}},
|
||||
expectedResponseWriterHeaders: http.Header{
|
||||
"Content-Type": []string{"application/octet-stream"},
|
||||
"X-Protected-Header1": []string{"protected1_from_response_writer"},
|
||||
"X-Custom-Header1": []string{"custom1_from_upstream"},
|
||||
"X-Extra-Header": []string{"test"},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "with restrict headers disabled",
|
||||
params: Params{Enabled: false, AllowList: []string{}},
|
||||
protectedHeaders: []string{"x-protected-header1"},
|
||||
extraHeaders: http.Header{"X-Extra-Header": []string{"test"}},
|
||||
upstreamContentType: "multipart/x-mixed-replace",
|
||||
responseWriterHeaders: http.Header{"X-Protected-Header1": []string{"protected1_from_response_writer"}},
|
||||
expectedResponseWriterHeaders: http.Header{
|
||||
"Content-Type": []string{"multipart/x-mixed-replace"},
|
||||
"X-Protected-Header1": []string{"protected1_from_response_writer"},
|
||||
"X-Protected-Header2": []string{"protected2_from_upstream"},
|
||||
"X-Custom-Header1": []string{"custom1_from_upstream"},
|
||||
"X-Custom-Header2": []string{"custom2_from_upstream"},
|
||||
"X-Extra-Header": []string{"test"},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "with no protected headers",
|
||||
params: Params{Enabled: true, AllowList: []string{"content-type"}},
|
||||
protectedHeaders: []string{},
|
||||
extraHeaders: http.Header{"X-Extra-Header": []string{"test"}},
|
||||
upstreamContentType: "multipart/x-mixed-replace",
|
||||
responseWriterHeaders: http.Header{"X-Protected-Header1": []string{"protected1_from_response_writer"}},
|
||||
expectedResponseWriterHeaders: http.Header{
|
||||
"Content-Type": []string{"application/octet-stream"},
|
||||
"X-Protected-Header1": []string{"protected1_from_response_writer"},
|
||||
"X-Extra-Header": []string{"test"},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "with non restricted content-type",
|
||||
params: Params{Enabled: true, AllowList: []string{"content-type"}},
|
||||
protectedHeaders: []string{},
|
||||
extraHeaders: http.Header{},
|
||||
upstreamContentType: "application/xml",
|
||||
responseWriterHeaders: http.Header{},
|
||||
expectedResponseWriterHeaders: http.Header{
|
||||
"Content-Type": []string{"application/xml"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
w := httptest.NewRecorder()
|
||||
maps.Copy(w.Header(), tc.responseWriterHeaders)
|
||||
|
||||
upstreamResp := &http.Response{Header: upstreamHeaders}
|
||||
upstreamResp.Header.Set("Content-Type", tc.upstreamContentType)
|
||||
|
||||
tc.params.ForwardResponseHeaders(w, upstreamResp, tc.protectedHeaders, tc.extraHeaders)
|
||||
|
||||
require.Equal(t, tc.expectedResponseWriterHeaders, w.Header())
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import (
|
|||
"gitlab.com/gitlab-org/labkit/mask"
|
||||
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/forwardheaders"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper/fail"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/log"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/senddata"
|
||||
|
|
@ -25,19 +26,20 @@ import (
|
|||
type entry struct{ senddata.Prefix }
|
||||
|
||||
type entryParams struct {
|
||||
URL string
|
||||
AllowRedirects bool
|
||||
AllowLocalhost bool
|
||||
AllowedURIs []string
|
||||
SSRFFilter bool
|
||||
DialTimeout config.TomlDuration
|
||||
ResponseHeaderTimeout config.TomlDuration
|
||||
ErrorResponseStatus int
|
||||
TimeoutResponseStatus int
|
||||
Body string
|
||||
Header http.Header
|
||||
ResponseHeaders http.Header
|
||||
Method string
|
||||
URL string
|
||||
AllowRedirects bool
|
||||
AllowLocalhost bool
|
||||
AllowedURIs []string
|
||||
SSRFFilter bool
|
||||
DialTimeout config.TomlDuration
|
||||
ResponseHeaderTimeout config.TomlDuration
|
||||
ErrorResponseStatus int
|
||||
TimeoutResponseStatus int
|
||||
Body string
|
||||
Header http.Header
|
||||
ResponseHeaders http.Header
|
||||
Method string
|
||||
RestrictForwardedResponseHeaders forwardheaders.Params
|
||||
}
|
||||
|
||||
type cacheKey struct {
|
||||
|
|
@ -66,11 +68,11 @@ var rangeHeaderKeys = []string{
|
|||
// Keep cache headers from the original response, not the proxied response. The
|
||||
// original response comes from the Rails application, which should be the
|
||||
// source of truth for caching.
|
||||
var preserveHeaderKeys = map[string]bool{
|
||||
"Cache-Control": true,
|
||||
"Expires": true,
|
||||
"Date": true, // Support for HTTP 1.0 proxies
|
||||
"Pragma": true, // Support for HTTP 1.0 proxies
|
||||
var preserveHeaderKeys = []string{
|
||||
"Cache-Control",
|
||||
"Expires",
|
||||
"Date", // Support for HTTP 1.0 proxies
|
||||
"Pragma", // Support for HTTP 1.0 proxies
|
||||
}
|
||||
|
||||
var httpClientNoRedirect = func(_ *http.Request, _ []*http.Request) error {
|
||||
|
|
@ -137,7 +139,7 @@ func (e *entry) Inject(w http.ResponseWriter, r *http.Request, sendData string)
|
|||
e.handleRequestError(w, r, err, ¶ms)
|
||||
return
|
||||
}
|
||||
e.copyResponseHeaders(w, resp, params.ResponseHeaders)
|
||||
params.RestrictForwardedResponseHeaders.ForwardResponseHeaders(w, resp, preserveHeaderKeys, params.ResponseHeaders)
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
|
||||
defer func() {
|
||||
|
|
@ -196,23 +198,6 @@ func (e *entry) handleRequestError(w http.ResponseWriter, r *http.Request, err e
|
|||
fail.Request(w, r, fmt.Errorf("SendURL: Do request: %v", err), fail.WithStatus(status))
|
||||
}
|
||||
|
||||
func (e *entry) copyResponseHeaders(w http.ResponseWriter, resp *http.Response, responseHeaders map[string][]string) {
|
||||
w.Header().Del("Content-Length")
|
||||
|
||||
for key, value := range resp.Header {
|
||||
if !preserveHeaderKeys[key] {
|
||||
w.Header()[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
for key, values := range responseHeaders {
|
||||
w.Header().Del(key)
|
||||
for _, value := range values {
|
||||
w.Header().Add(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *entry) streamResponse(w http.ResponseWriter, body io.Reader) error {
|
||||
n, err := io.Copy(newFlushingResponseWriter(w), body)
|
||||
sendURLBytes.Add(float64(n))
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ import (
|
|||
|
||||
const testData = `123456789012345678901234567890`
|
||||
const testDataEtag = `W/"myetag"`
|
||||
const entryServerExtraHeader1 = "X-Custom-Header1"
|
||||
const entryServerExtraHeader1Value = "Header1"
|
||||
const entryServerExtraHeader2 = "X-Custom-Header2"
|
||||
const entryServerExtraHeader2Value = "Header2"
|
||||
|
||||
type option struct {
|
||||
Key string
|
||||
|
|
@ -65,6 +69,8 @@ func testEntryServer(t *testing.T, requestURL string, httpHeaders http.Header, a
|
|||
w.Header().Set("Expires", "Wed, 21 Oct 2015 07:28:00 GMT")
|
||||
w.Header().Set("Date", "Wed, 21 Oct 2015 06:28:00 GMT")
|
||||
w.Header().Set("Pragma", "")
|
||||
w.Header().Set(entryServerExtraHeader1, entryServerExtraHeader1Value)
|
||||
w.Header().Set(entryServerExtraHeader2, entryServerExtraHeader2Value)
|
||||
|
||||
http.ServeContent(w, r, "archive.txt", time.Now(), tempFile)
|
||||
}
|
||||
|
|
@ -330,3 +336,26 @@ func TestSSRFFilterWithAllowLocalhost(t *testing.T) {
|
|||
|
||||
require.Equal(t, http.StatusOK, response.Code)
|
||||
}
|
||||
|
||||
func TestRestrictForwardedResponseHeaders(t *testing.T) {
|
||||
restrictForwardedResponseHeadersParams := &map[string]interface{}{
|
||||
"Enabled": true,
|
||||
"AllowList": []string{entryServerExtraHeader1},
|
||||
}
|
||||
|
||||
response := testEntryServer(t, "/get/request", nil, false, option{Key: "RestrictForwardedResponseHeaders", Value: restrictForwardedResponseHeadersParams}, option{Key: "ResponseHeaders", Value: http.Header{"CustomHeader": {"Test"}}})
|
||||
|
||||
require.Equal(t, http.StatusOK, response.Code)
|
||||
|
||||
expectedHeaders := http.Header{
|
||||
"Content-Disposition": []string{"attachment; filename=\"archive.txt\""},
|
||||
"Cache-Control": []string{"no-cache"},
|
||||
"Expires": []string{""},
|
||||
"Date": []string{"Wed, 21 Oct 2015 05:28:00 GMT"},
|
||||
"Pragma": []string{"no-cache"},
|
||||
entryServerExtraHeader1: []string{entryServerExtraHeader1Value},
|
||||
"Customheader": []string{"Test"},
|
||||
}
|
||||
|
||||
require.Equal(t, expectedHeaders, response.Header())
|
||||
}
|
||||
|
|
|
|||
17
yarn.lock
17
yarn.lock
|
|
@ -1365,12 +1365,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/at.js/-/at.js-1.5.7.tgz#1ee6f838cc4410a1d797770934df91d90df8179e"
|
||||
integrity sha512-c6ySRK/Ma7lxwpIVbSAF3P+xiTLrNTGTLRx4/pHK111AdFxwgUwrYF6aVZFXvmG65jHOJHoa0eQQ21RW6rm0Rg==
|
||||
|
||||
"@gitlab/cluster-client@^2.5.0":
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/cluster-client/-/cluster-client-2.5.0.tgz#de5e3be45bbcd1da36e8337ace9d8e81521efa0b"
|
||||
integrity sha512-uaVceqNLmAayvWLCjWUcYUkZ2AfWaj0yTWiu075sGt81zFKmWF+asz+vwWUXBClLKNaHZEoGrDkeJgOTlJP2HQ==
|
||||
"@gitlab/cluster-client@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/cluster-client/-/cluster-client-3.0.0.tgz#96351f55eef83c0dc3f5c030e2fc71d6c4753bd1"
|
||||
integrity sha512-Hg9m9Kf38UUj/30PXxQY96SLVLqXd2mQeVDW1OYzHtOVOU59FXT8s9BydC2OR9J02+WHQQLMHYQKaMYO/wpIKg==
|
||||
dependencies:
|
||||
axios "^0.24.0"
|
||||
core-js "^3.29.1"
|
||||
mitt "^3.0.1"
|
||||
|
||||
|
|
@ -15035,10 +15034,10 @@ vite-plugin-ruby@^5.1.1:
|
|||
debug "^4.3.4"
|
||||
fast-glob "^3.3.2"
|
||||
|
||||
vite@^6.3.1:
|
||||
version "6.3.1"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-6.3.1.tgz#6b080ff907308ca691c5639c095ab6d938734c0d"
|
||||
integrity sha512-kkzzkqtMESYklo96HKKPE5KKLkC1amlsqt+RjFMlX2AvbRB/0wghap19NdBxxwGZ+h/C6DLCrcEphPIItlGrRQ==
|
||||
vite@^6.3.2:
|
||||
version "6.3.2"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-6.3.2.tgz#4c1bb01b1cea853686a191657bbc14272a038f0a"
|
||||
integrity sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==
|
||||
dependencies:
|
||||
esbuild "^0.25.0"
|
||||
fdir "^6.4.3"
|
||||
|
|
|
|||
Loading…
Reference in New Issue