Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ee772e0c77
commit
1c8734ca5c
|
@ -182,28 +182,6 @@ Layout/HashAlignment:
|
|||
- 'ee/spec/support/shared_examples/status_page/publish_shared_examples.rb'
|
||||
- 'ee/spec/support/shared_examples/status_page/reference_links_examples.rb'
|
||||
- 'ee/spec/workers/scan_security_report_secrets_worker_spec.rb'
|
||||
- 'lib/api/issue_links.rb'
|
||||
- 'lib/api/issues.rb'
|
||||
- 'lib/api/labels.rb'
|
||||
- 'lib/api/maven_packages.rb'
|
||||
- 'lib/api/members.rb'
|
||||
- 'lib/api/merge_requests.rb'
|
||||
- 'lib/api/metrics/dashboard/annotations.rb'
|
||||
- 'lib/api/metrics/user_starred_dashboards.rb'
|
||||
- 'lib/api/milestone_responses.rb'
|
||||
- 'lib/api/notes.rb'
|
||||
- 'lib/api/pages_domains.rb'
|
||||
- 'lib/api/project_packages.rb'
|
||||
- 'lib/api/project_templates.rb'
|
||||
- 'lib/api/projects.rb'
|
||||
- 'lib/api/protected_branches.rb'
|
||||
- 'lib/api/releases.rb'
|
||||
- 'lib/api/rubygem_packages.rb'
|
||||
- 'lib/api/sidekiq_metrics.rb'
|
||||
- 'lib/api/users.rb'
|
||||
- 'lib/backup/gitaly_backup.rb'
|
||||
- 'lib/banzai/filter/references/abstract_reference_filter.rb'
|
||||
- 'lib/banzai/reference_redactor.rb'
|
||||
- 'lib/gitlab/abuse.rb'
|
||||
- 'lib/gitlab/access.rb'
|
||||
- 'lib/gitlab/application_rate_limiter.rb'
|
||||
|
|
|
@ -118,6 +118,7 @@ Style/FormatString:
|
|||
- 'app/models/integrations/mattermost.rb'
|
||||
- 'app/models/integrations/pipelines_email.rb'
|
||||
- 'app/models/integrations/pivotaltracker.rb'
|
||||
- 'app/models/integrations/pumble.rb'
|
||||
- 'app/models/integrations/pushover.rb'
|
||||
- 'app/models/integrations/redmine.rb'
|
||||
- 'app/models/integrations/unify_circuit.rb'
|
||||
|
|
|
@ -104,15 +104,16 @@ const getAttrsFactory = ({ attributeTransformer, markdown }) =>
|
|||
function getAttrs(proseMirrorNodeSpec, hastNode, hastParents) {
|
||||
const { getAttrs: specGetAttrs } = proseMirrorNodeSpec;
|
||||
const attributes = {
|
||||
...createSourceMapAttributes(hastNode, markdown),
|
||||
...(isFunction(specGetAttrs) ? specGetAttrs(hastNode, hastParents, markdown) : {}),
|
||||
};
|
||||
const { transform } = attributeTransformer;
|
||||
|
||||
return mapValues(attributes, (value, key) =>
|
||||
attributeTransformer.attributes.includes(key)
|
||||
? attributeTransformer.transform(value, key)
|
||||
: value,
|
||||
);
|
||||
return {
|
||||
...createSourceMapAttributes(hastNode, markdown),
|
||||
...mapValues(attributes, (attributeValue, attributeName) =>
|
||||
transform(attributeName, attributeValue, hastNode),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { render } from '~/lib/gfm';
|
||||
import { isValidAttribute } from '~/lib/dompurify';
|
||||
import { createProseMirrorDocFromMdastTree } from './hast_to_prosemirror_converter';
|
||||
|
||||
const wrappableTags = ['img', 'br', 'code', 'i', 'em', 'b', 'strong', 'a', 'strike', 's', 'del'];
|
||||
|
@ -184,28 +185,34 @@ const factorySpecs = {
|
|||
},
|
||||
};
|
||||
|
||||
const resolveUrl = (url) => {
|
||||
try {
|
||||
return new URL(url, window.location.origin).toString();
|
||||
} catch {
|
||||
const SANITIZE_ALLOWLIST = ['level', 'identifier', 'numeric', 'language', 'url'];
|
||||
|
||||
const sanitizeAttribute = (attributeName, attributeValue, hastNode) => {
|
||||
if (!attributeValue || SANITIZE_ALLOWLIST.includes(attributeName)) {
|
||||
return attributeValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a workaround to validate the value of the canonicalSrc
|
||||
* attribute using DOMPurify without passing the attribute name. canonicalSrc
|
||||
* is not an allowed attribute in DOMPurify therefore the library will remove
|
||||
* it regardless of its value.
|
||||
*
|
||||
* We want to preserve canonicalSrc, and we also want to make sure that its
|
||||
* value is sanitized.
|
||||
*/
|
||||
const validateAttributeAs = attributeName === 'canonicalSrc' ? 'src' : attributeName;
|
||||
|
||||
if (!isValidAttribute(hastNode.tagName, validateAttributeAs, attributeValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return attributeValue;
|
||||
};
|
||||
|
||||
const attributeTransformer = {
|
||||
attributes: ['href', 'src'],
|
||||
transform: (url) => {
|
||||
if (!url) {
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a URL if provided. The URL is not resolved against
|
||||
* the client origin initially to protect the URL protocol
|
||||
* when it is available, for example, we want to preserve
|
||||
* mailto and application-specific protocols
|
||||
*/
|
||||
return resolveUrl(url);
|
||||
transform: (attributeName, attributeValue, hastNode) => {
|
||||
return sanitizeAttribute(attributeName, attributeValue, hastNode);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -93,3 +93,5 @@ addHook('afterSanitizeAttributes', (node) => {
|
|||
});
|
||||
|
||||
export const sanitize = (val, config) => dompurifySanitize(val, { ...defaultConfig, ...config });
|
||||
|
||||
export { isValidAttribute } from 'dompurify';
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
module AcceptsPendingInvitations
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def accept_pending_invitations
|
||||
return unless resource.active_for_authentication?
|
||||
def accept_pending_invitations(user: resource)
|
||||
return unless user.active_for_authentication?
|
||||
|
||||
if resource.pending_invitations.load.any?
|
||||
resource.accept_pending_invitations!
|
||||
clear_stored_location_for_resource
|
||||
if user.pending_invitations.load.any?
|
||||
user.accept_pending_invitations!
|
||||
clear_stored_location_for(user: user)
|
||||
after_pending_invitations_hook
|
||||
end
|
||||
end
|
||||
|
@ -17,8 +17,8 @@ module AcceptsPendingInvitations
|
|||
# no-op
|
||||
end
|
||||
|
||||
def clear_stored_location_for_resource
|
||||
session_key = stored_location_key_for(resource)
|
||||
def clear_stored_location_for(user:)
|
||||
session_key = stored_location_key_for(user)
|
||||
|
||||
session.delete(session_key)
|
||||
end
|
||||
|
|
|
@ -6,6 +6,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
include AuthHelper
|
||||
include InitializesCurrentUserMode
|
||||
include KnownSignIn
|
||||
include AcceptsPendingInvitations
|
||||
|
||||
after_action :verify_known_sign_in
|
||||
|
||||
|
@ -159,6 +160,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
|
||||
def sign_in_user_flow(auth_user_class)
|
||||
auth_user = build_auth_user(auth_user_class)
|
||||
new_user = auth_user.new?
|
||||
user = auth_user.find_and_update!
|
||||
|
||||
if auth_user.valid_sign_in?
|
||||
|
@ -178,6 +180,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
flash[:notice] = _('Welcome back! Your account had been deactivated due to inactivity but is now reactivated.')
|
||||
end
|
||||
|
||||
accept_pending_invitations(user: user) if new_user
|
||||
store_after_sign_up_path_for_user if intent_to_register?
|
||||
sign_in_and_redirect(user, event: :authentication)
|
||||
end
|
||||
|
|
|
@ -21,7 +21,7 @@ class Integration < ApplicationRecord
|
|||
asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker datadog discord
|
||||
drone_ci emails_on_push ewm external_wiki flowdock hangouts_chat harbor irker jira
|
||||
mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email
|
||||
pivotaltracker prometheus pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack zentao
|
||||
pivotaltracker prometheus pumble pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack zentao
|
||||
].freeze
|
||||
|
||||
# TODO Shimo is temporary disabled on group and instance-levels.
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Integrations
|
||||
class Pumble < BaseChatNotification
|
||||
def title
|
||||
'Pumble'
|
||||
end
|
||||
|
||||
def description
|
||||
s_("PumbleIntegration|Send notifications about project events to Pumble.")
|
||||
end
|
||||
|
||||
def self.to_param
|
||||
'pumble'
|
||||
end
|
||||
|
||||
def help
|
||||
docs_link = ActionController::Base.helpers.link_to(
|
||||
_('Learn more.'),
|
||||
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/pumble'),
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer'
|
||||
)
|
||||
# rubocop:disable Layout/LineLength
|
||||
s_("PumbleIntegration|Send notifications about project events to Pumble. %{docs_link}") % { docs_link: docs_link.html_safe }
|
||||
# rubocop:enable Layout/LineLength
|
||||
end
|
||||
|
||||
def default_channel_placeholder
|
||||
end
|
||||
|
||||
def self.supported_events
|
||||
%w[push issue confidential_issue merge_request note confidential_note tag_push
|
||||
pipeline wiki_page]
|
||||
end
|
||||
|
||||
def default_fields
|
||||
[
|
||||
{ type: 'text', name: 'webhook', placeholder: "https://api.pumble.com/workspaces/x/...", required: true },
|
||||
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
|
||||
{
|
||||
type: 'select',
|
||||
name: 'branches_to_be_notified',
|
||||
title: s_('Integrations|Branches for which notifications are to be sent'),
|
||||
choices: self.class.branch_choices
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def notify(message, opts)
|
||||
header = { 'Content-Type' => 'application/json' }
|
||||
response = Gitlab::HTTP.post(webhook, headers: header, body: { text: message.summary }.to_json)
|
||||
|
||||
response if response.success?
|
||||
end
|
||||
end
|
||||
end
|
|
@ -216,6 +216,7 @@ class Project < ApplicationRecord
|
|||
has_one :pipelines_email_integration, class_name: 'Integrations::PipelinesEmail'
|
||||
has_one :pivotaltracker_integration, class_name: 'Integrations::Pivotaltracker'
|
||||
has_one :prometheus_integration, class_name: 'Integrations::Prometheus', inverse_of: :project
|
||||
has_one :pumble_integration, class_name: 'Integrations::Pumble'
|
||||
has_one :pushover_integration, class_name: 'Integrations::Pushover'
|
||||
has_one :redmine_integration, class_name: 'Integrations::Redmine'
|
||||
has_one :shimo_integration, class_name: 'Integrations::Shimo'
|
||||
|
|
|
@ -7,12 +7,13 @@
|
|||
|
||||
- return unless branches.any?
|
||||
|
||||
.card
|
||||
.card-header
|
||||
= render Pajamas::CardComponent.new(card_options: {class: 'gl-mb-5'}, body_options: {class: 'gl-py-0'}, footer_options: {class: 'gl-text-center'}) do |c|
|
||||
- c.header do
|
||||
= panel_title
|
||||
%ul.content-list.all-branches.qa-all-branches
|
||||
- branches.first(overview_max_branches).each do |branch|
|
||||
= render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch), commit_status: @branch_pipeline_statuses[branch.name], show_commit_status: @branch_pipeline_statuses.any?
|
||||
- c.body do
|
||||
%ul.content-list.all-branches.qa-all-branches
|
||||
- branches.first(overview_max_branches).each do |branch|
|
||||
= render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch), commit_status: @branch_pipeline_statuses[branch.name], show_commit_status: @branch_pipeline_statuses.any?
|
||||
- if branches.size > overview_max_branches
|
||||
.card-footer.text-center
|
||||
- c.footer do
|
||||
= link_to show_more_text, project_branches_filtered_path(project, state: state), id: "state-#{state}", data: { state: state }
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
- else
|
||||
= sprite_icon('star-o', css_class: 'icon')
|
||||
%span= s_('ProjectOverview|Star')
|
||||
= link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'gl-button btn btn-default btn-sm star-count count' do
|
||||
= link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'gl-button btn btn-default btn-sm has-tooltip star-count count' do
|
||||
= @project.star_count
|
||||
|
||||
- else
|
||||
|
@ -15,5 +15,5 @@
|
|||
= link_to new_user_session_path, class: 'gl-button btn btn-default btn-sm has-tooltip star-btn', title: s_('ProjectOverview|You must sign in to star a project') do
|
||||
= sprite_icon('star-o', css_class: 'icon')
|
||||
%span= s_('ProjectOverview|Star')
|
||||
= link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'gl-button btn btn-default btn-sm star-count count' do
|
||||
= link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'gl-button btn btn-default btn-sm has-tooltip star-count count' do
|
||||
= @project.star_count
|
||||
|
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/362179
|
|||
milestone: '15.1'
|
||||
type: development
|
||||
group: group::static analysis
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
data_category: optional
|
||||
key_path: counts.groups_inheriting_pumble_active
|
||||
description: Count of active groups inheriting integrations for Pumble
|
||||
product_section: dev
|
||||
product_stage: ecosystem
|
||||
product_group: integrations
|
||||
product_category: integrations
|
||||
value_type: number
|
||||
status: active
|
||||
time_frame: all
|
||||
data_source: database
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
performance_indicator_type: []
|
||||
milestone: "15.3"
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
data_category: optional
|
||||
key_path: counts.groups_pumble_active
|
||||
description: Count of groups with active integrations for Pumble
|
||||
product_section: dev
|
||||
product_stage: ecosystem
|
||||
product_group: integrations
|
||||
product_category: integrations
|
||||
value_type: number
|
||||
status: active
|
||||
time_frame: all
|
||||
data_source: database
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
performance_indicator_type: []
|
||||
milestone: "15.3"
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
data_category: optional
|
||||
key_path: counts.instances_pumble_active
|
||||
description: Count of active instance-level integrations for Pumble
|
||||
product_section: dev
|
||||
product_stage: ecosystem
|
||||
product_group: integrations
|
||||
product_category: integrations
|
||||
value_type: number
|
||||
status: active
|
||||
time_frame: all
|
||||
data_source: database
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
performance_indicator_type: []
|
||||
milestone: "15.3"
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
data_category: optional
|
||||
key_path: counts.projects_inheriting_pumble_active
|
||||
description: Count of active projects inheriting integrations for Pumble
|
||||
product_section: dev
|
||||
product_stage: ecosystem
|
||||
product_group: integrations
|
||||
product_category: integrations
|
||||
value_type: number
|
||||
status: active
|
||||
time_frame: all
|
||||
data_source: database
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
performance_indicator_type: []
|
||||
milestone: "15.3"
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
data_category: optional
|
||||
key_path: counts.projects_pumble_active
|
||||
description: Count of projects with active integrations for Pumble
|
||||
product_section: dev
|
||||
product_stage: ecosystem
|
||||
product_group: integrations
|
||||
product_category: integrations
|
||||
value_type: number
|
||||
status: active
|
||||
time_frame: all
|
||||
data_source: database
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
performance_indicator_type: []
|
||||
milestone: "15.3"
|
|
@ -38,6 +38,7 @@ classes:
|
|||
- Integrations::PipelinesEmail
|
||||
- Integrations::Pivotaltracker
|
||||
- Integrations::Prometheus
|
||||
- Integrations::Pumble
|
||||
- Integrations::Pushover
|
||||
- Integrations::Redmine
|
||||
- Integrations::Shimo
|
||||
|
|
|
@ -46,6 +46,7 @@ required number of seconds.
|
|||
"user",
|
||||
"pipeline",
|
||||
"builds",
|
||||
"total_builds_count",
|
||||
"namespace"
|
||||
],
|
||||
"properties" : {
|
||||
|
@ -61,7 +62,9 @@ required number of seconds.
|
|||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"path": { "type": "string" },
|
||||
"created_at": { "type": ["string", "null"], "format": "date-time" }
|
||||
"created_at": { "type": ["string", "null"], "format": "date-time" },
|
||||
"shared_runners_enabled": { "type": "boolean" },
|
||||
"group_runners_enabled": { "type": "boolean" }
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
|
@ -121,6 +124,7 @@ required number of seconds.
|
|||
}
|
||||
}
|
||||
},
|
||||
"total_builds_count": { "type": "integer" },
|
||||
"namespace": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
|
|
@ -1077,6 +1077,9 @@ License.current.trial?
|
|||
|
||||
# License ID for lookup on CustomersDot
|
||||
License.current.license_id
|
||||
|
||||
# License data in Base64-encoded ASCII format
|
||||
License.current.data
|
||||
```
|
||||
|
||||
### Check if a project feature is available on the instance
|
||||
|
|
|
@ -20380,6 +20380,7 @@ State of a Sentry error.
|
|||
| <a id="servicetypepipelines_email_service"></a>`PIPELINES_EMAIL_SERVICE` | PipelinesEmailService type. |
|
||||
| <a id="servicetypepivotaltracker_service"></a>`PIVOTALTRACKER_SERVICE` | PivotaltrackerService type. |
|
||||
| <a id="servicetypeprometheus_service"></a>`PROMETHEUS_SERVICE` | PrometheusService type. |
|
||||
| <a id="servicetypepumble_service"></a>`PUMBLE_SERVICE` | PumbleService type. |
|
||||
| <a id="servicetypepushover_service"></a>`PUSHOVER_SERVICE` | PushoverService type. |
|
||||
| <a id="servicetyperedmine_service"></a>`REDMINE_SERVICE` | RedmineService type. |
|
||||
| <a id="servicetypeshimo_service"></a>`SHIMO_SERVICE` | ShimoService type. |
|
||||
|
@ -20719,8 +20720,6 @@ Vulnerability sort values.
|
|||
| ----- | ----------- |
|
||||
| <a id="vulnerabilitysortdetected_asc"></a>`detected_asc` | Detection timestamp in ascending order. |
|
||||
| <a id="vulnerabilitysortdetected_desc"></a>`detected_desc` | Detection timestamp in descending order. |
|
||||
| <a id="vulnerabilitysortreport_type_asc"></a>`report_type_asc` | Report Type in ascending order. |
|
||||
| <a id="vulnerabilitysortreport_type_desc"></a>`report_type_desc` | Report Type in descending order. |
|
||||
| <a id="vulnerabilitysortseverity_asc"></a>`severity_asc` | Severity in ascending order. |
|
||||
| <a id="vulnerabilitysortseverity_desc"></a>`severity_desc` | Severity in descending order. |
|
||||
|
||||
|
|
|
@ -383,6 +383,51 @@ Get Unify Circuit integration settings for a project.
|
|||
GET /projects/:id/integrations/unify-circuit
|
||||
```
|
||||
|
||||
## Pumble
|
||||
|
||||
Pumble chat tool.
|
||||
|
||||
### Create/Edit Pumble integration
|
||||
|
||||
Set Pumble integration for a project.
|
||||
|
||||
```plaintext
|
||||
PUT /projects/:id/integrations/pumble
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `webhook` | string | true | The Pumble webhook. For example, `https://api.pumble.com/workspaces/x/...`. |
|
||||
| `branches_to_be_notified` | string | false | Branches to send notifications for. Valid options are `all`, `default`, `protected`, and `default_and_protected`. The default is `default`. |
|
||||
| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events. |
|
||||
| `confidential_note_events` | boolean | false | Enable notifications for confidential note events. |
|
||||
| `issues_events` | boolean | false | Enable notifications for issue events. |
|
||||
| `merge_requests_events` | boolean | false | Enable notifications for merge request events. |
|
||||
| `note_events` | boolean | false | Enable notifications for note events. |
|
||||
| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines. |
|
||||
| `pipeline_events` | boolean | false | Enable notifications for pipeline events. |
|
||||
| `push_events` | boolean | false | Enable notifications for push events. |
|
||||
| `tag_push_events` | boolean | false | Enable notifications for tag push events. |
|
||||
| `wiki_page_events` | boolean | false | Enable notifications for wiki page events. |
|
||||
|
||||
### Disable Pumble integration
|
||||
|
||||
Disable the Pumble integration for a project. Integration settings are preserved.
|
||||
|
||||
```plaintext
|
||||
DELETE /projects/:id/integrations/pumble
|
||||
```
|
||||
|
||||
### Get Pumble integration settings
|
||||
|
||||
Get Pumble integration settings for a project.
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/integrations/pumble
|
||||
```
|
||||
|
||||
## Webex Teams
|
||||
|
||||
Webex Teams collaboration tool.
|
||||
|
|
|
@ -306,9 +306,19 @@ We also need to build a proof of concept for removing data on the PostgreSQL
|
|||
side (using foreign keys with `ON DELETE CASCADE`) and removing data through
|
||||
Rails associations, as this might be an important area of uncertainty.
|
||||
|
||||
We need to [better understand](https://gitlab.com/gitlab-org/gitlab/-/issues/360148)
|
||||
how unique constraints we are currently using will perform when using the
|
||||
partitioned schema.
|
||||
We [learned](https://gitlab.com/gitlab-org/gitlab/-/issues/360148) that `PostgreSQL`
|
||||
does not allow to create a single index (unique or otherwise) across all partitions of a table.
|
||||
|
||||
One solution to solve this problem is to embed the partitioning key inside the uniqueness constraint.
|
||||
|
||||
This might mean prepending the partition ID in a hexadecimal format before the token itself and storing
|
||||
the concatenated string in a database. To do that we would need to reserve an appropriate number of
|
||||
leading bytes in a token to accommodate for the maximum number of partitions we may have in the future.
|
||||
It seems that reserving four characters, what would translate into 16-bits number in base-16,
|
||||
might be sufficient. The maximum number we can encode this way would be FFFF, what is 65535 in decimal.
|
||||
|
||||
This would provide a unique constraint per-partition which
|
||||
is sufficient for global uniqueness.
|
||||
|
||||
We have also designed a query analyzer that makes it possible to detect direct
|
||||
usage of zero partitions, legacy tables that have been attached as first
|
||||
|
|
|
@ -269,9 +269,15 @@ Arguments:
|
|||
|
||||
#### Ordinary Redis counters
|
||||
|
||||
Example of implementation:
|
||||
Example of implementation: [`Gitlab::UsageDataCounters::WikiPageCounter`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters/wiki_page_counter.rb), using Redis methods [`INCR`](https://redis.io/commands/incr) and [`GET`](https://redis.io/commands/get).
|
||||
|
||||
Using Redis methods [`INCR`](https://redis.io/commands/incr/), [`GET`](https://redis.io/commands/get/), and [`Gitlab::UsageDataCounters::WikiPageCounter`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters/wiki_page_counter.rb)
|
||||
Events are handled by counter classes in the `Gitlab::UsageDataCounters` namespace, inheriting from `BaseCounter`, that are either:
|
||||
|
||||
1. Listed in [`Gitlab::UsageDataCounters::COUNTERS`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters.rb#L5) to be then included in `Gitlab::UsageData`.
|
||||
|
||||
1. Specified in the metric definition using the `RedisMetric` instrumentation class as a `counter_class` option to be picked up using the [metric instrumentation](metrics_instrumentation.md) framework. Refer to the [Redis metrics](metrics_instrumentation.md#redis-metrics) documentation for an example implementation.
|
||||
|
||||
Inheriting classes are expected to override `KNOWN_EVENTS` and `PREFIX` constants to build event names and associated metrics. For example, for prefix `issues` and events array `%w[create, update, delete]`, three metrics will be added to the Service Ping payload: `counts.issues_create`, `counts.issues_update` and `counts.issues_delete`.
|
||||
|
||||
##### `UsageData` API
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 35 KiB |
Binary file not shown.
Before Width: | Height: | Size: 21 KiB |
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
|
@ -4,103 +4,65 @@ group: Optimize
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Productivity Analytics **(PREMIUM)**
|
||||
# Productivity analytics **(PREMIUM)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12079) in GitLab 12.3.
|
||||
You can use productivity analytics to identify:
|
||||
|
||||
Track development velocity with Productivity Analytics.
|
||||
- Your development velocity based on how long it takes for a merge request to merge.
|
||||
- The most time consuming merge requests and potential causes.
|
||||
- Authors, labels, or milestones with the longest time to merge, or most changes.
|
||||
|
||||
For many companies, the development cycle is a black box and getting an estimate of how
|
||||
long, on average, it takes to deliver features is an enormous endeavor.
|
||||
Use productivity analytics to view the following merge request statistics for your groups:
|
||||
|
||||
While [Value Stream Analytics](../analytics/value_stream_analytics.md) focuses on the entire
|
||||
Software Development Life Cycle (SDLC) process, Productivity Analytics provides a way for Engineering Management to drill down in a systematic way to uncover patterns and causes for success or failure at an individual, project, or group level.
|
||||
- Amount of time between merge request creation and merge.
|
||||
- Amount of time between commits, comments, and merge.
|
||||
- Complexity of changes, like number of lines of code per commit and number of files.
|
||||
|
||||
Productivity can slow down for many reasons ranging from degrading codebase to quickly growing teams. To investigate, department or team leaders can start by visualizing the time it takes for merge requests to be merged.
|
||||
To view merge request data for projects, use [Merge request analytics](../analytics/merge_request_analytics.md).
|
||||
|
||||
## Visualizations and metrics
|
||||
## View productivity analytics
|
||||
|
||||
With Productivity Analytics, GitLab users can:
|
||||
Prerequisite:
|
||||
|
||||
- Visualize typical merge request (MR) lifetime and statistics. A histogram shows the distribution of the time elapsed between creating and merging merge requests.
|
||||
- Drill down into the most time consuming merge requests, select outliers, and filter subsequent charts to investigate potential causes.
|
||||
- Filter by group, project, author, label, milestone, or a specific date range. For example, filter down to the merge requests of a specific author in a group or project during a milestone or specific date range.
|
||||
- Measure velocity over time. To observe progress, visualize the trends of each metric from the charts over time. Zoom in on a particular date range if you notice outliers.
|
||||
- You must have at least the Reporter role for the group.
|
||||
|
||||
## Metrics charts
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Analytics > Productivity**.
|
||||
1. Optional. Filter results:
|
||||
1. Select a project from the dropdown list.
|
||||
1. To filter results by author, milestone, or label,
|
||||
select **Filter results...** and enter a value.
|
||||
1. To adjust the date range:
|
||||
- In the **From** field, select a start date.
|
||||
- In the **To** field, select an end date.
|
||||
|
||||
To access the charts, navigate to a group's sidebar and select **Analytics > Productivity Analytics**.
|
||||
Metrics and visualizations of **merged** merge requests are available on a project or group level.
|
||||
## View time metrics for merge requests
|
||||
|
||||
### Time to merge
|
||||
Use the following charts in productivity analytics to view the velocity of your merge requests:
|
||||
|
||||
The **Time to merge** histogram shows the number of merge requests and the number
|
||||
of days it took to merge after creation. Select a column to filter subsequent charts.
|
||||
- **Time to merge**: number of days it took for a
|
||||
merge requests to merge after they were created.
|
||||
- **Trendline**: number of merge requests that were merged in a specific time period.
|
||||
|
||||
![Metrics for number of days merge requests per number of days](img/productivity_analytics_time_to_merge_v14_4.png)
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Analytics > Productivity**.
|
||||
|
||||
### Trendline
|
||||
To filter time metrics:
|
||||
|
||||
The **Trendline** scatterplot shows all merge requests on a certain date,
|
||||
and the days it took to complete the action and a 30 day rolling median. Select the dropdown to view:
|
||||
1. To filter the **Trendline** chart, in the **Time to merge** chart, select a column.
|
||||
1. To view a specific merge request, below the charts, select a merge request from the **List**.
|
||||
|
||||
- Time from first commit to first comment.
|
||||
- Time from first comment until last commit.
|
||||
- Time from last commit to merge.
|
||||
- Number of commits per merge request.
|
||||
- Number of lines of code (LOC) per commit.
|
||||
- Number of files touched.
|
||||
## View commit statistics
|
||||
|
||||
![Metrics for amount of merge requests merged on a certain date](img/productivity_analytics_trendline_v14_4.png)
|
||||
To view commit statistics for your group:
|
||||
|
||||
### Commits and merge request size
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Analytics > Productivity**.
|
||||
1. Under the **Trendline** scatterplot, view the commit statistics:
|
||||
- The left histogram shows the number of hours between commits, comments, and merges.
|
||||
- The right histogram shows the number of commits and changes per merge request.
|
||||
|
||||
Under the **Trendline** scatterplot, the left-side histogram shows
|
||||
the time taken (in hours) between commits and comments until the merge
|
||||
request is merged. Select the dropdown to view:
|
||||
To filter commit statistics:
|
||||
|
||||
- Time from first commit to first comment.
|
||||
- Time from first comment until last commit.
|
||||
- Time from last commit to merge.
|
||||
|
||||
The right-side histogram shows the size or complexity of a merge request.
|
||||
Select the dropdown to view:
|
||||
|
||||
- Number of commits per merge request.
|
||||
- Number of lines of code (LOC) per commit.
|
||||
- Number of files touched.
|
||||
|
||||
![Metrics for amount of commits and complexity of changes per merge request.](img/product_analytics_commits_per_mr_v14_4.png)
|
||||
|
||||
### Merge request list
|
||||
|
||||
The **List** table shows a list of merge requests with their respective time duration metrics.
|
||||
|
||||
Sort metrics by:
|
||||
|
||||
- Time from first commit to first comment.
|
||||
- Time from first comment until last commit.
|
||||
- Time from last commit to merge.
|
||||
|
||||
Filter metrics by:
|
||||
|
||||
- Number of commits per merge request.
|
||||
- Number of lines of code per commit.
|
||||
- Number of files touched.
|
||||
|
||||
## Filter by date range
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13188) in GitLab 12.4.
|
||||
|
||||
You can filter analytics based on a date range. To filter results:
|
||||
|
||||
1. Select a group.
|
||||
1. Optional. Select a project.
|
||||
1. Select a date range by using the available date pickers.
|
||||
|
||||
## Permissions
|
||||
|
||||
The **Productivity Analytics** dashboard can be accessed only:
|
||||
|
||||
- On [GitLab Premium](https://about.gitlab.com/pricing/) and above.
|
||||
- By users with at least the Reporter role.
|
||||
1. To view different types of commit data, select the dropdown list next to each histogram.
|
||||
1. To view a specific merge request, below the charts, select a merge request from the **List**.
|
||||
|
|
|
@ -149,7 +149,7 @@ base address for Docker images. You can override this for most scanners by setti
|
|||
|
||||
The [Container Scanning](container_scanning/index.md) analyzer is an exception, and it
|
||||
does not use the `SECURE_ANALYZERS_PREFIX` variable. To override its Docker image, see
|
||||
the instructions for
|
||||
the instructions for
|
||||
[Running container scanning in an offline environment](container_scanning/index.md#running-container-scanning-in-an-offline-environment).
|
||||
|
||||
## Default behavior of GitLab security scanning tools
|
||||
|
@ -390,8 +390,10 @@ Validation depends on the schema version declared in the security report artifac
|
|||
|
||||
- If your security report specifies a supported schema version, GitLab uses this version to validate.
|
||||
- If your security report uses a deprecated version, GitLab attempts validation against that version and adds a deprecation warning to the validation result.
|
||||
- If your security report uses a version that is not supported, GitLab attempts to validate it against the latest schema version available in GitLab.
|
||||
- If your security report does not specify a schema version, GitLab attempts to validate it against the lastest schema version available in GitLab. Since the `version` property is required, validation always fails in this case, but other validation errors may also be present.
|
||||
- If your security report uses a supported MAJOR-MINOR version of the report schema but the PATCH version doesn't match any vendored versions, GitLab attempts to validate it against latest vendored PATCH version of the schema.
|
||||
- Example: security report uses version 14.1.1 but the latest vendored version is 14.1.0. GitLab would validate against schema version 14.1.0.
|
||||
- If your security report uses a version that is not supported, GitLab attempts to validate it against the latest schema version available in your installation but doesn't ingest the report.
|
||||
- If your security report does not specify a schema version, GitLab attempts to validate it against the latest schema version available in GitLab. Because the `version` property is required, validation always fails in this case, but other validation errors may also be present.
|
||||
|
||||
You can always find supported and deprecated schema versions in the [source code](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/parsers/security/validators/schema_validator.rb).
|
||||
|
||||
|
|
|
@ -73,6 +73,7 @@ You can configure the following integrations.
|
|||
| [Pipelines emails](pipeline_status_emails.md) | Send the pipeline status to a list of recipients by email. | **{dotted-circle}** No |
|
||||
| [Pivotal Tracker](pivotal_tracker.md) | Add commit messages as comments to Pivotal Tracker stories. | **{dotted-circle}** No |
|
||||
| [Prometheus](prometheus.md) | Monitor application metrics. | **{dotted-circle}** No |
|
||||
| [Pumble](pumble.md) | Send event notifications to a Pumble channel. | **{dotted-circle}** No |
|
||||
| Pushover | Get real-time notifications on your device. | **{dotted-circle}** No |
|
||||
| [Redmine](redmine.md) | Use Redmine as the issue tracker. | **{dotted-circle}** No |
|
||||
| [Slack application](gitlab_slack_application.md) | Use Slack's official GitLab application. | **{dotted-circle}** No |
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
stage: Ecosystem
|
||||
group: Integrations
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Pumble **(FREE)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93623) in GitLab 15.3.
|
||||
|
||||
You can configure GitLab to send notifications to a Pumble channel:
|
||||
|
||||
1. Create a webhook for the channel.
|
||||
1. Add the webhook to GitLab.
|
||||
|
||||
## Create a webhook for your Pumble channel
|
||||
|
||||
1. Follow the steps in [Incoming Webhooks for Pumble](https://pumble.com/help/integrations/custom-apps/incoming-webhooks-for-pumble/) in the Pumble documentation.
|
||||
1. Copy the webhook URL.
|
||||
|
||||
## Configure settings in GitLab
|
||||
|
||||
After you have a webhook URL for your Pumble channel, configure GitLab to send
|
||||
notifications:
|
||||
|
||||
1. To enable the integration for your group or project:
|
||||
1. In your group or project, on the left sidebar, select **Settings > Integrations**.
|
||||
1. To enable the integration for your instance:
|
||||
1. On the top bar, select **Menu > Admin**.
|
||||
1. On the left sidebar, select **Settings > Integrations**.
|
||||
1. Select the **Pumble** integration.
|
||||
1. Ensure that the **Active** toggle is enabled.
|
||||
1. Select the checkboxes corresponding to the GitLab events you want to receive in Pumble.
|
||||
1. Paste the **Webhook** URL for the Pumble channel.
|
||||
1. Configure the remaining options.
|
||||
1. Optional. To test the integration, select **Test settings**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
The Pumble channel begins to receive all applicable GitLab events.
|
|
@ -127,7 +127,7 @@ users were not explicitly listed in the approval rules.
|
|||
### Group approvers
|
||||
|
||||
You can add a group of users as approvers, but those users count as approvers only if
|
||||
they have direct membership to the group. Group approvers are
|
||||
they have **direct membership** to the group. Inherited members do not count. Group approvers are
|
||||
restricted to only groups [with share access to the project](../../members/share_project_with_groups.md).
|
||||
|
||||
A user's membership in an approvers group affects their individual ability to
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -678,6 +678,15 @@ module API
|
|||
desc: 'Contents of the credentials.json file of your service account, like: { "type": "service_account", "project_id": ... }'
|
||||
}
|
||||
],
|
||||
'pumble' => [
|
||||
{
|
||||
required: true,
|
||||
name: :webhook,
|
||||
type: String,
|
||||
desc: 'The Pumble chat webhook. For example, https://api.pumble.com/workspaces/x/...'
|
||||
},
|
||||
chat_notification_events
|
||||
].flatten,
|
||||
'pushover' => [
|
||||
{
|
||||
required: true,
|
||||
|
|
|
@ -37,7 +37,7 @@ module API
|
|||
requires :target_project_id, type: String, desc: 'The ID of the target project'
|
||||
requires :target_issue_iid, type: Integer, desc: 'The IID of the target issue'
|
||||
optional :link_type, type: String, values: IssueLink.link_types.keys,
|
||||
desc: 'The type of the relation'
|
||||
desc: 'The type of the relation'
|
||||
end
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
post ':id/issues/:issue_iid/links' do
|
||||
|
|
|
@ -16,7 +16,7 @@ module API
|
|||
optional :labels, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names'
|
||||
optional :milestone, type: String, desc: 'Milestone title'
|
||||
optional :milestone_id, types: String, values: %w[Any None Upcoming Started],
|
||||
desc: 'Return issues assigned to milestones without the specified timebox value ("Any", "None", "Upcoming" or "Started")'
|
||||
desc: 'Return issues assigned to milestones without the specified timebox value ("Any", "None", "Upcoming" or "Started")'
|
||||
mutually_exclusive :milestone_id, :milestone
|
||||
|
||||
optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IID array of issues'
|
||||
|
@ -27,8 +27,8 @@ module API
|
|||
|
||||
optional :assignee_id, type: Integer, desc: 'Return issues which are not assigned to the user with the given ID'
|
||||
optional :assignee_username, type: Array[String], check_assignees_count: true,
|
||||
coerce_with: Validations::Validators::CheckAssigneesCount.coerce,
|
||||
desc: 'Return issues which are not assigned to the user with the given username'
|
||||
coerce_with: Validations::Validators::CheckAssigneesCount.coerce,
|
||||
desc: 'Return issues which are not assigned to the user with the given username'
|
||||
mutually_exclusive :assignee_id, :assignee_username
|
||||
|
||||
use :negatable_issue_filter_params_ee
|
||||
|
@ -40,7 +40,7 @@ module API
|
|||
# 'milestone_id' only accepts wildcard values 'Any', 'None', 'Upcoming', 'Started'
|
||||
# the param has '_id' in the name to keep consistency (ex. assignee_id accepts id and wildcard values).
|
||||
optional :milestone_id, types: String, values: %w[Any None Upcoming Started],
|
||||
desc: 'Return issues assigned to milestones with the specified timebox value ("Any", "None", "Upcoming" or "Started")'
|
||||
desc: 'Return issues assigned to milestones with the specified timebox value ("Any", "None", "Upcoming" or "Started")'
|
||||
optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IID array of issues'
|
||||
optional :search, type: String, desc: 'Search issues for text present in the title, description, or any combination of these'
|
||||
optional :in, type: String, desc: '`title`, `description`, or a string joining them with comma'
|
||||
|
@ -51,10 +51,10 @@ module API
|
|||
mutually_exclusive :author_id, :author_username
|
||||
|
||||
optional :assignee_id, types: [Integer, String], integer_none_any: true,
|
||||
desc: 'Return issues which are assigned to the user with the given ID'
|
||||
desc: 'Return issues which are assigned to the user with the given ID'
|
||||
optional :assignee_username, type: Array[String], check_assignees_count: true,
|
||||
coerce_with: Validations::Validators::CheckAssigneesCount.coerce,
|
||||
desc: 'Return issues which are assigned to the user with the given username'
|
||||
coerce_with: Validations::Validators::CheckAssigneesCount.coerce,
|
||||
desc: 'Return issues which are assigned to the user with the given username'
|
||||
mutually_exclusive :assignee_id, :assignee_username
|
||||
|
||||
optional :created_after, type: DateTime, desc: 'Return issues created after the specified time'
|
||||
|
@ -77,13 +77,13 @@ module API
|
|||
params :issues_params do
|
||||
optional :with_labels_details, type: Boolean, desc: 'Return titles of labels and other details', default: false
|
||||
optional :state, type: String, values: %w[opened closed all], default: 'all',
|
||||
desc: 'Return opened, closed, or all issues'
|
||||
desc: 'Return opened, closed, or all issues'
|
||||
optional :order_by, type: String, values: Helpers::IssuesHelpers.sort_options, default: 'created_at',
|
||||
desc: 'Return issues ordered by `created_at`, `due_date`, `label_priority`, `milestone_due`, `popularity`, `priority`, `relative_position`, `title`, or `updated_at` fields.'
|
||||
desc: 'Return issues ordered by `created_at`, `due_date`, `label_priority`, `milestone_due`, `popularity`, `priority`, `relative_position`, `title`, or `updated_at` fields.'
|
||||
optional :sort, type: String, values: %w[asc desc], default: 'desc',
|
||||
desc: 'Return issues sorted in `asc` or `desc` order.'
|
||||
desc: 'Return issues sorted in `asc` or `desc` order.'
|
||||
optional :due_date, type: String, values: %w[0 any today tomorrow overdue week month next_month_and_previous_two_weeks] << '',
|
||||
desc: 'Return issues that have no due date (`0`), or whose due date is this week, this month, between two weeks ago and next month, or which are overdue. Accepts: `overdue`, `week`, `month`, `next_month_and_previous_two_weeks`, `0`'
|
||||
desc: 'Return issues that have no due date (`0`), or whose due date is this week, this month, between two weeks ago and next month, or which are overdue. Accepts: `overdue`, `week`, `month`, `next_month_and_previous_two_weeks`, `0`'
|
||||
optional :issue_type, type: String, values: WorkItems::Type.allowed_types_for_issues, desc: "The type of the issue. Accepts: #{WorkItems::Type.allowed_types_for_issues.join(', ')}"
|
||||
|
||||
use :issues_stats_params
|
||||
|
|
|
@ -23,11 +23,11 @@ module API
|
|||
end
|
||||
params do
|
||||
optional :with_counts, type: Boolean, default: false,
|
||||
desc: 'Include issue and merge request counts'
|
||||
desc: 'Include issue and merge request counts'
|
||||
optional :include_ancestor_groups, type: Boolean, default: true,
|
||||
desc: 'Include ancestor groups'
|
||||
desc: 'Include ancestor groups'
|
||||
optional :search, type: String,
|
||||
desc: 'Keyword to filter labels by. This feature was added in GitLab 13.6'
|
||||
desc: 'Keyword to filter labels by. This feature was added in GitLab 13.6'
|
||||
use :pagination
|
||||
end
|
||||
get ':id/labels' do
|
||||
|
@ -40,7 +40,7 @@ module API
|
|||
end
|
||||
params do
|
||||
optional :include_ancestor_groups, type: Boolean, default: true,
|
||||
desc: 'Include ancestor groups'
|
||||
desc: 'Include ancestor groups'
|
||||
end
|
||||
get ':id/labels/:name' do
|
||||
get_label(user_project, Entities::ProjectLabel, declared_params)
|
||||
|
|
|
@ -283,12 +283,12 @@ module API
|
|||
''
|
||||
else
|
||||
file_params = {
|
||||
file: params[:file],
|
||||
size: params['file.size'],
|
||||
file: params[:file],
|
||||
size: params['file.size'],
|
||||
file_name: file_name,
|
||||
file_type: params['file.type'],
|
||||
file_sha1: params['file.sha1'],
|
||||
file_md5: params['file.md5']
|
||||
file_md5: params['file.md5']
|
||||
}
|
||||
|
||||
::Packages::CreatePackageFileService.new(package, file_params.merge(build: current_authenticated_job)).execute
|
||||
|
|
|
@ -156,9 +156,9 @@ module API
|
|||
params do
|
||||
requires :user_id, type: Integer, desc: 'The user ID of the member'
|
||||
optional :skip_subresources, type: Boolean, default: false,
|
||||
desc: 'Flag indicating if the deletion of direct memberships of the removed member in subgroups and projects should be skipped'
|
||||
desc: 'Flag indicating if the deletion of direct memberships of the removed member in subgroups and projects should be skipped'
|
||||
optional :unassign_issuables, type: Boolean, default: false,
|
||||
desc: 'Flag indicating if the removed member should be unassigned from any issues or merge requests within given group or project'
|
||||
desc: 'Flag indicating if the removed member should be unassigned from any issues or merge requests within given group or project'
|
||||
end
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
delete ":id/members/:user_id", feature_category: feature_category do
|
||||
|
|
|
@ -159,7 +159,7 @@ module API
|
|||
params do
|
||||
use :merge_requests_params
|
||||
optional :non_archived, type: Boolean, desc: 'Return merge requests from non archived projects',
|
||||
default: true
|
||||
default: true
|
||||
end
|
||||
get ":id/merge_requests", feature_category: :code_review, urgency: :low do
|
||||
validate_anonymous_search_access! if declared_params[:search].present?
|
||||
|
|
|
@ -20,11 +20,11 @@ module API
|
|||
resource annotations_source[:resource] do
|
||||
params do
|
||||
requires :starting_at, type: DateTime,
|
||||
desc: 'Date time indicating starting moment to which the annotation relates.'
|
||||
desc: 'Date time indicating starting moment to which the annotation relates.'
|
||||
optional :ending_at, type: DateTime,
|
||||
desc: 'Date time indicating ending moment to which the annotation relates.'
|
||||
desc: 'Date time indicating ending moment to which the annotation relates.'
|
||||
requires :dashboard_path, type: String, coerce_with: -> (val) { CGI.unescape(val) },
|
||||
desc: 'The path to a file defining the dashboard on which the annotation should be added'
|
||||
desc: 'The path to a file defining the dashboard on which the annotation should be added'
|
||||
requires :description, type: String, desc: 'The description of the annotation'
|
||||
end
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ module API
|
|||
|
||||
params do
|
||||
requires :dashboard_path, type: String, allow_blank: false, coerce_with: ->(val) { CGI.unescape(val) },
|
||||
desc: 'Url encoded path to a file defining the dashboard to which the star should be added'
|
||||
desc: 'Url encoded path to a file defining the dashboard to which the star should be added'
|
||||
end
|
||||
|
||||
post ':id/metrics/user_starred_dashboards' do
|
||||
|
@ -30,7 +30,7 @@ module API
|
|||
|
||||
params do
|
||||
optional :dashboard_path, type: String, allow_blank: false, coerce_with: ->(val) { CGI.unescape(val) },
|
||||
desc: 'Url encoded path to a file defining the dashboard from which the star should be removed'
|
||||
desc: 'Url encoded path to a file defining the dashboard from which the star should be removed'
|
||||
end
|
||||
|
||||
delete ':id/metrics/user_starred_dashboards' do
|
||||
|
|
|
@ -14,12 +14,12 @@ module API
|
|||
|
||||
params :list_params do
|
||||
optional :state, type: String, values: %w[active closed all], default: 'all',
|
||||
desc: 'Return "active", "closed", or "all" milestones'
|
||||
desc: 'Return "active", "closed", or "all" milestones'
|
||||
optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IIDs of the milestones'
|
||||
optional :title, type: String, desc: 'The title of the milestones'
|
||||
optional :search, type: String, desc: 'The search criteria for the title or description of the milestone'
|
||||
optional :include_parent_milestones, type: Grape::API::Boolean, default: false,
|
||||
desc: 'Include group milestones from parent and its ancestors'
|
||||
desc: 'Include group milestones from parent and its ancestors'
|
||||
use :pagination
|
||||
end
|
||||
|
||||
|
@ -27,7 +27,7 @@ module API
|
|||
requires :milestone_id, type: Integer, desc: 'The milestone ID number'
|
||||
optional :title, type: String, desc: 'The title of the milestone'
|
||||
optional :state_event, type: String, values: %w[close activate],
|
||||
desc: 'The state event of the milestone '
|
||||
desc: 'The state event of the milestone '
|
||||
use :optional_params
|
||||
at_least_one_of :title, :description, :start_date, :due_date, :state_event
|
||||
end
|
||||
|
|
|
@ -30,7 +30,7 @@ module API
|
|||
optional :sort, type: String, values: %w[asc desc], default: 'desc',
|
||||
desc: 'Return notes sorted in `asc` or `desc` order.'
|
||||
optional :activity_filter, type: String, values: UserPreference::NOTES_FILTERS.stringify_keys.keys, default: 'all_notes',
|
||||
desc: 'The type of notables which are returned.'
|
||||
desc: 'The type of notables which are returned.'
|
||||
use :pagination
|
||||
end
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
|
|
|
@ -97,7 +97,7 @@ module API
|
|||
optional :certificate, types: [File, String], desc: 'The certificate', as: :user_provided_certificate
|
||||
optional :key, types: [File, String], desc: 'The key', as: :user_provided_key
|
||||
optional :auto_ssl_enabled, allow_blank: false, type: Boolean, default: false,
|
||||
desc: "Enables automatic generation of SSL certificates issued by Let's Encrypt for custom domains."
|
||||
desc: "Enables automatic generation of SSL certificates issued by Let's Encrypt for custom domains."
|
||||
# rubocop:enable Scalability/FileUploads
|
||||
all_or_none_of :user_provided_certificate, :user_provided_key
|
||||
end
|
||||
|
@ -123,7 +123,7 @@ module API
|
|||
optional :certificate, types: [File, String], desc: 'The certificate', as: :user_provided_certificate
|
||||
optional :key, types: [File, String], desc: 'The key', as: :user_provided_key
|
||||
optional :auto_ssl_enabled, allow_blank: true, type: Boolean,
|
||||
desc: "Enables automatic generation of SSL certificates issued by Let's Encrypt for custom domains."
|
||||
desc: "Enables automatic generation of SSL certificates issued by Let's Encrypt for custom domains."
|
||||
# rubocop:enable Scalability/FileUploads
|
||||
end
|
||||
put ":id/pages/domains/:domain", requirements: PAGES_DOMAINS_ENDPOINT_REQUIREMENTS do
|
||||
|
|
|
@ -32,9 +32,9 @@ module API
|
|||
optional :package_name, type: String,
|
||||
desc: 'Return packages with this name'
|
||||
optional :include_versionless, type: Boolean,
|
||||
desc: 'Returns packages without a version'
|
||||
desc: 'Returns packages without a version'
|
||||
optional :status, type: String, values: Packages::Package.statuses.keys,
|
||||
desc: 'Return packages with specified status'
|
||||
desc: 'Return packages with specified status'
|
||||
end
|
||||
get ':id/packages' do
|
||||
packages = ::Packages::PackagesFinder.new(
|
||||
|
|
|
@ -37,7 +37,7 @@ module API
|
|||
params do
|
||||
requires :name, type: String, desc: 'The name of the template'
|
||||
optional :source_template_project_id, type: Integer,
|
||||
desc: 'The project id where a given template is being stored. This is useful when multiple templates from different projects have the same name'
|
||||
desc: 'The project id where a given template is being stored. This is useful when multiple templates from different projects have the same name'
|
||||
optional :project, type: String, desc: 'The project name to use when expanding placeholders in the template. Only affects licenses'
|
||||
optional :fullname, type: String, desc: 'The full name of the copyright holder to use when expanding placeholders in the template. Only affects licenses'
|
||||
end
|
||||
|
|
|
@ -688,11 +688,11 @@ module API
|
|||
optional :search, type: String, desc: 'Return list of groups matching the search criteria'
|
||||
optional :skip_groups, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Array of group ids to exclude from list'
|
||||
optional :with_shared, type: Boolean, default: false,
|
||||
desc: 'Include shared groups'
|
||||
desc: 'Include shared groups'
|
||||
optional :shared_visible_only, type: Boolean, default: false,
|
||||
desc: 'Limit to shared groups user has access to'
|
||||
desc: 'Limit to shared groups user has access to'
|
||||
optional :shared_min_access_level, type: Integer, values: Gitlab::Access.all_values,
|
||||
desc: 'Limit returned shared groups by minimum access level to the project'
|
||||
desc: 'Limit returned shared groups by minimum access level to the project'
|
||||
use :pagination
|
||||
end
|
||||
get ':id/groups', feature_category: :source_code_management do
|
||||
|
|
|
@ -61,8 +61,8 @@ module API
|
|||
values: ProtectedBranch::MergeAccessLevel.allowed_access_levels,
|
||||
desc: 'Access levels allowed to merge (defaults: `40`, maintainer access level)'
|
||||
optional :allow_force_push, type: Boolean,
|
||||
default: false,
|
||||
desc: 'Allow force push for all users with push access.'
|
||||
default: false,
|
||||
desc: 'Allow force push for all users with push access.'
|
||||
|
||||
use :optional_params_ee
|
||||
end
|
||||
|
|
|
@ -23,9 +23,9 @@ module API
|
|||
params do
|
||||
requires :id, type: Integer, desc: 'The ID of the group to get releases for'
|
||||
optional :sort, type: String, values: %w[asc desc], default: 'desc',
|
||||
desc: 'Return projects sorted in ascending and descending order by released_at'
|
||||
desc: 'Return projects sorted in ascending and descending order by released_at'
|
||||
optional :simple, type: Boolean, default: false,
|
||||
desc: 'Return only the ID, URL, name, and path of each project'
|
||||
desc: 'Return only the ID, URL, name, and path of each project'
|
||||
|
||||
use :pagination
|
||||
end
|
||||
|
@ -61,7 +61,7 @@ module API
|
|||
optional :sort, type: String, values: %w[asc desc], default: 'desc',
|
||||
desc: 'Return releases sorted in `asc` or `desc` order.'
|
||||
optional :include_html_description, type: Boolean,
|
||||
desc: 'If `true`, a response includes HTML rendered markdown of the release description.'
|
||||
desc: 'If `true`, a response includes HTML rendered markdown of the release description.'
|
||||
end
|
||||
route_setting :authentication, job_token_allowed: true
|
||||
get ':id/releases' do
|
||||
|
@ -89,7 +89,7 @@ module API
|
|||
params do
|
||||
requires :tag_name, type: String, desc: 'The name of the tag', as: :tag
|
||||
optional :include_html_description, type: Boolean,
|
||||
desc: 'If `true`, a response includes HTML rendered markdown of the release description.'
|
||||
desc: 'If `true`, a response includes HTML rendered markdown of the release description.'
|
||||
end
|
||||
route_setting :authentication, job_token_allowed: true
|
||||
get ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMENTS do
|
||||
|
|
|
@ -109,7 +109,7 @@ module API
|
|||
).execute(:rubygems, name: ::Packages::Rubygems::TEMPORARY_PACKAGE_NAME)
|
||||
|
||||
file_params = {
|
||||
file: params[:file],
|
||||
file: params[:file],
|
||||
file_name: PACKAGE_FILENAME
|
||||
}
|
||||
|
||||
|
|
|
@ -22,14 +22,14 @@ module API
|
|||
def process_metrics
|
||||
Sidekiq::ProcessSet.new(false).map do |process|
|
||||
{
|
||||
hostname: process['hostname'],
|
||||
pid: process['pid'],
|
||||
tag: process['tag'],
|
||||
started_at: Time.at(process['started_at']),
|
||||
queues: process['queues'],
|
||||
labels: process['labels'],
|
||||
hostname: process['hostname'],
|
||||
pid: process['pid'],
|
||||
tag: process['tag'],
|
||||
started_at: Time.at(process['started_at']),
|
||||
queues: process['queues'],
|
||||
labels: process['labels'],
|
||||
concurrency: process['concurrency'],
|
||||
busy: process['busy']
|
||||
busy: process['busy']
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -68,9 +68,9 @@ module API
|
|||
|
||||
params :sort_params do
|
||||
optional :order_by, type: String, values: %w[id name username created_at updated_at],
|
||||
default: 'id', desc: 'Return users ordered by a field'
|
||||
default: 'id', desc: 'Return users ordered by a field'
|
||||
optional :sort, type: String, values: %w[asc desc], default: 'desc',
|
||||
desc: 'Return users sorted in ascending and descending order'
|
||||
desc: 'Return users sorted in ascending and descending order'
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -940,7 +940,7 @@ module API
|
|||
params do
|
||||
requires :name, type: String, desc: 'The name of the personal access token'
|
||||
requires :scopes, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, values: ::Gitlab::Auth.all_available_scopes.map(&:to_s),
|
||||
desc: 'The array of scopes of the personal access token'
|
||||
desc: 'The array of scopes of the personal access token'
|
||||
optional :expires_at, type: Date, desc: 'The expiration date in the format YEAR-MONTH-DAY of the personal access token'
|
||||
end
|
||||
post feature_category: :authentication_and_authorization do
|
||||
|
|
|
@ -94,7 +94,7 @@ module Backup
|
|||
def build_env
|
||||
{
|
||||
'SSL_CERT_FILE' => Gitlab::X509::Certificate.default_cert_file,
|
||||
'SSL_CERT_DIR' => Gitlab::X509::Certificate.default_cert_dir
|
||||
'SSL_CERT_DIR' => Gitlab::X509::Certificate.default_cert_dir
|
||||
}.merge(ENV)
|
||||
end
|
||||
|
||||
|
|
|
@ -240,11 +240,11 @@ module Banzai
|
|||
object_parent_type = parent.is_a?(Group) ? :group : :project
|
||||
|
||||
{
|
||||
original: escape_html_entities(text),
|
||||
link: link_content,
|
||||
link_reference: link_reference,
|
||||
original: escape_html_entities(text),
|
||||
link: link_content,
|
||||
link_reference: link_reference,
|
||||
object_parent_type => parent.id,
|
||||
object_sym => object.id
|
||||
object_sym => object.id
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -41,8 +41,8 @@ module Banzai
|
|||
nodes_for_document = entry[:nodes]
|
||||
|
||||
doc_data = {
|
||||
document: entry[:document],
|
||||
total_reference_count: nodes_for_document.count,
|
||||
document: entry[:document],
|
||||
total_reference_count: nodes_for_document.count,
|
||||
visible_reference_count: nodes_for_document.count
|
||||
}
|
||||
|
||||
|
|
|
@ -38,13 +38,14 @@ module Gitlab
|
|||
def initialize(report_type, report_version)
|
||||
@report_type = report_type.to_sym
|
||||
@report_version = report_version.to_s
|
||||
@supported_versions = SUPPORTED_VERSIONS[@report_type]
|
||||
end
|
||||
|
||||
delegate :validate, to: :schemer
|
||||
|
||||
private
|
||||
|
||||
attr_reader :report_type, :report_version
|
||||
attr_reader :report_type, :report_version, :supported_versions
|
||||
|
||||
def schemer
|
||||
JSONSchemer.schema(pathname)
|
||||
|
@ -60,10 +61,24 @@ module Gitlab
|
|||
report_declared_version = File.join(root_path, report_version, file_name)
|
||||
return report_declared_version if File.file?(report_declared_version)
|
||||
|
||||
if latest_vendored_patch_version
|
||||
latest_vendored_patch_version_file = File.join(root_path, latest_vendored_patch_version, file_name)
|
||||
return latest_vendored_patch_version_file if File.file?(latest_vendored_patch_version)
|
||||
end
|
||||
|
||||
earliest_supported_version = SUPPORTED_VERSIONS[report_type].min
|
||||
File.join(root_path, earliest_supported_version, file_name)
|
||||
end
|
||||
|
||||
def latest_vendored_patch_version
|
||||
::Security::ReportSchemaVersionMatcher.new(
|
||||
report_declared_version: report_version,
|
||||
supported_versions: supported_versions
|
||||
).call
|
||||
rescue ArgumentError
|
||||
nil
|
||||
end
|
||||
|
||||
def file_name
|
||||
report_type == :api_fuzzing ? "dast-report-format.json" : "#{report_type.to_s.dasherize}-report-format.json"
|
||||
end
|
||||
|
@ -79,16 +94,80 @@ module Gitlab
|
|||
@warnings = []
|
||||
@deprecation_warnings = []
|
||||
|
||||
populate_errors
|
||||
populate_warnings
|
||||
populate_schema_version_errors
|
||||
populate_validation_errors
|
||||
populate_deprecation_warnings
|
||||
end
|
||||
|
||||
def valid?
|
||||
errors.empty?
|
||||
def populate_schema_version_errors
|
||||
add_schema_version_errors if add_schema_version_error?
|
||||
end
|
||||
|
||||
def populate_errors
|
||||
def add_schema_version_errors
|
||||
if report_version.nil?
|
||||
template = _("Report version not provided,"\
|
||||
" %{report_type} report type supports versions: %{supported_schema_versions}."\
|
||||
" GitLab will attempt to validate this report against the earliest supported versions of this report"\
|
||||
" type, to show all the errors but will not ingest the report")
|
||||
message = format(template, report_type: report_type, supported_schema_versions: supported_schema_versions)
|
||||
else
|
||||
template = _("Version %{report_version} for report type %{report_type} is unsupported, supported versions"\
|
||||
" for this report type are: %{supported_schema_versions}."\
|
||||
" GitLab will attempt to validate this report against the earliest supported versions of this report"\
|
||||
" type, to show all the errors but will not ingest the report")
|
||||
message = format(template, report_version: report_version, report_type: report_type, supported_schema_versions: supported_schema_versions)
|
||||
end
|
||||
|
||||
log_warnings(problem_type: 'using_unsupported_schema_version')
|
||||
add_message_as(level: :error, message: message)
|
||||
end
|
||||
|
||||
def add_schema_version_error?
|
||||
!report_uses_supported_schema_version? &&
|
||||
!report_uses_deprecated_schema_version? &&
|
||||
!report_uses_supported_major_and_minor_schema_version?
|
||||
end
|
||||
|
||||
def report_uses_deprecated_schema_version?
|
||||
DEPRECATED_VERSIONS[report_type].include?(report_version)
|
||||
end
|
||||
|
||||
def report_uses_supported_schema_version?
|
||||
SUPPORTED_VERSIONS[report_type].include?(report_version)
|
||||
end
|
||||
|
||||
def report_uses_supported_major_and_minor_schema_version?
|
||||
if !find_latest_patch_version.nil?
|
||||
add_supported_major_minor_behavior_warning
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def find_latest_patch_version
|
||||
::Security::ReportSchemaVersionMatcher.new(
|
||||
report_declared_version: report_version,
|
||||
supported_versions: SUPPORTED_VERSIONS[report_type]
|
||||
).call
|
||||
rescue ArgumentError
|
||||
nil
|
||||
end
|
||||
|
||||
def add_supported_major_minor_behavior_warning
|
||||
template = _("This report uses a supported MAJOR.MINOR schema version but the PATCH version doesn't match"\
|
||||
" any vendored schema version. Validation will be attempted against version"\
|
||||
" %{find_latest_patch_version}")
|
||||
|
||||
message = format(template, find_latest_patch_version: find_latest_patch_version)
|
||||
|
||||
add_message_as(
|
||||
level: :warning,
|
||||
message: message
|
||||
)
|
||||
end
|
||||
|
||||
def populate_validation_errors
|
||||
schema_validation_errors = schema.validate(report_data).map { |error| JSONSchemer::Errors.pretty(error) }
|
||||
|
||||
log_warnings(problem_type: 'schema_validation_fails') unless schema_validation_errors.empty?
|
||||
|
@ -96,10 +175,6 @@ module Gitlab
|
|||
@errors += schema_validation_errors
|
||||
end
|
||||
|
||||
def populate_warnings
|
||||
add_unsupported_report_version_message if !report_uses_supported_schema_version? && !report_uses_deprecated_schema_version?
|
||||
end
|
||||
|
||||
def populate_deprecation_warnings
|
||||
add_deprecated_report_version_message if report_uses_deprecated_schema_version?
|
||||
end
|
||||
|
@ -107,10 +182,19 @@ module Gitlab
|
|||
def add_deprecated_report_version_message
|
||||
log_warnings(problem_type: 'using_deprecated_schema_version')
|
||||
|
||||
message = "Version #{report_version} for report type #{report_type} has been deprecated, supported versions for this report type are: #{supported_schema_versions}"
|
||||
template = _("Version %{report_version} for report type %{report_type} has been deprecated,"\
|
||||
" supported versions for this report type are: %{supported_schema_versions}."\
|
||||
" GitLab will attempt to parse and ingest this report if valid.")
|
||||
|
||||
message = format(template, report_version: report_version, report_type: report_type, supported_schema_versions: supported_schema_versions)
|
||||
|
||||
add_message_as(level: :deprecation_warning, message: message)
|
||||
end
|
||||
|
||||
def valid?
|
||||
errors.empty?
|
||||
end
|
||||
|
||||
def log_warnings(problem_type:)
|
||||
Gitlab::AppLogger.info(
|
||||
message: 'security report schema validation problem',
|
||||
|
@ -123,30 +207,6 @@ module Gitlab
|
|||
)
|
||||
end
|
||||
|
||||
def add_unsupported_report_version_message
|
||||
log_warnings(problem_type: 'using_unsupported_schema_version')
|
||||
|
||||
handle_unsupported_report_version
|
||||
end
|
||||
|
||||
def report_uses_deprecated_schema_version?
|
||||
DEPRECATED_VERSIONS[report_type].include?(report_version)
|
||||
end
|
||||
|
||||
def report_uses_supported_schema_version?
|
||||
SUPPORTED_VERSIONS[report_type].include?(report_version)
|
||||
end
|
||||
|
||||
def handle_unsupported_report_version
|
||||
if report_version.nil?
|
||||
message = "Report version not provided, #{report_type} report type supports versions: #{supported_schema_versions}"
|
||||
else
|
||||
message = "Version #{report_version} for report type #{report_type} is unsupported, supported versions for this report type are: #{supported_schema_versions}"
|
||||
end
|
||||
|
||||
add_message_as(level: :error, message: message)
|
||||
end
|
||||
|
||||
def supported_schema_versions
|
||||
SUPPORTED_VERSIONS[report_type].join(", ")
|
||||
end
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
module Security
|
||||
class ReportSchemaVersionMatcher
|
||||
def initialize(report_declared_version:, supported_versions:)
|
||||
@report_version = Gem::Version.new(report_declared_version)
|
||||
@supported_versions = supported_versions.sort.map { |version| Gem::Version.new(version) }
|
||||
end
|
||||
|
||||
attr_reader :report_version, :supported_versions
|
||||
|
||||
def call
|
||||
find_matching_versions
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_matching_versions
|
||||
dependency = Gem::Dependency.new('', approximate_version)
|
||||
matches = supported_versions.map do |supported_version|
|
||||
exact_version = ['', supported_version.to_s]
|
||||
[supported_version.to_s, dependency.match?(*exact_version)]
|
||||
end
|
||||
matches.to_h.select { |_, matches_dependency| matches_dependency == true }.keys.max
|
||||
end
|
||||
|
||||
def approximate_version
|
||||
"~> #{generate_patch_version}"
|
||||
end
|
||||
|
||||
def generate_patch_version
|
||||
# We can't use #approximate_recommendation here because
|
||||
# for "14.0.32" it would yield "~> 14.0" and according to
|
||||
# https://www.rubydoc.info/github/rubygems/rubygems/Gem/Version#label-Preventing+Version+Catastrophe-3A
|
||||
# "~> 3.0" covers [3.0...4.0)
|
||||
# and version 14.1.0 would fall within that range
|
||||
#
|
||||
# Instead we replace the patch number with 0 and get "~> 14.0.0"
|
||||
# Which will work as we want it to
|
||||
(report_version.segments[0...2] << 0).join('.')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -31894,6 +31894,12 @@ msgstr ""
|
|||
msgid "Puma is running with a thread count above 1 and the Rugged service is enabled. This may decrease performance in some environments. See our %{link_start}documentation%{link_end} for details of this issue."
|
||||
msgstr ""
|
||||
|
||||
msgid "PumbleIntegration|Send notifications about project events to Pumble."
|
||||
msgstr ""
|
||||
|
||||
msgid "PumbleIntegration|Send notifications about project events to Pumble. %{docs_link}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Purchase more minutes"
|
||||
msgstr ""
|
||||
|
||||
|
@ -32837,6 +32843,9 @@ msgstr ""
|
|||
msgid "Report for the scan has been removed from the database."
|
||||
msgstr ""
|
||||
|
||||
msgid "Report version not provided, %{report_type} report type supports versions: %{supported_schema_versions}. GitLab will attempt to validate this report against the earliest supported versions of this report type, to show all the errors but will not ingest the report"
|
||||
msgstr ""
|
||||
|
||||
msgid "Report your license usage data to GitLab"
|
||||
msgstr ""
|
||||
|
||||
|
@ -40324,6 +40333,9 @@ msgstr ""
|
|||
msgid "This release was created with a date in the past. Evidence collection at the moment of the release is unavailable."
|
||||
msgstr ""
|
||||
|
||||
msgid "This report uses a supported MAJOR.MINOR schema version but the PATCH version doesn't match any vendored schema version. Validation will be attempted against version %{find_latest_patch_version}"
|
||||
msgstr ""
|
||||
|
||||
msgid "This repository"
|
||||
msgstr ""
|
||||
|
||||
|
@ -42970,6 +42982,12 @@ msgstr ""
|
|||
msgid "Version"
|
||||
msgstr ""
|
||||
|
||||
msgid "Version %{report_version} for report type %{report_type} has been deprecated, supported versions for this report type are: %{supported_schema_versions}. GitLab will attempt to parse and ingest this report if valid."
|
||||
msgstr ""
|
||||
|
||||
msgid "Version %{report_version} for report type %{report_type} is unsupported, supported versions for this report type are: %{supported_schema_versions}. GitLab will attempt to validate this report against the earliest supported versions of this report type, to show all the errors but will not ingest the report"
|
||||
msgstr ""
|
||||
|
||||
msgid "Version %{versionNumber}"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -51,8 +51,8 @@
|
|||
"@babel/preset-env": "^7.18.2",
|
||||
"@gitlab/at.js": "1.5.7",
|
||||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/svgs": "2.33.0",
|
||||
"@gitlab/ui": "42.25.0",
|
||||
"@gitlab/svgs": "3.1.0",
|
||||
"@gitlab/ui": "43.5.0",
|
||||
"@gitlab/visual-review-tools": "1.7.3",
|
||||
"@rails/actioncable": "6.1.4-7",
|
||||
"@rails/ujs": "6.1.4-7",
|
||||
|
|
|
@ -182,12 +182,14 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
|
|||
context 'email confirmation disabled' do
|
||||
let(:send_email_confirmation) { false }
|
||||
|
||||
it 'signs up and redirects to the most recent membership activity page with all the projects/groups invitations automatically accepted' do
|
||||
fill_in_sign_up_form(new_user)
|
||||
fill_in_welcome_form
|
||||
context 'the user signs up for an account with the invitation email address' do
|
||||
it 'redirects to the most recent membership activity page with all the projects/groups invitations automatically accepted' do
|
||||
fill_in_sign_up_form(new_user)
|
||||
fill_in_welcome_form
|
||||
|
||||
expect(page).to have_current_path(activity_group_path(group), ignore_query: true)
|
||||
expect(page).to have_content('You have been granted Owner access to group Owned.')
|
||||
expect(page).to have_current_path(activity_group_path(group), ignore_query: true)
|
||||
expect(page).to have_content('You have been granted Owner access to group Owned.')
|
||||
end
|
||||
end
|
||||
|
||||
context 'the user sign-up using a different email address' do
|
||||
|
@ -227,11 +229,13 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
|
|||
end
|
||||
end
|
||||
|
||||
it 'signs up and redirects to the group activity page with all the project/groups invitation automatically accepted' do
|
||||
fill_in_sign_up_form(new_user)
|
||||
fill_in_welcome_form
|
||||
context 'the user signs up for an account with the invitation email address' do
|
||||
it 'redirects to the most recent membership activity page with all the projects/groups invitations automatically accepted' do
|
||||
fill_in_sign_up_form(new_user)
|
||||
fill_in_welcome_form
|
||||
|
||||
expect(page).to have_current_path(activity_group_path(group), ignore_query: true)
|
||||
expect(page).to have_current_path(activity_group_path(group), ignore_query: true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'the user sign-up using a different email address' do
|
||||
|
|
|
@ -85,7 +85,46 @@ RSpec.describe 'OAuth Registration', :js, :allow_forgery_protection do
|
|||
expect(page).to have_content('Please complete your profile with email address')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when registering via an invitation email' do
|
||||
let_it_be(:owner) { create(:user) }
|
||||
let_it_be(:group) { create(:group, name: 'Owned') }
|
||||
let_it_be(:project) { create(:project, :repository, namespace: group) }
|
||||
|
||||
let(:invite_email) { generate(:email) }
|
||||
let(:extra_params) { { invite_type: Emails::Members::INITIAL_INVITE } }
|
||||
let(:group_invite) do
|
||||
create(
|
||||
:group_member, :invited,
|
||||
group: group,
|
||||
invite_email: invite_email,
|
||||
created_by: owner
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
project.add_maintainer(owner)
|
||||
group.add_owner(owner)
|
||||
group_invite.generate_invite_token!
|
||||
|
||||
mock_auth_hash(provider, uid, invite_email, additional_info: additional_info)
|
||||
end
|
||||
|
||||
it 'redirects to the activity page with all the projects/groups invitations accepted' do
|
||||
visit invite_path(group_invite.raw_invite_token, extra_params)
|
||||
click_link_or_button "oauth-login-#{provider}"
|
||||
fill_in_welcome_form
|
||||
|
||||
expect(page).to have_content('You have been granted Owner access to group Owned.')
|
||||
expect(page).to have_current_path(activity_group_path(group), ignore_query: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fill_in_welcome_form
|
||||
select 'Software Developer', from: 'user_role'
|
||||
click_button 'Get started!'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -87,8 +87,8 @@ describe('Delete user modal', () => {
|
|||
});
|
||||
|
||||
it('has disabled buttons', () => {
|
||||
expect(findPrimaryButton().attributes('disabled')).toBeTruthy();
|
||||
expect(findSecondaryButton().attributes('disabled')).toBeTruthy();
|
||||
expect(findPrimaryButton().attributes('disabled')).toBe('true');
|
||||
expect(findSecondaryButton().attributes('disabled')).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -105,8 +105,8 @@ describe('Delete user modal', () => {
|
|||
});
|
||||
|
||||
it('has disabled buttons', () => {
|
||||
expect(findPrimaryButton().attributes('disabled')).toBeTruthy();
|
||||
expect(findSecondaryButton().attributes('disabled')).toBeTruthy();
|
||||
expect(findPrimaryButton().attributes('disabled')).toBe('true');
|
||||
expect(findSecondaryButton().attributes('disabled')).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -123,8 +123,8 @@ describe('Delete user modal', () => {
|
|||
});
|
||||
|
||||
it('has enabled buttons', () => {
|
||||
expect(findPrimaryButton().attributes('disabled')).toBeFalsy();
|
||||
expect(findSecondaryButton().attributes('disabled')).toBeFalsy();
|
||||
expect(findPrimaryButton().attributes('disabled')).toBeUndefined();
|
||||
expect(findSecondaryButton().attributes('disabled')).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('when primary action is clicked', () => {
|
||||
|
|
|
@ -58,7 +58,7 @@ describe('Ci variable modal', () => {
|
|||
});
|
||||
|
||||
it('button is disabled when no key/value pair are present', () => {
|
||||
expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy();
|
||||
expect(findAddorUpdateButton().attributes('disabled')).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -71,7 +71,7 @@ describe('Ci variable modal', () => {
|
|||
});
|
||||
|
||||
it('button is enabled when key/value pair are present', () => {
|
||||
expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy();
|
||||
expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Add variable button dispatches addVariable action', () => {
|
||||
|
@ -249,7 +249,7 @@ describe('Ci variable modal', () => {
|
|||
});
|
||||
|
||||
it('disables the submit button', () => {
|
||||
expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy();
|
||||
expect(findAddorUpdateButton().attributes('disabled')).toBe('disabled');
|
||||
});
|
||||
|
||||
it('shows the correct error text', () => {
|
||||
|
@ -316,7 +316,7 @@ describe('Ci variable modal', () => {
|
|||
});
|
||||
|
||||
it('does not disable the submit button', () => {
|
||||
expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy();
|
||||
expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -261,7 +261,7 @@ describe('Client side Markdown processing', () => {
|
|||
...source('<img src="bar" alt="foo" />'),
|
||||
alt: 'foo',
|
||||
canonicalSrc: 'bar',
|
||||
src: 'http://test.host/bar',
|
||||
src: 'bar',
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
@ -283,7 +283,7 @@ describe('Client side Markdown processing', () => {
|
|||
image({
|
||||
...source('<img src="bar" alt="foo" />'),
|
||||
alt: 'foo',
|
||||
src: 'http://test.host/bar',
|
||||
src: 'bar',
|
||||
canonicalSrc: 'bar',
|
||||
}),
|
||||
),
|
||||
|
@ -297,7 +297,7 @@ describe('Client side Markdown processing', () => {
|
|||
link(
|
||||
{
|
||||
...source('[GitLab](https://gitlab.com "Go to GitLab")'),
|
||||
href: 'https://gitlab.com/',
|
||||
href: 'https://gitlab.com',
|
||||
canonicalSrc: 'https://gitlab.com',
|
||||
title: 'Go to GitLab',
|
||||
},
|
||||
|
@ -316,7 +316,7 @@ describe('Client side Markdown processing', () => {
|
|||
link(
|
||||
{
|
||||
...source('[GitLab](https://gitlab.com "Go to GitLab")'),
|
||||
href: 'https://gitlab.com/',
|
||||
href: 'https://gitlab.com',
|
||||
canonicalSrc: 'https://gitlab.com',
|
||||
title: 'Go to GitLab',
|
||||
},
|
||||
|
@ -335,7 +335,7 @@ describe('Client side Markdown processing', () => {
|
|||
{
|
||||
...source('www.commonmark.org'),
|
||||
canonicalSrc: 'http://www.commonmark.org',
|
||||
href: 'http://www.commonmark.org/',
|
||||
href: 'http://www.commonmark.org',
|
||||
},
|
||||
'www.commonmark.org',
|
||||
),
|
||||
|
@ -389,7 +389,7 @@ describe('Client side Markdown processing', () => {
|
|||
sourceMapKey: null,
|
||||
sourceMarkdown: null,
|
||||
canonicalSrc: 'https://gitlab.com',
|
||||
href: 'https://gitlab.com/',
|
||||
href: 'https://gitlab.com',
|
||||
},
|
||||
'https://gitlab.com',
|
||||
),
|
||||
|
@ -616,7 +616,7 @@ two
|
|||
...source('![bar](foo.png)'),
|
||||
alt: 'bar',
|
||||
canonicalSrc: 'foo.png',
|
||||
src: 'http://test.host/foo.png',
|
||||
src: 'foo.png',
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
@ -969,12 +969,12 @@ Paragraph
|
|||
{
|
||||
...source('[![moon](moon.jpg)](/uri)'),
|
||||
canonicalSrc: '/uri',
|
||||
href: 'http://test.host/uri',
|
||||
href: '/uri',
|
||||
},
|
||||
image({
|
||||
...source('![moon](moon.jpg)'),
|
||||
canonicalSrc: 'moon.jpg',
|
||||
src: 'http://test.host/moon.jpg',
|
||||
src: 'moon.jpg',
|
||||
alt: 'moon',
|
||||
}),
|
||||
),
|
||||
|
@ -1010,7 +1010,7 @@ Paragraph
|
|||
{
|
||||
...source('[moon](moon.jpg)'),
|
||||
canonicalSrc: 'moon.jpg',
|
||||
href: 'http://test.host/moon.jpg',
|
||||
href: 'moon.jpg',
|
||||
},
|
||||
'moon',
|
||||
),
|
||||
|
@ -1021,7 +1021,7 @@ Paragraph
|
|||
link(
|
||||
{
|
||||
...source('[sun](sun.jpg)'),
|
||||
href: 'http://test.host/sun.jpg',
|
||||
href: 'sun.jpg',
|
||||
canonicalSrc: 'sun.jpg',
|
||||
},
|
||||
'sun',
|
||||
|
@ -1141,7 +1141,7 @@ _world_.
|
|||
link(
|
||||
{
|
||||
...source('[GitLab][gitlab-url]'),
|
||||
href: 'https://gitlab.com/',
|
||||
href: 'https://gitlab.com',
|
||||
canonicalSrc: 'https://gitlab.com',
|
||||
title: 'GitLab',
|
||||
},
|
||||
|
@ -1235,4 +1235,72 @@ body {
|
|||
expect(tiptapEditor.getHTML()).toEqual(expectedHtml);
|
||||
},
|
||||
);
|
||||
|
||||
describe('attribute sanitization', () => {
|
||||
// eslint-disable-next-line no-script-url
|
||||
const protocolBasedInjectionSimpleNoSpaces = "javascript:alert('XSS');";
|
||||
// eslint-disable-next-line no-script-url
|
||||
const protocolBasedInjectionSimpleSpacesBefore = "javascript: alert('XSS');";
|
||||
|
||||
const docWithImageFactory = (urlInput, urlOutput) => {
|
||||
const input = `<img src="${urlInput}">`;
|
||||
|
||||
return {
|
||||
input,
|
||||
expectedDoc: doc(
|
||||
paragraph(
|
||||
source(input),
|
||||
image({
|
||||
...source(input),
|
||||
src: urlOutput,
|
||||
canonicalSrc: urlOutput,
|
||||
}),
|
||||
),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const docWithLinkFactory = (urlInput, urlOutput) => {
|
||||
const input = `<a href="${urlInput}">foo</a>`;
|
||||
|
||||
return {
|
||||
input,
|
||||
expectedDoc: doc(
|
||||
paragraph(
|
||||
source(input),
|
||||
link({ ...source(input), href: urlOutput, canonicalSrc: urlOutput }, 'foo'),
|
||||
),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
it.each`
|
||||
desc | urlInput | urlOutput
|
||||
${'protocol-based JS injection: simple, no spaces'} | ${protocolBasedInjectionSimpleNoSpaces} | ${null}
|
||||
${'protocol-based JS injection: simple, spaces before'} | ${"javascript :alert('XSS');"} | ${null}
|
||||
${'protocol-based JS injection: simple, spaces after'} | ${protocolBasedInjectionSimpleSpacesBefore} | ${null}
|
||||
${'protocol-based JS injection: simple, spaces before and after'} | ${"javascript : alert('XSS');"} | ${null}
|
||||
${'protocol-based JS injection: UTF-8 encoding'} | ${'javascript:'} | ${null}
|
||||
${'protocol-based JS injection: long UTF-8 encoding'} | ${'javascript:'} | ${null}
|
||||
${'protocol-based JS injection: long UTF-8 encoding without semicolons'} | ${'javascript:alert('XSS')'} | ${null}
|
||||
${'protocol-based JS injection: hex encoding'} | ${'javascript:'} | ${null}
|
||||
${'protocol-based JS injection: long hex encoding'} | ${'javascript:'} | ${null}
|
||||
${'protocol-based JS injection: hex encoding without semicolons'} | ${'javascript:alert('XSS')'} | ${null}
|
||||
${'protocol-based JS injection: Unicode'} | ${"\u0001java\u0003script:alert('XSS')"} | ${null}
|
||||
${'protocol-based JS injection: spaces and entities'} | ${" javascript:alert('XSS');"} | ${null}
|
||||
${'vbscript'} | ${'vbscript:alert(document.domain)'} | ${null}
|
||||
${'protocol-based JS injection: preceding colon'} | ${":javascript:alert('XSS');"} | ${":javascript:alert('XSS');"}
|
||||
${'protocol-based JS injection: null char'} | ${"java\0script:alert('XSS')"} | ${"java<76>script:alert('XSS')"}
|
||||
${'protocol-based JS injection: invalid URL char'} | ${"java\\script:alert('XSS')"} | ${"java\\script:alert('XSS')"}
|
||||
`('sanitize $desc:\n\tURL "$urlInput" becomes "$urlOutput"', ({ urlInput, urlOutput }) => {
|
||||
const exampleFactories = [docWithImageFactory, docWithLinkFactory];
|
||||
|
||||
exampleFactories.forEach(async (exampleFactory) => {
|
||||
const { input, expectedDoc } = exampleFactory(urlInput, urlOutput);
|
||||
const document = await deserialize(input);
|
||||
|
||||
expect(document.toJSON()).toEqual(expectedDoc.toJSON());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1213,47 +1213,47 @@ paragraph
|
|||
};
|
||||
|
||||
it.each`
|
||||
mark | markdown | modifiedMarkdown | editAction
|
||||
${'bold'} | ${'**bold**'} | ${'**bold modified**'} | ${defaultEditAction}
|
||||
${'bold'} | ${'__bold__'} | ${'__bold modified__'} | ${defaultEditAction}
|
||||
${'bold'} | ${'<strong>bold</strong>'} | ${'<strong>bold modified</strong>'} | ${defaultEditAction}
|
||||
${'bold'} | ${'<b>bold</b>'} | ${'<b>bold modified</b>'} | ${defaultEditAction}
|
||||
${'italic'} | ${'_italic_'} | ${'_italic modified_'} | ${defaultEditAction}
|
||||
${'italic'} | ${'*italic*'} | ${'*italic modified*'} | ${defaultEditAction}
|
||||
${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'} | ${defaultEditAction}
|
||||
${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'} | ${defaultEditAction}
|
||||
${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${defaultEditAction}
|
||||
${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com/">link modified</a>'} | ${defaultEditAction}
|
||||
${'link'} | ${'link www.gitlab.com'} | ${'modified link www.gitlab.com'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link https://www.gitlab.com'} | ${'modified link https://www.gitlab.com'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link(https://www.gitlab.com)'} | ${'modified link(https://www.gitlab.com)'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link(engineering@gitlab.com)'} | ${'modified link(engineering@gitlab.com)'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link <https://www.gitlab.com>'} | ${'modified link <https://www.gitlab.com>'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link [https://www.gitlab.com>'} | ${'modified link \\[https://www.gitlab.com>'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link <https://www.gitlab.com'} | ${'modified link <https://www.gitlab.com'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link https://www.gitlab.com>'} | ${'modified link [https://www.gitlab.com>](https://www.gitlab.com%3E)'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link https://www.gitlab.com/path'} | ${'modified link https://www.gitlab.com/path'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link https://www.gitlab.com?query=search'} | ${'modified link https://www.gitlab.com?query=search'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link https://www.gitlab.com/#fragment'} | ${'modified link https://www.gitlab.com/#fragment'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link https://www.gitlab.com/?query=search'} | ${'modified link https://www.gitlab.com/?query=search'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link https://www.gitlab.com#fragment'} | ${'modified link https://www.gitlab.com#fragment'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link **https://www.gitlab.com]**'} | ${'modified link [**https://www.gitlab.com\\]**](https://www.gitlab.com%5D)'} | ${prependContentEditAction}
|
||||
${'code'} | ${'`code`'} | ${'`code modified`'} | ${defaultEditAction}
|
||||
${'code'} | ${'<code>code</code>'} | ${'<code>code modified</code>'} | ${defaultEditAction}
|
||||
${'strike'} | ${'~~striked~~'} | ${'~~striked modified~~'} | ${defaultEditAction}
|
||||
${'strike'} | ${'<del>striked</del>'} | ${'<del>striked modified</del>'} | ${defaultEditAction}
|
||||
${'strike'} | ${'<strike>striked</strike>'} | ${'<strike>striked modified</strike>'} | ${defaultEditAction}
|
||||
${'strike'} | ${'<s>striked</s>'} | ${'<s>striked modified</s>'} | ${defaultEditAction}
|
||||
${'list'} | ${'- list item'} | ${'- list item modified'} | ${defaultEditAction}
|
||||
${'list'} | ${'* list item'} | ${'* list item modified'} | ${defaultEditAction}
|
||||
${'list'} | ${'+ list item'} | ${'+ list item modified'} | ${defaultEditAction}
|
||||
${'list'} | ${'- list item 1\n- list item 2'} | ${'- list item 1\n- list item 2 modified'} | ${defaultEditAction}
|
||||
${'list'} | ${'2) list item'} | ${'2) list item modified'} | ${defaultEditAction}
|
||||
${'list'} | ${'1. list item'} | ${'1. list item modified'} | ${defaultEditAction}
|
||||
${'taskList'} | ${'2) [ ] task list item'} | ${'2) [ ] task list item modified'} | ${defaultEditAction}
|
||||
${'taskList'} | ${'2) [x] task list item'} | ${'2) [x] task list item modified'} | ${defaultEditAction}
|
||||
${'image'} | ${'![image](image.png)'} | ${'![image](image.png) modified'} | ${defaultEditAction}
|
||||
${'footnoteReference'} | ${'[^1] footnote\n\n[^1]: footnote definition'} | ${'modified [^1] footnote\n\n[^1]: footnote definition'} | ${prependContentEditAction}
|
||||
mark | markdown | modifiedMarkdown | editAction
|
||||
${'bold'} | ${'**bold**'} | ${'**bold modified**'} | ${defaultEditAction}
|
||||
${'bold'} | ${'__bold__'} | ${'__bold modified__'} | ${defaultEditAction}
|
||||
${'bold'} | ${'<strong>bold</strong>'} | ${'<strong>bold modified</strong>'} | ${defaultEditAction}
|
||||
${'bold'} | ${'<b>bold</b>'} | ${'<b>bold modified</b>'} | ${defaultEditAction}
|
||||
${'italic'} | ${'_italic_'} | ${'_italic modified_'} | ${defaultEditAction}
|
||||
${'italic'} | ${'*italic*'} | ${'*italic modified*'} | ${defaultEditAction}
|
||||
${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'} | ${defaultEditAction}
|
||||
${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'} | ${defaultEditAction}
|
||||
${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${defaultEditAction}
|
||||
${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com">link modified</a>'} | ${defaultEditAction}
|
||||
${'link'} | ${'link www.gitlab.com'} | ${'modified link www.gitlab.com'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link https://www.gitlab.com'} | ${'modified link https://www.gitlab.com'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link(https://www.gitlab.com)'} | ${'modified link(https://www.gitlab.com)'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link(engineering@gitlab.com)'} | ${'modified link(engineering@gitlab.com)'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link <https://www.gitlab.com>'} | ${'modified link <https://www.gitlab.com>'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link [https://www.gitlab.com>'} | ${'modified link \\[https://www.gitlab.com>'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link <https://www.gitlab.com'} | ${'modified link <https://www.gitlab.com'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link https://www.gitlab.com>'} | ${'modified link https://www.gitlab.com>'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link https://www.gitlab.com/path'} | ${'modified link https://www.gitlab.com/path'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link https://www.gitlab.com?query=search'} | ${'modified link https://www.gitlab.com?query=search'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link https://www.gitlab.com/#fragment'} | ${'modified link https://www.gitlab.com/#fragment'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link https://www.gitlab.com/?query=search'} | ${'modified link https://www.gitlab.com/?query=search'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link https://www.gitlab.com#fragment'} | ${'modified link https://www.gitlab.com#fragment'} | ${prependContentEditAction}
|
||||
${'link'} | ${'link **https://www.gitlab.com]**'} | ${'modified link **https://www.gitlab.com\\]**'} | ${prependContentEditAction}
|
||||
${'code'} | ${'`code`'} | ${'`code modified`'} | ${defaultEditAction}
|
||||
${'code'} | ${'<code>code</code>'} | ${'<code>code modified</code>'} | ${defaultEditAction}
|
||||
${'strike'} | ${'~~striked~~'} | ${'~~striked modified~~'} | ${defaultEditAction}
|
||||
${'strike'} | ${'<del>striked</del>'} | ${'<del>striked modified</del>'} | ${defaultEditAction}
|
||||
${'strike'} | ${'<strike>striked</strike>'} | ${'<strike>striked modified</strike>'} | ${defaultEditAction}
|
||||
${'strike'} | ${'<s>striked</s>'} | ${'<s>striked modified</s>'} | ${defaultEditAction}
|
||||
${'list'} | ${'- list item'} | ${'- list item modified'} | ${defaultEditAction}
|
||||
${'list'} | ${'* list item'} | ${'* list item modified'} | ${defaultEditAction}
|
||||
${'list'} | ${'+ list item'} | ${'+ list item modified'} | ${defaultEditAction}
|
||||
${'list'} | ${'- list item 1\n- list item 2'} | ${'- list item 1\n- list item 2 modified'} | ${defaultEditAction}
|
||||
${'list'} | ${'2) list item'} | ${'2) list item modified'} | ${defaultEditAction}
|
||||
${'list'} | ${'1. list item'} | ${'1. list item modified'} | ${defaultEditAction}
|
||||
${'taskList'} | ${'2) [ ] task list item'} | ${'2) [ ] task list item modified'} | ${defaultEditAction}
|
||||
${'taskList'} | ${'2) [x] task list item'} | ${'2) [x] task list item modified'} | ${defaultEditAction}
|
||||
${'image'} | ${'![image](image.png)'} | ${'![image](image.png) modified'} | ${defaultEditAction}
|
||||
${'footnoteReference'} | ${'[^1] footnote\n\n[^1]: footnote definition'} | ${'modified [^1] footnote\n\n[^1]: footnote definition'} | ${prependContentEditAction}
|
||||
`(
|
||||
'preserves original $mark syntax when sourceMarkdown is available for $markdown',
|
||||
async ({ markdown, modifiedMarkdown, editAction }) => {
|
||||
|
|
|
@ -119,7 +119,7 @@ describe('Issuable output', () => {
|
|||
|
||||
expect(findEdited().exists()).toBe(true);
|
||||
expect(findEdited().props('updatedByPath')).toMatch(/\/some_user$/);
|
||||
expect(findEdited().props('updatedAt')).toBeTruthy();
|
||||
expect(findEdited().props('updatedAt')).toBe(initialRequest.updated_at);
|
||||
expect(wrapper.vm.state.lock_version).toBe(initialRequest.lock_version);
|
||||
})
|
||||
.then(() => {
|
||||
|
@ -133,7 +133,7 @@ describe('Issuable output', () => {
|
|||
expect(findEdited().exists()).toBe(true);
|
||||
expect(findEdited().props('updatedByName')).toBe('Other User');
|
||||
expect(findEdited().props('updatedByPath')).toMatch(/\/other_user$/);
|
||||
expect(findEdited().props('updatedAt')).toBeTruthy();
|
||||
expect(findEdited().props('updatedAt')).toBe(secondRequest.updated_at);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -148,7 +148,7 @@ describe('ProjectDropdown', () => {
|
|||
});
|
||||
|
||||
it('emits `error` event', () => {
|
||||
expect(wrapper.emitted('error')).toBeTruthy();
|
||||
expect(wrapper.emitted('error')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -124,7 +124,7 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
|
|||
});
|
||||
|
||||
it('clicked on link with view', () => {
|
||||
expect(primaryLink.props('menuItem').view).toBeTruthy();
|
||||
expect(primaryLink.props('menuItem').view).toBe(TEST_NAV_DATA.views.projects.namespace);
|
||||
});
|
||||
|
||||
it('changes active view', () => {
|
||||
|
|
|
@ -357,7 +357,7 @@ describe('issue_note', () => {
|
|||
createWrapper();
|
||||
updateActions();
|
||||
wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', params);
|
||||
expect(wrapper.emitted('handleUpdateNote')).toBeTruthy();
|
||||
expect(wrapper.emitted('handleUpdateNote')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('does not stringify empty position', () => {
|
||||
|
|
|
@ -102,7 +102,7 @@ describe('Pipelines filtered search', () => {
|
|||
it('emits filterPipelines on submit with correct filter', () => {
|
||||
findFilteredSearch().vm.$emit('submit', mockSearch);
|
||||
|
||||
expect(wrapper.emitted('filterPipelines')).toBeTruthy();
|
||||
expect(wrapper.emitted('filterPipelines')).toHaveLength(1);
|
||||
expect(wrapper.emitted('filterPipelines')[0]).toEqual([mockSearch]);
|
||||
});
|
||||
|
||||
|
|
|
@ -50,39 +50,33 @@ describe('PrometheusMetrics', () => {
|
|||
customMetrics.showMonitoringCustomMetricsPanelState(PANEL_STATE.LOADING);
|
||||
|
||||
expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toEqual(false);
|
||||
expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBeTruthy();
|
||||
expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBeTruthy();
|
||||
expect(
|
||||
customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'),
|
||||
).toBeTruthy();
|
||||
expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBe(true);
|
||||
expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBe(true);
|
||||
expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true);
|
||||
|
||||
expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBeTruthy();
|
||||
expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy();
|
||||
expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBe(true);
|
||||
expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true);
|
||||
});
|
||||
|
||||
it('should show metrics list when called with `list`', () => {
|
||||
customMetrics.showMonitoringCustomMetricsPanelState(PANEL_STATE.LIST);
|
||||
|
||||
expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy();
|
||||
expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBeTruthy();
|
||||
expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true);
|
||||
expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBe(true);
|
||||
expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toEqual(false);
|
||||
expect(
|
||||
customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'),
|
||||
).toBeTruthy();
|
||||
expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true);
|
||||
|
||||
expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toEqual(false);
|
||||
expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy();
|
||||
expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true);
|
||||
});
|
||||
|
||||
it('should show empty state when called with `empty`', () => {
|
||||
customMetrics.showMonitoringCustomMetricsPanelState(PANEL_STATE.EMPTY);
|
||||
|
||||
expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy();
|
||||
expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true);
|
||||
expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toEqual(false);
|
||||
expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBeTruthy();
|
||||
expect(
|
||||
customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'),
|
||||
).toBeTruthy();
|
||||
expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBe(true);
|
||||
expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true);
|
||||
|
||||
expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toEqual(false);
|
||||
expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toEqual(false);
|
||||
|
@ -94,14 +88,12 @@ describe('PrometheusMetrics', () => {
|
|||
|
||||
const $metricsListLi = customMetrics.$monitoredCustomMetricsList.find('li');
|
||||
|
||||
expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy();
|
||||
expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true);
|
||||
expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toEqual(false);
|
||||
expect(
|
||||
customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'),
|
||||
).toBeTruthy();
|
||||
expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true);
|
||||
|
||||
expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toEqual(false);
|
||||
expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy();
|
||||
expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true);
|
||||
|
||||
expect($metricsListLi.length).toEqual(metrics.length);
|
||||
});
|
||||
|
@ -114,10 +106,10 @@ describe('PrometheusMetrics', () => {
|
|||
false,
|
||||
);
|
||||
|
||||
expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy();
|
||||
expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBeTruthy();
|
||||
expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBeTruthy();
|
||||
expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy();
|
||||
expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true);
|
||||
expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBe(true);
|
||||
expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBe(true);
|
||||
expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -54,25 +54,25 @@ describe('PrometheusMetrics', () => {
|
|||
it('should show loading state when called with `loading`', () => {
|
||||
prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.LOADING);
|
||||
|
||||
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy();
|
||||
expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeTruthy();
|
||||
expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeTruthy();
|
||||
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(false);
|
||||
expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(true);
|
||||
expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(true);
|
||||
});
|
||||
|
||||
it('should show metrics list when called with `list`', () => {
|
||||
prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.LIST);
|
||||
|
||||
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
|
||||
expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeTruthy();
|
||||
expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeFalsy();
|
||||
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
|
||||
expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(true);
|
||||
expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(false);
|
||||
});
|
||||
|
||||
it('should show empty state when called with `empty`', () => {
|
||||
prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY);
|
||||
|
||||
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
|
||||
expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy();
|
||||
expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeTruthy();
|
||||
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
|
||||
expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(false);
|
||||
expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -88,8 +88,8 @@ describe('PrometheusMetrics', () => {
|
|||
|
||||
const $metricsListLi = prometheusMetrics.$monitoredMetricsList.find('li');
|
||||
|
||||
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
|
||||
expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeFalsy();
|
||||
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
|
||||
expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(false);
|
||||
|
||||
expect(prometheusMetrics.$monitoredMetricsCount.text()).toEqual(
|
||||
'3 exporters with 12 metrics were found',
|
||||
|
@ -102,8 +102,8 @@ describe('PrometheusMetrics', () => {
|
|||
it('should show missing environment variables list', () => {
|
||||
prometheusMetrics.populateActiveMetrics(missingVarMetrics);
|
||||
|
||||
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
|
||||
expect(prometheusMetrics.$missingEnvVarPanel.hasClass('hidden')).toBeFalsy();
|
||||
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
|
||||
expect(prometheusMetrics.$missingEnvVarPanel.hasClass('hidden')).toBe(false);
|
||||
|
||||
expect(prometheusMetrics.$missingEnvVarMetricCount.text()).toEqual('2');
|
||||
expect(prometheusMetrics.$missingEnvVarPanel.find('li').length).toEqual(2);
|
||||
|
@ -143,12 +143,12 @@ describe('PrometheusMetrics', () => {
|
|||
|
||||
prometheusMetrics.loadActiveMetrics();
|
||||
|
||||
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy();
|
||||
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(false);
|
||||
expect(axios.get).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
|
||||
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
|
||||
});
|
||||
|
||||
it('should show empty state if response failed to load', async () => {
|
||||
|
@ -158,8 +158,8 @@ describe('PrometheusMetrics', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
|
||||
expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy();
|
||||
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
|
||||
expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(false);
|
||||
});
|
||||
|
||||
it('should populate metrics list once response is loaded', async () => {
|
||||
|
|
|
@ -35,6 +35,7 @@ RSpec.describe GitlabSchema.types['ServiceType'] do
|
|||
PIPELINES_EMAIL_SERVICE
|
||||
PIVOTALTRACKER_SERVICE
|
||||
PROMETHEUS_SERVICE
|
||||
PUMBLE_SERVICE
|
||||
PUSHOVER_SERVICE
|
||||
REDMINE_SERVICE
|
||||
SHIMO_SERVICE
|
||||
|
|
|
@ -68,6 +68,49 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
|
|||
describe '#valid?' do
|
||||
subject { validator.valid? }
|
||||
|
||||
context 'when given a supported MAJOR.MINOR schema version' do
|
||||
let(:report_type) { :dast }
|
||||
let(:report_version) do
|
||||
latest_vendored_version = described_class::SUPPORTED_VERSIONS[report_type].last.split(".")
|
||||
(latest_vendored_version[0...2] << "34").join(".")
|
||||
end
|
||||
|
||||
context 'and the report is valid' do
|
||||
let(:report_data) do
|
||||
{
|
||||
'version' => report_version,
|
||||
'vulnerabilities' => []
|
||||
}
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'and the report is invalid' do
|
||||
let(:report_data) do
|
||||
{
|
||||
'version' => report_version
|
||||
}
|
||||
end
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
|
||||
it 'logs related information' do
|
||||
expect(Gitlab::AppLogger).to receive(:info).with(
|
||||
message: "security report schema validation problem",
|
||||
security_report_type: report_type,
|
||||
security_report_version: report_version,
|
||||
project_id: project.id,
|
||||
security_report_failure: 'schema_validation_fails',
|
||||
security_report_scanner_id: 'gemnasium',
|
||||
security_report_scanner_version: '2.1.0'
|
||||
)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when given a supported schema version' do
|
||||
let(:report_type) { :dast }
|
||||
let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last }
|
||||
|
@ -320,6 +363,11 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
|
|||
context 'when given an unsupported schema version' do
|
||||
let(:report_type) { :dast }
|
||||
let(:report_version) { "12.37.0" }
|
||||
let(:expected_unsupported_message) do
|
||||
"Version #{report_version} for report type #{report_type} is unsupported, supported versions for this report type are: "\
|
||||
"#{supported_dast_versions}. GitLab will attempt to validate this report against the earliest supported "\
|
||||
"versions of this report type, to show all the errors but will not ingest the report"
|
||||
end
|
||||
|
||||
context 'and the report is valid' do
|
||||
let(:report_data) do
|
||||
|
@ -331,7 +379,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
|
|||
|
||||
let(:expected_errors) do
|
||||
[
|
||||
"Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: #{supported_dast_versions}"
|
||||
expected_unsupported_message
|
||||
]
|
||||
end
|
||||
|
||||
|
@ -347,7 +395,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
|
|||
|
||||
let(:expected_errors) do
|
||||
[
|
||||
"Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: #{supported_dast_versions}",
|
||||
expected_unsupported_message,
|
||||
"root is missing required keys: vulnerabilities"
|
||||
]
|
||||
end
|
||||
|
@ -359,6 +407,12 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
|
|||
context 'when not given a schema version' do
|
||||
let(:report_type) { :dast }
|
||||
let(:report_version) { nil }
|
||||
let(:expected_missing_version_message) do
|
||||
"Report version not provided, #{report_type} report type supports versions: #{supported_dast_versions}. GitLab "\
|
||||
"will attempt to validate this report against the earliest supported versions of this report type, to show all "\
|
||||
"the errors but will not ingest the report"
|
||||
end
|
||||
|
||||
let(:report_data) do
|
||||
{
|
||||
'vulnerabilities' => []
|
||||
|
@ -368,7 +422,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
|
|||
let(:expected_errors) do
|
||||
[
|
||||
"root is missing required keys: version",
|
||||
"Report version not provided, dast report type supports versions: #{supported_dast_versions}"
|
||||
expected_missing_version_message
|
||||
]
|
||||
end
|
||||
|
||||
|
@ -414,9 +468,14 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
|
|||
end
|
||||
|
||||
let(:report_version) { described_class::DEPRECATED_VERSIONS[report_type].last }
|
||||
let(:expected_deprecation_message) do
|
||||
"Version #{report_version} for report type #{report_type} has been deprecated, supported versions for this "\
|
||||
"report type are: #{supported_dast_versions}. GitLab will attempt to parse and ingest this report if valid."
|
||||
end
|
||||
|
||||
let(:expected_deprecation_warnings) do
|
||||
[
|
||||
"Version V2.7.0 for report type dast has been deprecated, supported versions for this report type are: #{supported_dast_versions}"
|
||||
expected_deprecation_message
|
||||
]
|
||||
end
|
||||
|
||||
|
@ -464,6 +523,62 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
|
|||
describe '#warnings' do
|
||||
subject { validator.warnings }
|
||||
|
||||
context 'when given a supported MAJOR.MINOR schema version' do
|
||||
let(:report_type) { :dast }
|
||||
let(:report_version) do
|
||||
latest_vendored_version = described_class::SUPPORTED_VERSIONS[report_type].last.split(".")
|
||||
(latest_vendored_version[0...2] << "34").join(".")
|
||||
end
|
||||
|
||||
let(:latest_patch_version) do
|
||||
::Security::ReportSchemaVersionMatcher.new(
|
||||
report_declared_version: report_version,
|
||||
supported_versions: described_class::SUPPORTED_VERSIONS[report_type]
|
||||
).call
|
||||
end
|
||||
|
||||
let(:message) do
|
||||
"This report uses a supported MAJOR.MINOR schema version but the PATCH version doesn't match"\
|
||||
" any vendored schema version. Validation will be attempted against version"\
|
||||
" #{latest_patch_version}"
|
||||
end
|
||||
|
||||
context 'and the report is valid' do
|
||||
let(:report_data) do
|
||||
{
|
||||
'version' => report_version,
|
||||
'vulnerabilities' => []
|
||||
}
|
||||
end
|
||||
|
||||
it { is_expected.to match_array([message]) }
|
||||
end
|
||||
|
||||
context 'and the report is invalid' do
|
||||
let(:report_data) do
|
||||
{
|
||||
'version' => report_version
|
||||
}
|
||||
end
|
||||
|
||||
it { is_expected.to match_array([message]) }
|
||||
|
||||
it 'logs related information' do
|
||||
expect(Gitlab::AppLogger).to receive(:info).with(
|
||||
message: "security report schema validation problem",
|
||||
security_report_type: report_type,
|
||||
security_report_version: report_version,
|
||||
project_id: project.id,
|
||||
security_report_failure: 'schema_validation_fails',
|
||||
security_report_scanner_id: 'gemnasium',
|
||||
security_report_scanner_version: '2.1.0'
|
||||
)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when given a supported schema version' do
|
||||
let(:report_type) { :dast }
|
||||
let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last }
|
||||
|
|
|
@ -597,6 +597,7 @@ project:
|
|||
- alert_management_alerts
|
||||
- repository_storage_moves
|
||||
- freeze_periods
|
||||
- pumble_integration
|
||||
- webex_teams_integration
|
||||
- build_report_results
|
||||
- vulnerability_statistic
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Security::ReportSchemaVersionMatcher do
|
||||
let(:vendored_versions) { %w[14.0.0 14.0.1 14.0.2 14.1.0] }
|
||||
let(:version_finder) do
|
||||
described_class.new(
|
||||
report_declared_version: report_version,
|
||||
supported_versions: vendored_versions
|
||||
)
|
||||
end
|
||||
|
||||
describe '#call' do
|
||||
subject { version_finder.call }
|
||||
|
||||
context 'when minor version matches' do
|
||||
context 'and report schema patch version does not match any vendored schema versions' do
|
||||
context 'and report version is 14.1.1' do
|
||||
let(:report_version) { '14.1.1' }
|
||||
|
||||
it 'returns 14.1.0' do
|
||||
expect(subject).to eq('14.1.0')
|
||||
end
|
||||
end
|
||||
|
||||
context 'and report version is 14.0.32' do
|
||||
let(:report_version) { '14.0.32' }
|
||||
|
||||
it 'returns 14.0.2' do
|
||||
expect(subject).to eq('14.0.2')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when report minor version does not match' do
|
||||
let(:report_version) { '14.2.1' }
|
||||
|
||||
it 'does not return a version' do
|
||||
expect(subject).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Integrations::Pumble do
|
||||
it_behaves_like "chat integration", "Pumble" do
|
||||
let(:client_arguments) { webhook_url }
|
||||
let(:payload) do
|
||||
{
|
||||
text: be_present
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -44,6 +44,7 @@ RSpec.describe Project, factory_default: :keep do
|
|||
it { is_expected.to have_one(:mattermost_integration) }
|
||||
it { is_expected.to have_one(:hangouts_chat_integration) }
|
||||
it { is_expected.to have_one(:unify_circuit_integration) }
|
||||
it { is_expected.to have_one(:pumble_integration) }
|
||||
it { is_expected.to have_one(:webex_teams_integration) }
|
||||
it { is_expected.to have_one(:packagist_integration) }
|
||||
it { is_expected.to have_one(:pushover_integration) }
|
||||
|
|
|
@ -66,6 +66,7 @@ RSpec.describe API::Integrations do
|
|||
mattermost: %i[deployment_channel labels_to_be_notified],
|
||||
mock_ci: %i[enable_ssl_verification],
|
||||
prometheus: %i[manual_configuration],
|
||||
pumble: %i[branches_to_be_notified notify_only_broken_pipelines],
|
||||
slack: %i[alert_events alert_channel deployment_channel labels_to_be_notified],
|
||||
unify_circuit: %i[branches_to_be_notified notify_only_broken_pipelines],
|
||||
webex_teams: %i[branches_to_be_notified notify_only_broken_pipelines]
|
||||
|
|
|
@ -8,9 +8,8 @@ Gem::Specification.new do |gem|
|
|||
gem.description = gem.summary
|
||||
gem.homepage = "https://github.com/tduehr/omniauth-cas3"
|
||||
|
||||
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
||||
gem.files = `git ls-files`.split("\n")
|
||||
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
||||
gem.files = Dir.glob("lib/**/*.*")
|
||||
gem.test_files = Dir.glob("spec/**/**/*.*")
|
||||
gem.name = "omniauth-cas3"
|
||||
gem.require_paths = ["lib"]
|
||||
gem.version = Omniauth::Cas3::VERSION
|
||||
|
|
16
yarn.lock
16
yarn.lock
|
@ -1051,15 +1051,15 @@
|
|||
stylelint-declaration-strict-value "1.8.0"
|
||||
stylelint-scss "4.2.0"
|
||||
|
||||
"@gitlab/svgs@2.33.0":
|
||||
version "2.33.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.33.0.tgz#e970ae10ee558e1e2b01116b2fe6ea25161a4609"
|
||||
integrity sha512-8B5pGmZ6QnywxmWCmqMTkJfPlETbx4R7AK7si8Jf2DyWZ7Agfg9NOdgBq++IuiVjbxBO7VTQcZbVSavxrce6QA==
|
||||
"@gitlab/svgs@3.1.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.1.0.tgz#0108498a17e2f79d16158015db0be764b406cc09"
|
||||
integrity sha512-kZ45VTQOgLdwQCLRSj7+aohF+6AUnAaoucR1CFY/6DPDLnNNGeflwsCLN0sFBKwx42HLxFfNwvDmKOMLdSQg5A==
|
||||
|
||||
"@gitlab/ui@42.25.0":
|
||||
version "42.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-42.25.0.tgz#d79873347be9868c4d3d3123295ce1f12967f330"
|
||||
integrity sha512-yxSQeLbhrPD4KKQPCo+glarlhoa4cj46j7mgQtTRbJFw2ZWPcpJ4xuujCb8GoyGPlHpWaS8VJyv3l+hwBQs3qg==
|
||||
"@gitlab/ui@43.5.0":
|
||||
version "43.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-43.5.0.tgz#c0652c99cd7ba9c69cef1cdf75c85b9164536e24"
|
||||
integrity sha512-mbWXKylbnEuCXZuNMVic7K6Dvo8hjwYQkpyVvxdpmTCY+eTOtjxenVHE4HgZ5G/7cjznRnj4WLk0Ot8AquifBQ==
|
||||
dependencies:
|
||||
"@popperjs/core" "^2.11.2"
|
||||
bootstrap-vue "2.20.1"
|
||||
|
|
Loading…
Reference in New Issue